伺服器與客戶端組合模式
在建構 React 應用程式時,您需要考慮應用程式的哪些部分應在伺服器或客戶端渲染。本頁介紹使用伺服器元件與客戶端元件時的一些推薦組合模式。
何時使用伺服器元件與客戶端元件?
以下是伺服器元件與客戶端元件的不同使用情境簡要總結:
您需要做什麼? | 伺服器元件 | 客戶端元件 |
---|---|---|
獲取資料 | ||
直接存取後端資源 | ||
將敏感資訊保留在伺服器上(存取權杖、API 金鑰等) | ||
將大型依賴保留在伺服器上 / 減少客戶端 JavaScript | ||
新增互動性與事件監聽器(onClick() 、onChange() 等) | ||
使用狀態與生命週期效果(useState() 、useReducer() 、useEffect() 等) | ||
使用僅限瀏覽器的 API | ||
使用依賴狀態、效果或僅限瀏覽器 API 的自訂 Hook | ||
使用 React Class 元件 |
伺服器元件模式
在選擇客戶端渲染之前,您可能希望在伺服器上執行一些工作,例如獲取資料或存取資料庫或後端服務。
以下是使用伺服器元件時的一些常見模式:
在元件之間共享資料
在伺服器上獲取資料時,可能會遇到需要在不同元件之間共享資料的情況。例如,您可能有一個佈局和一個頁面依賴相同的資料。
與其使用 React Context(在伺服器上不可用)或將資料作為 props 傳遞,您可以使用 fetch
或 React 的 cache
函數在需要資料的元件中獲取相同資料,而無需擔心對相同資料發送重複請求。這是因為 React 擴展了 fetch
來自動記憶資料請求,而當 fetch
不可用時可以使用 cache
函數。
了解更多關於 React 中的 記憶化 (memoization)。
防止伺服器專用程式碼進入客戶端環境
由於 JavaScript 模組可以在伺服器元件與客戶端元件之間共享,原本僅打算在伺服器上執行的程式碼可能會意外進入客戶端。
例如,考慮以下資料獲取函數:
乍看之下,getData
似乎在伺服器和客戶端都能工作。然而,此函數包含一個 API_KEY
,其設計初衷是僅在伺服器上執行。
由於環境變數 API_KEY
沒有 NEXT_PUBLIC
前綴,它是一個私有變數,只能在伺服器上存取。為了防止您的環境變數洩漏到客戶端,Next.js 會將私有環境變數替換為空字串。
因此,儘管 getData()
可以在客戶端導入並執行,但它不會如預期般工作。雖然將變數設為公開可以使函數在客戶端工作,但您可能不希望將敏感資訊暴露給客戶端。
為了防止這種伺服器程式碼意外在客戶端使用的情況,我們可以使用 server-only
套件,在開發者意外將這些模組導入客戶端元件時提供建置時錯誤。
要使用 server-only
,首先安裝套件:
然後將套件導入任何包含伺服器專用程式碼的模組:
現在,任何導入 getData()
的客戶端元件都會收到一個建置時錯誤,說明此模組只能在伺服器上使用。
對應的套件 client-only
可用於標記包含僅限客戶端程式碼的模組——例如,存取 window
物件的程式碼。
使用第三方套件與提供者
由於伺服器元件是 React 的新功能,生態系統中的第三方套件與提供者才剛開始為使用客戶端專用功能(如 useState
、useEffect
和 createContext
)的元件添加 "use client"
指令。
目前,許多來自 npm
套件且使用客戶端專用功能的元件尚未添加此指令。這些第三方元件在客戶端元件中可以如預期般工作,因為它們有 "use client"
指令,但在伺服器元件中無法工作。
例如,假設您安裝了一個假想的 acme-carousel
套件,其中包含一個 <Carousel />
元件。此元件使用 useState
,但尚未添加 "use client"
指令。
如果您在客戶端元件中使用 <Carousel />
,它會如預期般工作:
然而,如果您嘗試直接在伺服器元件中使用它,將會看到錯誤:
這是因為 Next.js 不知道 <Carousel />
正在使用客戶端專用功能。
要解決此問題,您可以將依賴客戶端專用功能的第三方元件包裝在您自己的客戶端元件中:
現在,您可以直接在伺服器元件中使用 <Carousel />
:
我們不預期您需要包裝大多數第三方元件,因為您可能會在客戶端元件中使用它們。然而,一個例外是提供者 (providers),因為它們依賴 React 狀態和上下文,並且通常需要在應用程式的根目錄中使用。在下方了解更多關於第三方上下文提供者的資訊。
使用上下文提供者
上下文提供者通常渲染在應用程式的根目錄附近,以共享全域關注點,例如當前主題。由於 React 上下文 在伺服器元件中不受支援,嘗試在應用程式的根目錄建立上下文會導致錯誤:
要解決此問題,請在客戶端元件中建立您的上下文並渲染其提供者:
您的伺服器元件現在可以直接渲染您的提供者,因為它已被標記為客戶端元件:
在根目錄渲染提供者後,整個應用程式中的所有其他客戶端元件都能使用此上下文。
須知:您應盡可能在樹狀結構的深層渲染提供者——請注意
ThemeProvider
僅包裹{children}
而不是整個<html>
文件。這使得 Next.js 更容易優化伺服器元件的靜態部分。
給函式庫作者的建議
同樣地,建立供其他開發者使用的套件的函式庫作者可以使用 "use client"
指令來標記其套件的客戶端入口點。這使得套件使用者可以直接將套件元件導入其伺服器元件,而無需建立包裝邊界。
您可以通過在樹狀結構的更深層使用 'use client' 來優化您的套件,允許導入的模組成為伺服器元件模組圖的一部分。
值得注意的是,某些打包工具可能會移除 "use client"
指令。您可以在 React Wrap Balancer 和 Vercel Analytics 儲存庫中找到如何配置 esbuild 以包含 "use client"
指令的範例。
客戶端元件
將客戶端元件移至樹狀結構的深層
為了減少客戶端 JavaScript 套件大小,我們建議將客戶端元件移至元件樹的深層。
例如,您可能有一個包含靜態元素(例如標誌、連結等)和一個使用狀態的互動式搜尋列的佈局。
與其將整個佈局設為客戶端元件,不如將互動邏輯移至客戶端元件(例如 <SearchBar />
),並將佈局保留為伺服器元件。這意味著您無需將佈局的所有元件 JavaScript 發送到客戶端。
從伺服器元件傳遞 props 到客戶端元件(序列化)
如果您在伺服器元件中獲取資料,您可能希望將資料作為 props 傳遞給客戶端元件。從伺服器傳遞給客戶端元件的 props 需要能被 React 序列化 (serializable)。
如果您的客戶端元件依賴不可序列化的資料,您可以使用 第三方函式庫在客戶端獲取資料 或通過 路由處理器 (Route Handler) 在伺服器上獲取。
交錯使用伺服器與客戶端元件
當交錯使用客戶端元件 (Client Components) 和伺服器元件 (Server Components) 時,將你的使用者介面視為元件樹會有所幫助。從 根佈局(這是一個伺服器元件)開始,你可以透過添加 "use client"
指令來在客戶端渲染特定的元件子樹。
在這些客戶端子樹中,你仍然可以嵌套伺服器元件或呼叫伺服器動作 (Server Actions),但需要注意以下幾點:
- 在請求-回應的生命週期中,你的程式碼會從伺服器移動到客戶端。如果你需要在客戶端時存取伺服器上的資料或資源,你將會向伺服器發起一個新的請求,而不是來回切換。
- 當向伺服器發起新請求時,所有伺服器元件會先被渲染,包括那些嵌套在客戶端元件內的元件。渲染結果(RSC 負載)將包含對客戶端元件位置的引用。然後,在客戶端上,React 會使用 RSC 負載將伺服器元件和客戶端元件協調成一棵單一的樹。
- 由於客戶端元件是在伺服器元件之後渲染的,因此你不能將伺服器元件導入到客戶端元件模組中(因為這需要向伺服器發起新的請求)。相反地,你可以將伺服器元件作為
props
傳遞給客戶端元件。請參閱下方的不支援的模式和支援的模式章節。
不支援的模式:將伺服器元件導入客戶端元件
以下模式不被支援。你不能將伺服器元件導入到客戶端元件中:
支援的模式:將伺服器元件作為 Props 傳遞給客戶端元件
以下模式是被支援的。你可以將伺服器元件作為 prop 傳遞給客戶端元件。
一個常見的模式是使用 React 的 children
prop 在你的客戶端元件中建立一個「插槽」(slot)。
在下面的範例中,<ClientComponent>
接受一個 children
prop:
<ClientComponent>
並不知道 children
最終將由伺服器元件的結果填充。<ClientComponent>
的唯一責任是決定 children
最終將被放置在何處。
在父級伺服器元件中,你可以同時導入 <ClientComponent>
和 <ServerComponent>
,並將 <ServerComponent>
作為 <ClientComponent>
的子元件傳遞:
透過這種方式,<ClientComponent>
和 <ServerComponent>
被解耦,可以獨立渲染。在這種情況下,子元件 <ServerComponent>
可以在伺服器上渲染,遠早於 <ClientComponent>
在客戶端上渲染。
須知:
- 「提升內容」(lifting content up) 的模式已被用來避免在父元件重新渲染時重新渲染嵌套的子元件。
- 你不限於使用
children
prop。你可以使用任何 prop 來傳遞 JSX。