├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── components ├── App.server.js ├── AuthButton.server.js ├── Cache.client.js ├── EditButton.client.js ├── LocationContext.client.js ├── Note.server.js ├── NoteEditor.client.js ├── NoteList.server.js ├── NoteListSkeleton.js ├── NotePreview.js ├── NoteSkeleton.js ├── Root.client.js ├── SearchField.client.js ├── SidebarNote.client.js ├── SidebarNote.js ├── Spinner.js └── TextWithMarkdown.js ├── libs ├── initSupabase.js ├── send-res-with-module-map.js └── send-res.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── api │ ├── auth.js │ ├── index.js │ └── notes │ │ ├── [id].js │ │ └── index.js └── index.js ├── public ├── checkmark.svg ├── chevron-down.svg ├── chevron-up.svg ├── cross.svg ├── favicon.ico ├── logo.svg ├── menu.svg ├── og.png └── x.svg ├── schema.sql ├── scripts ├── build.js └── client-react-loader.js ├── style.css └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | yarn-error.log 4 | .DS_Store 5 | .vscode 6 | 7 | .vercel 8 | 9 | .env 10 | 11 | libs/react-client-manifest.json 12 | libs/send-res.build.js 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | yarn-error.log 4 | .DS_Store 5 | .vscode 6 | 7 | .vercel 8 | 9 | .env 10 | 11 | libs/react-client-manifest.json 12 | libs/send-res.build.js 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vercel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ THIS REPO IS OUTDATED AND ARCHIVED 2 | 3 | For an up to date example of Next.js 13 Server Components, please refer to https://github.com/supabase/auth-helpers/tree/main/examples/nextjs-server-components 4 | 5 |
6 | 7 | # React Server Components in Next.js 8 | 9 | Experimental app of React Server Components with Next.js, based on [React Server Components Demo](https://github.com/reactjs/server-components-demo). 10 | **It's not ready for adoption. Use this in your projects at your own risk.** 11 | 12 | ## Development 13 | 14 | ### Prepare 15 | 16 | You need these environment variables to run this app (you can create a `.env` file): 17 | 18 | ``` 19 | ENDPOINT='http://localhost:3000' # need to be absolute url to run in prod/local 20 | NEXT_PUBLIC_ENDPOINT='http://localhost:3000' # same as above 21 | NEXT_PUBLIC_SUPABASE_URL='https://XXXX.supabase.co' # Supabase API URL: https://app.supabase.io/project/{YOUR_PROJECT}/settings/api 22 | NEXT_PUBLIC_SUPABASE_ANON_KEY='anon.key.xxx.yyy' # Supabase anon Key: https://app.supabase.io/project/{YOUR_PROJECT}/settings/api 23 | ``` 24 | 25 | ### Start 26 | 27 | 1. `yarn install` (this will trigger the postinstall command) 28 | 2. `yarn dev` 29 | 30 | Go to `localhost:3000` to view the application. 31 | 32 | ### Deploy 33 | 34 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext-server-components&env=ENDPOINT,NEXT_PUBLIC_ENDPOINT,NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY&project-name=next-server-components&repo-name=next-server-components&demo-title=React%20Server%20Components%20(Experimental%20Demo)&demo-description=Experimental%20demo%20of%20React%20Server%20Components%20with%20Next.js.%20&demo-url=https%3A%2F%2Fnext-server-components.supabase.vercel.app&demo-image=https%3A%2F%2Fnext-server-components.supabase.vercel.app%2Fog.png) 35 | 36 | ## Caveats 37 | 38 | - Only `.js` extension is supported. 39 | - Client / server components should be under the `components` directory. 40 | - Some React Hooks are not supported in server components, such as `useContext`. 41 | - You have to manually import `React` in your server components. 42 | 43 | ## How does it work? 44 | 45 | Application APIs: 46 | 47 | - `GET, POST /api/notes` (Get all notes, Create a new note) 48 | - `GET, PUT, DELETE /api/notes/` (Action for a specific note) 49 | 50 | React Server Components API (`pages/api/index.js`): 51 | 52 | - `GET /api` (render application and return the serialized components) 53 | 54 | Note: Some of the application APIs (`POST`, `PUT`, `DELETE`) will render and return the serialized components as well. The render logic is handled by `libs/send-res.js`. 55 | 56 | `libs/send-res.js` accepts the props (from `req.query.location` and `req.session.login`) that needs to be rendered by `components/App.server.js` (the component tree entry). Then, it renders the tree and streams it to `res` using: 57 | 58 | ```js 59 | pipeToNodeWritable(React.createElement(App, props), res, moduleMap) 60 | ``` 61 | 62 | `moduleMap` is generated by client-side Webpack (through Next.js). It traverses both `.server.js` and `.client.js` and generates the full module map from the `react-server-dom-webpack/plugin` Webpack plugin (see `next.config.js`). 63 | Then, we use a custom plugin to copy it to `libs/react-client-manifest.json` and include it from the lambdas (see `libs/send-res-with-module-map.js`). 64 | 65 | `App` is a special build of `components/App.server.js`, which removes all the client components (marked as special modules) because they're not accessible from the server. We bundled it together with `libs/send-res.js` with another Webpack loader into `libs/send-res.build.js`. The Webpack script and loader are under `scripts/`. It should run whenever a server component is updated. 66 | 67 | Finally, everything related to OAuth is inside `pages/api/auth.js`. It's a cookie-based session using GitHub for login. 68 | -------------------------------------------------------------------------------- /components/App.server.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | 3 | import SearchField from './SearchField.client' 4 | 5 | import Note from './Note.server' 6 | import NoteList from './NoteList.server' 7 | import AuthButton from './AuthButton.server' 8 | 9 | import NoteSkeleton from './NoteSkeleton' 10 | import NoteListSkeleton from './NoteListSkeleton' 11 | 12 | export default function App({ selectedId, isEditing, searchText, login }) { 13 | return ( 14 |
15 |
16 | 17 |
18 |
19 | 27 | React Notes 28 |
29 |
30 | 31 | 32 | Add 33 | 34 |
35 | 40 |
41 |
42 | }> 43 | 44 | 45 |
46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /components/AuthButton.server.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import EditButton from './EditButton.client' 4 | 5 | export default function AuthButton({ children, login, ...props }) { 6 | if (login) { 7 | return ( 8 | 9 | {children} 10 | {login.user_metadata.avatar_url && ( 11 | User Avatar 17 | )} 18 | 19 | ) 20 | } 21 | 22 | return ( 23 | 24 | Login to {children} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/Cache.client.js: -------------------------------------------------------------------------------- 1 | import { createFromFetch } from 'react-server-dom-webpack' 2 | 3 | const endpoint = process.env.NEXT_PUBLIC_ENDPOINT 4 | 5 | const cache = new Map() 6 | 7 | export function useRefresh() { 8 | return function refresh(key, seededResponse) { 9 | cache.clear() 10 | cache.set(key, seededResponse) 11 | } 12 | } 13 | 14 | export function useServerResponse(location) { 15 | const key = JSON.stringify(location) 16 | let response = cache.get(key) 17 | if (response) { 18 | return response 19 | } 20 | response = createFromFetch( 21 | fetch(endpoint + '/api?location=' + encodeURIComponent(key)) 22 | ) 23 | cache.set(key, response) 24 | return response 25 | } 26 | -------------------------------------------------------------------------------- /components/EditButton.client.js: -------------------------------------------------------------------------------- 1 | import React, { unstable_useTransition, useState } from 'react' 2 | import { Auth, Modal, Button } from '@supabase/ui' 3 | 4 | import { useLocation } from './LocationContext.client' 5 | import { supabase } from '../libs/initSupabase' 6 | 7 | export default function EditButton({ 8 | login, 9 | noteId, 10 | disabled, 11 | title, 12 | children, 13 | }) { 14 | const [, setLocation] = useLocation() 15 | const [startTransition, isPending] = unstable_useTransition() 16 | const isDraft = noteId == null 17 | const [modalVisible, setModalVisible] = useState(false) 18 | 19 | return ( 20 | <> 21 | setModalVisible(false)}>Close, 27 | ]} 28 | > 29 | 35 | 36 | 68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /components/LocationContext.client.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | export const LocationContext = createContext() 4 | export function useLocation() { 5 | return useContext(LocationContext) 6 | } 7 | -------------------------------------------------------------------------------- /components/Note.server.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { fetch } from 'react-fetch' 3 | import { format } from 'date-fns' 4 | 5 | import NotePreview from './NotePreview' 6 | import NoteEditor from './NoteEditor.client' 7 | import AuthButton from './AuthButton.server' 8 | 9 | const endpoint = process.env.ENDPOINT 10 | 11 | export default function Note({ selectedId, isEditing, login }) { 12 | const note = 13 | selectedId != null 14 | ? fetch(`${endpoint}/api/notes/${selectedId}`).json() 15 | : null 16 | 17 | if (note === null) { 18 | if (isEditing) { 19 | return 20 | } else { 21 | return ( 22 |
23 | 24 | Click a note on the left to view something! 25 | 26 |
27 | ) 28 | } 29 | } 30 | 31 | const { id, title, body, updated_at, created_by } = note 32 | const updatedAt = new Date(updated_at) 33 | 34 | if (isEditing) { 35 | return 36 | } else { 37 | return ( 38 |
39 |
40 |

{title}

41 |
42 | 43 | Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")} 44 | 45 | {login && login.id === created_by ? ( 46 | 47 | Edit 48 | 49 | ) : ( 50 |
51 | )} 52 |
53 |
54 | 55 |
56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/NoteEditor.client.js: -------------------------------------------------------------------------------- 1 | import React, { useState, unstable_useTransition } from 'react' 2 | import { createFromReadableStream } from 'react-server-dom-webpack' 3 | 4 | import NotePreview from './NotePreview' 5 | import { useRefresh } from './Cache.client' 6 | import { useLocation } from './LocationContext.client' 7 | 8 | export default function NoteEditor({ noteId, initialTitle, initialBody }) { 9 | const refresh = useRefresh() 10 | const [title, setTitle] = useState(initialTitle) 11 | const [body, setBody] = useState(initialBody) 12 | const [location, setLocation] = useLocation() 13 | const [startNavigating, isNavigating] = unstable_useTransition() 14 | const [isSaving, saveNote] = useMutation({ 15 | endpoint: noteId !== null ? `/api/notes/${noteId}` : `/api/notes`, 16 | method: noteId !== null ? 'PUT' : 'POST', 17 | }) 18 | const [isDeleting, deleteNote] = useMutation({ 19 | endpoint: `/api/notes/${noteId}`, 20 | method: 'DELETE', 21 | }) 22 | 23 | async function handleSave() { 24 | const payload = { title, body } 25 | const requestedLocation = { 26 | selectedId: noteId, 27 | isEditing: false, 28 | searchText: location.searchText, 29 | } 30 | const response = await saveNote(payload, requestedLocation) 31 | navigate(response) 32 | } 33 | 34 | async function handleDelete() { 35 | const payload = {} 36 | const requestedLocation = { 37 | selectedId: null, 38 | isEditing: false, 39 | searchText: location.searchText, 40 | } 41 | const response = await deleteNote(payload, requestedLocation) 42 | navigate(response) 43 | } 44 | 45 | async function navigate(response) { 46 | const cacheKey = response.headers.get('X-Location') 47 | const nextLocation = JSON.parse(cacheKey) 48 | const seededResponse = createFromReadableStream(response.body) 49 | startNavigating(() => { 50 | refresh(cacheKey, seededResponse) 51 | setLocation(nextLocation) 52 | }) 53 | } 54 | 55 | const isDraft = noteId === null 56 | return ( 57 |
58 |
e.preventDefault()} 62 | > 63 | 66 | { 71 | setTitle(e.target.value) 72 | }} 73 | /> 74 | 77 |