iT邦幫忙

2023 iThome 鐵人賽

DAY 12
1
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 12

Day 12 - Next.js 13 Server Components 和 Client Components 組合使用該注意什麼?

  • 分享至 

  • xImage
  •  

認識完了 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 ),夾帶以下資訊:

  1. Server Components 的渲染結果
  2. 一個說明 Client Components 的位置和它們對應到的 JS 檔案的 placeholder
  3. 要從 Server Components 傳到 Client Components 的 props
    server components and client components tree
    ( 圖片來源:https://www.plasmic.app/blog/how-react-server-components-work )

渲然完成後,會將結果傳給 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 的錯誤提示:
https://ithelp.ithome.com.tw/upload/images/20230912/20161853QniFTjUKcX.png

也因為 '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!

謝謝大家耐心的閱讀,我們明天見!


上一篇
Day 11 - Next.js 13 App Router :什麼時候適合使用 Server Components 或 Client Components?
下一篇
Day 13 - 怎麼限定模組使用環境? Server Components 使用第三方套件要注意什麼?
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
huli
iT邦新手 2 級 ‧ 2023-09-30 08:19:13

這篇寫得很清楚!之前在試的時候也發現對於 client and server component,Next.js 的文件也沒有寫得很清楚 😂

我前陣子才領悟到 server component 的關鍵應該是 async,如果一個元件沒有用 async 也沒有 hooks(也沒有存取到伺服器狀態),其實也不必擔心它到底是 server 還 client,反正在哪裡都可渲染

S.C iT邦新手 4 級 ‧ 2023-09-30 09:47:49 檢舉

感謝 huli 大的肯定!

我一開始對 client 和 server components 的概念也是一頭霧水,也是瘋狂踩雷踩出一點頭緒

後來發現國外論壇也有許多相關的疑問,Next 團隊也持續在修改官方文件和發影片講解兩者的差異,有幾次寫完文章,隔天起床發現官方文件又改了,要定期去看官方 repo 的最新 commit,檢查自己的認知有沒有過時,也是能感受出 Vercel 的積極度🤣

我要留言

立即登入留言