認識完了 Server Components 和 Clients Components 是什麼,以及兩者的使用時機後,今天要跟大家分享兩者使用上有什麼需要注意的地方。
將 Client Components 放到 leaf components
Leaf components 指的是沒有 child components 的 components。官方的第一個建議是,假如一個 component 中有 client-only 的元素,盡可能將這個 component 拆小,讓整體 layout 和靜態內容保持 Server Components。只將要使用 client-only 功能的 components 轉成 Client Components,再 import 進 Layout。
這樣講可能有點抽象,我們來看官方提供的範例:
假如網頁的 Header 包含了一個靜態的 logo 和一個需為 Client Component 的 search bar,相較於將整個 Header 做成 Client Components,官方建議將 logo 和 search bar 拆成兩個 components,讓 Layout 和 logo 保持 Server Components,只將 search bar 宣告為 Client Component,再 import 進 layout 中:
/* app/layout.tsx */
import SearchBar from './searchbar'
import Logo from './logo'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
以傳 Props 的形式在 Client Components 中加入 Server Components
如同上述的例子,在一個 component tree 中可以同時存在 Server Components 和 Client Components。當 React 在渲染 components 時,會先在 React server 渲染 Server Components,假如過程中有遇到 Client Components 就會先不渲染,只標記 Client Components 的位置。
渲染完 Server Components 後,React 會產生一個特殊的檔案格式 React Server Components Payload ( RSC Payload ),夾帶以下資訊:
渲然完成後,會將結果傳給 React Client,接著渲染 Client Components,和產生 initial HTML。
假如不知道 React Server 和 React Client 是什麼,可參考 Day 10 文章
因為 Client Components 是在 Server Components 之後渲染,在渲染 Client Components 時沒辦法回頭渲染 Server Components,所以沒辦法直接 import Server Components 到 Client Components 中。
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
那要如何在 Client Components 中加入 Server Components 呢?我們需要讓要 import 的 Server Components 能在 Client Components 之前渲染,當渲染 Client Components 時再嵌入已經渲染好的 Server Components。
所以我們可以在 Server Components 的位置留一個「空格」( slot ),告訴 Client Components 這邊要留一個地方給其他 components。怎麼留空格呢?我們用像是 React 的 children props ( 也可以使用其他 props ):
/* app/client-component.tsx */
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
這樣當我們在 Parent Component 中 import <ServerComponent>
進 <ClientComponent>
時, 就可以讓<ServerComponents>
在 Client Components 前渲染,等渲染 <ClientComponent>
時再將 <ServerComponent>
嵌入至 children props 的位置:
/* app/page.tsx */
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
props 除了可以用來製造空格,也可以用來傳 Server Components 給 Client Components 的資料。但有一點要注意,props 必須是 serializable 的物件,所以無法傳 functions, Dates 等 none-serializable 的物件。
說到這邊,你可能跟我一樣,想自己嘗試直接 import Server Component 進 Client Component,看看會發生什麼事,所以就寫了一個簡單的 Server Components Server.tsx
import 進 Client.tsx
這個 Client Components 中:
/* Server.tsx */
export default function Server() {
return <div>This is a server component!</div>;
}
/* Client.tsx */
import { useState } from 'react';
import Server from './Server';
export default function Client() {
const [count, setCount] = useState(0);
return (
<>
<Server />
{count}
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
}
這時你可能會發現,沒有任何報錯,counter 按鈕也可以正常運行。奇怪,官方不是說不能將 Server Components 直接傳入 Client Components 中嗎?
這就跟 'use client'
的作用有關了。事實上,'use client'
並不是用來宣告單一 component 是 Client Component,而是宣告一個 Server Component 和 Client Component 的分界線 ( boundary )。
所以,當我們在 Client.tsx
加入 'use client'
時,除了 <Client>
本身以外,import 進 <Client>
的 components 也會被當作 Client Component。
因此<Server>
被 import 進 <Client>
時會被當作一個 Client Component,而 <Server>
中並沒有使用任何 server-only 的功能 ( ex: async components、server-only 套件等等 ),因此做為一個 Client Component 不會有問題,就不會報錯。
假如我們稍微改一下 <Server>
,讓它變成一個 async component:
export default async function Server() {
return <div>This is a server component!</div>;
}
這時候就會跳出無法在 Client Components 中使用 async components 的錯誤提示:
也因為 'use client'
是設定 Server Components 和 Client Components 的界線,我們不需要在每個 Client Components 中標記 'use client’
,只需要在 Client Components 的「進入點」加上 ‘use client’
即可。 來看個例子:
我們建一個簡單的 Client Component <Parent>
,並在其中 import 一個子元件 <Child>
:
/* app/Parent.tsx */
'use client';
import Child from './Child';
export default function Parent() {
return (
<div>
This is a Client Component
<Child />
</div>
);
}
這時你會發現,在 <Child>
中,不需要標記 ‘use client’
就可以使用 Client Components 限定的 useState hook:
/* app/Child.tsx */
import { useState } from 'react';
export default function Child() {
const [count, setCount] = useState(0);
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
}
所以我們只需要在 Client Components 的進入點 <Parent>
標示 ‘use client’
,它的 child component <Child>
也會自動被視為一個 Client Component!
今天就先到這邊,因為之前有朋友問到為什麼他在 Client Components 中直接 import 一個 Server Components 沒有報錯,決定花點時間和大家做相關補充,希望對大家有幫助!
明天會繼續分享 Server Components 和 Client Components 的其他 best practices!
謝謝大家耐心的閱讀,我們明天見!
這篇寫得很清楚!之前在試的時候也發現對於 client and server component,Next.js 的文件也沒有寫得很清楚 😂
我前陣子才領悟到 server component 的關鍵應該是 async,如果一個元件沒有用 async 也沒有 hooks(也沒有存取到伺服器狀態),其實也不必擔心它到底是 server 還 client,反正在哪裡都可渲染
感謝 huli 大的肯定!
我一開始對 client 和 server components 的概念也是一頭霧水,也是瘋狂踩雷踩出一點頭緒
後來發現國外論壇也有許多相關的疑問,Next 團隊也持續在修改官方文件和發影片講解兩者的差異,有幾次寫完文章,隔天起床發現官方文件又改了,要定期去看官方 repo 的最新 commit,檢查自己的認知有沒有過時,也是能感受出 Vercel 的積極度🤣