Kyle Mo
@React 小聚
Basic Introduction Of React Server Components
How Next.js v13 Integrate Server Components ?
Recap & Conclusions
App Directory, Server & Client Components, Data Fetching, Streaming & Suspense, Limits, Future Roadmap...
Server Components (.server.js(/ts/jsx/tsx))
Client Components (.client.js(/ts/jsx/tsx))
Share Components (.js(/ts/jsx/tsx))
Server Component
Client Component
function Root() {
const [data, setData] = useState({});
// 向伺服器發送請求
const componentResponse = useServerResponse(data);
return (
<DataContext.Provider value={[data, setData]}>
componentResponse.render();
</DataContext.Provider>
);
}
Server Component Root
Server
Component 1
Client
Component 1
Server
Component 2
Client
Component 2
M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
M4:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":["$","ul",null,{"className":"notes-list","children":[["$","li","0",{"children":["$","@4",null,{"id":0,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","1",{"children":["$","@4",null,{"id":1,"title":"Make a thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a thing"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","2",{"children":["$","@4",null,{"id":2,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","3",{"children":["$","@4",null,{"id":3,"title":"I wrote this note today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note today"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}]]}]}]}]]}],["$","section","null",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click a note on the left to view something! ð¥º"}]}]}]}]]}]
module.exports = {
tag: 'Server Root',
props: {...},
children: [
{ tag: "Client Component1", props: {...}: children: [] },
{ tag: "Server Component1", props: {...}: children: [
{ tag: "Server Component2", props: {...}: children: [] },
{ tag: "Server Component3", props: {...}: children: [] },
]}
]
}
React Runtime
M1
Client
J0
Server
Render
Request JS Bundle
Response Text (Streaming)
Browser
Client Root
Server
App
Server
Component 1
Client
Component 1
React Runtime
Render
Response Text
(Streaming)
Reduce Bundle Size
Server Side Capabilities
Automatic Code Splitting
import ClientComponent1 from './ClientComponent1';
function ServerComponent() {
return (
<div>
// Client Component 會自動被 Code Splitting
<ClientComponent1 />
</div>
)
}
SSR Navigation
RSC Navigation
Layouts
Server Components
Streaming
Support for Data Fetching
Easily share UI between routes while preserving state and avoiding expensive re-renders.
Making server-first the default for the most dynamic applications.
Display instant loading states and stream in units of UI as they are rendered.
async Server Components and extended fetch API enables component-level fetching.
Server Component
Client Component
The fetch() API
Why Fetch Data In Server Components
Component-Level Data Fetching & Caching
Parallel & Sequential Data Fetching
Static & Dynamic Data Fetching
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
Automatic Request Deduplication
Provide a richer options object so that each request can separately set caching and revalidating rules
對於 backend data resources 有直接的存取權,例如 database 或是 file system
在 Server Side 可以避免一些敏滿資訊洩漏到 Client Side,例如 access token, API key...
讓資料的抓取與渲染在同一個環境中進行,以減少 client side 與 server side 間 back-and-forth 的溝通,可以盡量降低 client side 的工作量
減少 client-server 間的 waterfall 請求
import { use } from 'react';
export function Data({ url }) {
const data = use(fetch(url).thrn(res => res.json()))
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()
// Pass data to the page via props
return { props: { data } }
}
export default Page
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
// child component...
export default async function Child() {
const data = await getData();
return <p>child</p>
}
export default async function Page({ params: { username } }) {
// Initiate both requests in parallel
const artistData = getArtist(username);
const albumData = getArtistAlbums(username);
// Wait for the artist's promise to resolve first
const artist = await artistData;
return (
<>
<h1>{artist.name}</h1>
{/* Send the artist information first,
and wrap albums in a suspense boundary */}
<Suspense fallback={<div>Loading...</div>}>
<Albums promise={albumData} />
</Suspense>
</>
);
}
// Albums Component
async function Albums({ promise }) {
// Wait for the albums promise to resolve
const albums = await promise;
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.name}</li>
))}
</ul>
);
}
減少 Server 與 DB 的負擔
降低頁面的 Loading Time
Per Page
Per Request
Without Streaming & Suspense
CSS-In-JS (ref)
Third Party Libraries
Mutation (ref)
Hard to know is Server or Client Components
import Todo from './todo';
interface Todo {
id: number;
title: string;
completed: boolean;
}
async function getTodos() {
const res = await fetch('https://api.example.com/todos');
const todos: Todo[] = await res.json();
return todos;
}
export default async function Page() {
const todos = await getTodos();
return (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} {...todo} />
))}
</ul>
);
}
"use client";
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';
interface Todo {
id: number;
title: string;
completed: boolean;
}
export default function Todo(todo: Todo) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [isFetching, setIsFetching] = useState(false);
// Create inline loading UI
const isMutating = isFetching || isPending;
async function handleChange() {
setIsFetching(true);
// Mutate external data source
await fetch(`https://api.example.com/todo/${todo.id}`, {
method: 'PUT',
body: JSON.stringify({ completed: !todo.completed }),
});
setIsFetching(false);
startTransition(() => {
// Refresh the current route and fetch new data from the server without
// losing client-side browser or React state.
router.refresh();
});
}
return (
<li style={{ opacity: !isMutating ? 1 : 0.7 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={handleChange}
disabled={isPending}
/>
{todo.title}
</li>
);
}