Skip to content

Commit 5d6c7c8

Browse files
committed
Add simple next.js demo
1 parent f9dc2f6 commit 5d6c7c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+13348
-0
lines changed

nextjs-server-components/.gitignore

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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

nextjs-server-components/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.

nextjs-server-components/README.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# React Server Components in Next.js
2+
3+
Experimental app of React Server Components with Next.js, based on [React Server Components Demo](https://github.com/reactjs/server-components-demo).
4+
**It's not ready for adoption. Use this in your projects at your own risk.**
5+
6+
## Development
7+
8+
### Prepare
9+
10+
You need these environment variables to run this app (you can create a `.env` file):
11+
12+
```
13+
REDIS_URL='rediss://:<password>@<url>:<port>' // or `redis://` if no TLS
14+
ENDPOINT='http://localhost:3000' // need to be absolute url to run in prod/local
15+
NEXT_PUBLIC_ENDPOINT='http://localhost:3000' // same as above
16+
SESSION_KEY='<random key for cookie-based session>'
17+
OAUTH_CLIENT_KEY='github oauth app id'
18+
OAUTH_CLIENT_SECRET='github oauth app secret'
19+
```
20+
21+
### Start
22+
23+
1. `yarn install` (this will trigger the postinstall command)
24+
2. `yarn dev`
25+
26+
Go to `localhost:3000` to view the application.
27+
28+
### Deploy
29+
30+
[![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=REDIS_URL,ENDPOINT,NEXT_PUBLIC_ENDPOINT,SESSION_KEY,OAUTH_CLIENT_KEY,OAUTH_CLIENT_SECRET&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.vercel.app&demo-image=https%3A%2F%2Fnext-server-components.vercel.app%2Fog.png)
31+
32+
## Caveats
33+
34+
- Only `.js` extension is supported.
35+
- Client / server components should be under the `components` directory.
36+
- Some React Hooks are not supported in server components, such as `useContext`.
37+
- You have to manually import `React` in your server components.
38+
39+
## How does it work?
40+
41+
Application APIs:
42+
43+
- `GET, POST /api/notes` (Get all notes, Create a new note)
44+
- `GET, PUT, DELETE /api/notes/<note_id>` (Action for a specific note)
45+
46+
React Server Components API (`pages/api/index.js`):
47+
48+
- `GET /api` (render application and return the serialized components)
49+
50+
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`.
51+
52+
`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:
53+
54+
```js
55+
pipeToNodeWritable(React.createElement(App, props), res, moduleMap)
56+
```
57+
58+
`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`).
59+
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`).
60+
61+
`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.
62+
63+
Finally, everything related to OAuth is inside `pages/api/auth.js`. It's a cookie-based session using GitHub for login.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
<div className="container">
15+
<div className="banner">
16+
<a
17+
href="https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html?utm_source=vercel"
18+
target="_blank"
19+
>
20+
Learn more →
21+
</a>
22+
</div>
23+
<div className="main">
24+
<input type="checkbox" class="sidebar-toggle" id="sidebar-toggle" />
25+
<section className="col sidebar">
26+
<section className="sidebar-header">
27+
<img
28+
className="logo"
29+
src="logo.svg"
30+
width="22px"
31+
height="20px"
32+
alt=""
33+
role="presentation"
34+
/>
35+
<strong>React Notes</strong>
36+
</section>
37+
<section className="sidebar-menu" role="menubar">
38+
<SearchField />
39+
<AuthButton login={login} noteId={null}>
40+
Add
41+
</AuthButton>
42+
</section>
43+
<nav>
44+
<Suspense fallback={<NoteListSkeleton />}>
45+
<NoteList searchText={searchText} />
46+
</Suspense>
47+
</nav>
48+
</section>
49+
<section className="col note-viewer">
50+
<Suspense fallback={<NoteSkeleton isEditing={isEditing} />}>
51+
<Note login={login} selectedId={selectedId} isEditing={isEditing} />
52+
</Suspense>
53+
</section>
54+
</div>
55+
</div>
56+
)
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
<EditButton {...props}>
9+
{children}
10+
<img
11+
src={`https://avatars.githubusercontent.com/${login}?s=40`}
12+
alt="User Avatar"
13+
title={login}
14+
className="avatar"
15+
/>
16+
</EditButton>
17+
)
18+
}
19+
20+
return (
21+
<EditButton login {...props}>
22+
Login to {children}
23+
</EditButton>
24+
)
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { unstable_useTransition } from 'react'
2+
3+
import { useLocation } from './LocationContext.client'
4+
5+
export default function EditButton({
6+
login,
7+
noteId,
8+
disabled,
9+
title,
10+
children,
11+
}) {
12+
const [, setLocation] = useLocation()
13+
const [startTransition, isPending] = unstable_useTransition()
14+
const isDraft = noteId == null
15+
return (
16+
<button
17+
className={[
18+
'edit-button',
19+
isDraft ? 'edit-button--solid' : 'edit-button--outline',
20+
].join(' ')}
21+
disabled={isPending || disabled}
22+
title={title}
23+
onClick={() => {
24+
if (login) {
25+
// login needed
26+
window.location = '/api/auth'
27+
return
28+
}
29+
if (isDraft) {
30+
// hide the sidebar
31+
const sidebarToggle = document.getElementById('sidebar-toggle')
32+
if (sidebarToggle) {
33+
sidebarToggle.checked = true
34+
}
35+
}
36+
startTransition(() => {
37+
setLocation(loc => ({
38+
selectedId: noteId,
39+
isEditing: true,
40+
searchText: loc.searchText,
41+
}))
42+
})
43+
}}
44+
role="menuitem"
45+
>
46+
{children}
47+
</button>
48+
)
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createContext, useContext } from 'react'
2+
3+
export const LocationContext = createContext()
4+
export function useLocation() {
5+
return useContext(LocationContext)
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 <NoteEditor noteId={null} initialTitle="Untitled" initialBody="" />
20+
} else {
21+
return (
22+
<div className="note--empty-state">
23+
<span className="note-text--empty-state">
24+
Click a note on the left to view something! 🥺
25+
</span>
26+
</div>
27+
)
28+
}
29+
}
30+
31+
let { id, title, body, updated_at, created_by: created_by } = note
32+
const updatedAt = new Date(updated_at)
33+
34+
if (isEditing) {
35+
return <NoteEditor noteId={id} initialTitle={title} initialBody={body} />
36+
} else {
37+
return (
38+
<div className="note">
39+
<div className="note-header">
40+
<h1 className="note-title">{title}</h1>
41+
{created_by ? (
42+
<div
43+
style={{
44+
flex: '1 0 100%',
45+
order: '-1',
46+
marginTop: 10,
47+
}}
48+
>
49+
By{' '}
50+
<img
51+
src={`https://avatars.githubusercontent.com/${created_by}?s=40`}
52+
alt="User Avatar"
53+
title={created_by}
54+
className="avatar"
55+
/>
56+
&nbsp;
57+
<a
58+
href={`https://github.com/${created_by}`}
59+
target="_blank"
60+
rel="noopener noreferrer"
61+
>
62+
{created_by}
63+
</a>
64+
</div>
65+
) : null}
66+
<div className="note-menu" role="menubar">
67+
<small className="note-updated-at" role="status">
68+
Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")}
69+
</small>
70+
{login === created_by ? (
71+
<AuthButton login={login} noteId={id}>
72+
Edit
73+
</AuthButton>
74+
) : (
75+
<div style={{ height: 30 }} />
76+
)}
77+
</div>
78+
</div>
79+
<NotePreview body={body} />
80+
</div>
81+
)
82+
}
83+
}

0 commit comments

Comments
 (0)