跳至內容

伺服器和客戶端組合模式

在構建 React 應用程式時,您需要考慮應用程式的哪些部分應該在伺服器端或客戶端渲染。本頁面涵蓋了使用伺服器元件和客戶端元件時的一些建議組合模式。

何時使用伺服器元件和客戶端元件?

以下是伺服器元件和客戶端元件不同使用案例的快速摘要:

您需要做什麼?伺服器元件客戶端元件
擷取資料
(直接)存取後端資源
將敏感資訊保留在伺服器上(例如存取權限、API 金鑰等)
將大型依賴項保留在伺服器上/減少客戶端 JavaScript
新增互動性和事件監聽器(例如 onClick()onChange() 等)
使用狀態和生命週期效應(例如 useState()useReducer()useEffect() 等)
使用僅限瀏覽器的 API
使用依賴於狀態、效應或僅限瀏覽器 API 的自訂 hooks
使用 React 類別元件

伺服器元件模式

在選擇客戶端渲染之前,您可能希望在伺服器上執行一些工作,例如擷取資料或存取您的資料庫或後端服務。

以下是使用伺服器元件時的一些常見模式:

在元件之間共享資料

在伺服器上擷取資料時,可能有些情況下您需要在不同的元件之間共享資料。例如,您可能有一個佈局和一個頁面都依賴於相同的資料。

您可以使用 fetch 或 React 的 cache 函數在需要的組件中獲取相同的資料,而不必擔心對相同資料發出重複的請求,這取代了使用 React Context(伺服器端無法使用)或將資料作為 props 傳遞的方式。這是因為 React 擴展了 fetch來自動記憶資料請求,並且在 fetch 不可用時可以使用 cache 函數。

查看此模式的範例

將僅限伺服器端的程式碼排除在客戶端環境之外

由於 JavaScript 模組可以在伺服器端和客戶端組件模組之間共用,因此原本只打算在伺服器端執行的程式碼可能會偷偷進入客戶端。

例如,以下列資料提取函數為例

lib/data.ts
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

乍看之下,getData 似乎可以在伺服器端和客戶端上運作。然而,這個函數包含一個 API_KEY,編寫它的目的是它只會在伺服器端執行。

由於環境變數 API_KEY 沒有以 NEXT_PUBLIC 為前綴,它是一個只能在伺服器端存取的私有變數。為了防止您的環境變數洩漏到客戶端,Next.js 會將私有環境變數替換為空字串。

因此,即使 getData() 可以在客戶端匯入和執行,它也不會如預期般運作。雖然將變數公開可以使函數在客戶端上運作,但您可能不想將敏感資訊暴露給客戶端。

為了防止這種伺服器端程式碼被客戶端意外使用的情況,我們可以使用 server-only 套件,以便在其他開發人員意外將其中一個模組匯入客戶端組件時,在建置時向他們發出錯誤訊息。

要使用 server-only,首先安裝套件

終端機
npm install server-only

然後將套件匯入任何包含僅限伺服器端程式碼的模組

lib/data.js
import 'server-only'
 
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

現在,任何匯入 getData() 的客戶端組件都會收到一個建置時錯誤,說明此模組只能在伺服器端使用。

對應的套件 client-only 可用於標記包含僅限客戶端程式碼的模組,例如存取 window 物件的程式碼。

使用第三方套件和提供器

由於伺服器元件是 React 的新功能,生態系統中的第三方套件和提供器才剛開始將 "use client" 指令添加到使用純客戶端功能(例如 useStateuseEffectcreateContext)的元件中。

目前,許多來自 npm 套件且使用純客戶端功能的元件尚未包含該指令。這些第三方元件在客戶端元件中可以正常運作,因為它們具有 "use client" 指令,但在伺服器元件中則無法運作。

例如,假設您安裝了一個名為 acme-carousel 的套件,其中包含一個 <Carousel /> 元件。此元件使用了 useState,但它還沒有 "use client" 指令。

如果您在客戶端元件中使用 <Carousel />,它將會如預期般運作。

app/gallery.tsx
'use client'
 
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
 
export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)
 
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>
 
      {/* Works, since Carousel is used within a Client Component */}
      {isOpen && <Carousel />}
    </div>
  )
}

但是,如果您嘗試直接在伺服器元件中使用它,您將會看到錯誤訊息。

app/page.tsx
import { Carousel } from 'acme-carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/* Error: `useState` can not be used within Server Components */}
      <Carousel />
    </div>
  )
}

這是因為 Next.js 無法辨識 <Carousel /> 正在使用純客戶端功能。

要解決此問題,您可以將依賴純客戶端功能的第三方元件包裝在您自己的客戶端元件中。

app/carousel.tsx
'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

現在,您可以在伺服器元件中直接使用 <Carousel /> 了。

app/page.tsx
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

我們預期您不需要包裝大多數的第三方元件,因為您很可能會在客戶端元件中使用它們。然而,一個例外是提供器,因為它們依賴 React 狀態和上下文,並且通常需要在應用程式的根層級使用。下方深入瞭解第三方上下文提供器

使用上下文提供器

上下文提供器通常會在應用程式的根層級附近渲染,以共享全域關注事項,例如目前的佈景主題。由於React 上下文在伺服器元件中不受支援,因此嘗試在應用程式的根層級建立上下文將會導致錯誤。

app/layout.tsx
import { createContext } from 'react'
 
//  createContext is not supported in Server Components
export const ThemeContext = createContext({})
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}

要解決此問題,請在客戶端元件內建立您的上下文並渲染其提供器。

app/theme-provider.tsx
'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

您的伺服器元件現在可以直接渲染您的提供器,因為它已被標記為客戶端元件。

app/layout.tsx
import ThemeProvider from './theme-provider'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

將提供者渲染在根組件後,應用程式中的所有其他客戶端組件都能夠使用此上下文。

注意事項:您應該盡可能在樹狀結構的深處渲染提供者 — 注意 ThemeProvider 僅包裹 {children} 而不是整個 <html> 文件。這讓 Next.js 更容易最佳化伺服器組件的靜態部分。

給函式庫作者的建議

同樣地,建立供其他開發者使用的套件的函式庫作者可以使用 "use client" 指令標記其套件的客戶端進入點。這允許套件的使用者將套件組件直接導入其伺服器組件,而無需建立包裝邊界。

您可以透過在樹狀結構的更深處使用「在樹狀結構更深處使用 'use client'」來最佳化您的套件,讓導入的模組成為伺服器組件模組圖的一部分。

值得注意的是,某些打包工具可能會移除 "use client" 指令。您可以在 React Wrap BalancerVercel Analytics 儲存庫中找到如何設定 esbuild 以包含 "use client" 指令的範例。

客戶端組件

將客戶端組件移至樹狀結構的更深處

為了減少客戶端 JavaScript 程式碼的大小,我們建議將客戶端組件移至組件樹狀結構的更深處。

例如,您可能有一個包含靜態元素(例如:標誌、連結等)和使用狀態的互動式搜尋列的佈局。

不要將整個佈局設為客戶端組件,而是將互動式邏輯移至客戶端組件(例如 <SearchBar />),並將佈局保留為伺服器組件。這表示您不必將佈局的所有組件 JavaScript 傳送到客戶端。

app/layout.tsx
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
 
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

從伺服器元件傳遞屬性到客戶端元件(序列化)

如果您在伺服器元件中擷取資料,您可能會想將資料作為屬性傳遞給客戶端元件。從伺服器傳遞到客戶端元件的屬性需要由 React 序列化

如果您的客戶端元件依賴*不可*序列化的資料,您可以使用第三方函式庫在客戶端上擷取資料,或使用路由處理程式在伺服器上擷取資料。

交錯伺服器和客戶端元件

當交錯客戶端和伺服器元件時,將您的 UI 視為元件樹可能會有幫助。從根佈局(一個伺服器元件)開始,您可以透過新增 "use client" 指令在客戶端上渲染某些元件子樹。

在這些客戶端子樹中,您仍然可以嵌套伺服器元件或呼叫伺服器動作,但有一些事項需要注意。

  • 在請求-回應生命週期中,您的程式碼會從伺服器移至客戶端。如果您需要在客戶端上存取伺服器上的資料或資源,您將會向伺服器發出一個新的請求,而不是來回切換。
  • 當向伺服器發出新請求時,所有伺服器元件都會先被渲染,包括嵌套在客戶端元件內的伺服器元件。渲染的結果(RSC Payload)將包含客戶端元件位置的參考。然後,在客戶端上,React 使用 RSC Payload 將伺服器和客戶端元件協調成單一樹狀結構。
  • 由於客戶端元件是在伺服器元件之後渲染的,因此您無法將伺服器元件匯入到客戶端元件模組中(因為這需要向伺服器發出新的請求)。您可以改為將伺服器元件作為 props 傳遞給客戶端元件。請參閱下面的不支援的模式支援的模式章節。

不支援的模式:將伺服器元件匯入到客戶端元件中

以下模式不支援。您無法將伺服器元件匯入到客戶端元件中。

app/client-component.tsx
'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 />
    </>
  )
}

支援的模式:將伺服器元件作為 Props 傳遞給客戶端元件

支援以下模式。您可以將伺服器元件作為 prop 傳遞給客戶端元件。

一個常見的模式是使用 React 的 children prop 在您的客戶端元件中建立一個「插槽」。

在下面的範例中,<ClientComponent> 接受一個 children prop。

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}
    </>
  )
}

<ClientComponent> 並不知道 children 最終會由伺服器元件的結果填入。<ClientComponent> 的唯一職責是決定 children 最終將放置在**哪裡**。

在父伺服器元件中,您可以同時匯入 <ClientComponent><ServerComponent>,並將 <ServerComponent> 作為 <ClientComponent> 的子元件傳遞。

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>
  )
}

使用這種方法,<ClientComponent><ServerComponent> 是分離的,可以獨立渲染。在這種情況下,子元件 <ServerComponent> 可以在伺服器上渲染,遠早於 <ClientComponent> 在客戶端上渲染。

須知

  • 「將內容上提」的模式已被用於避免在父元件重新渲染時重新渲染巢狀的子元件。
  • 您不限於使用 children prop。您可以使用任何 prop 來傳遞 JSX。