昨天分享了兩個,假如 components 中同時要有 Client Components 和 Server Components ,如何組合兩者官方提供的 best practices,今天要跟大家分享另外兩個 best practices,分別是 Server-Only 的程式碼留在 server-side 就好,和在 Server Comoponents 使用第三方套件的建議。
Server-Only 的程式碼留在 server-side 就好
有些 functions 假如只會在 Server Components 中使用,我們可以盡可能防止它在 client-side 也可以被呼叫,防止機密資料外洩。
舉例來說,有個負責處理 API 的 getData()
function,我們需要在 request headers 中帶入環境變數 API_KEY。這時我們在環境變數的命名上,可以避免使用 NEXT_PUBLIC 開頭,讓這個環境變數只能在 server-side 使用。假如 API_KEY 不是 NEXT_PUBLIC 開頭,它在 client-side 會變成一個空字串,所以 getData()
在 client-side 就無法正常運行。
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
除了環境變數命名的設計以外,也可以透過 server-only 這個 npm package 來限定某個模組只能在 Server Components 使用。方法很簡單,只需透過 npm 安裝:
npm install server-only
接著 import 進要使用它的模組中就完成了!
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()
}
這時假如你不小心在 Client Components 中使用了 server-only 的模組,就會跳出錯誤提示:
同理,你也可以安裝 client-only 這個 package 來防止不小心在 Server Components 中使用到 client-only 的模組。
在 Server Components 使用第三方套件的建議
因為目前 App Router 還是相對較新的架構,而且大部分 React 生態系中的套件,尤其是 React component libraries,都會使用到 useState, useEffect, context 等等 client-only features,多數團隊也還沒在使用到這些 features 的 components 中標示 ‘use client’ 來切分 Client Components 與 Server Components 的 boundries,因此假如直接 import 進 Server Component 中可能會發生錯誤。
我們來看官方範例:
假如我們直接從 UI library import 一個用了 useState 的 component <Carousel>
,因為 <Carousel>
或是它的父層都沒有標記 'use client'
,來告訴 React 這邊是 Client Components 的進入點,React 會嘗試在 React Server 渲染它,這時就會出現 useState 不能在 Server Components 使用的錯誤。
/* 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>
)
}
那假如現在想在 Server Components 中使用有 client-only features 的第三方套件,該怎麼辦呢?如同上述提到,問題出在套件沒有劃出 Client Components 跟 Server Components 的界線,那我們就自己來劃。
我們只需要建一個 Client Component,並標記 'use client' 來當作 Client Component 的進入點,再將 <Carousel>
import 進來:
/* app/Carousel.tsx */
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
接著在 Server Components 中 import 我們創建的 Carousel component 就可以正常使用該套件了!
但這時你可能會問,假如我們要使用 component libraries,很高機率都是要 import 一些有互動性的 components,不太會在 Server Components 使用吧?的確,多數情況 component libraries 會在 Client Components 中使用,所以不會碰到 boudaries 的問題,但假如要使用 context provider 來調整一些設定時,因為需要吃到 context 的 components 都要包在 provider 中,就有可能碰到上述提到的問題。
比方說我們想在 root layout 中建一個 ThemeContext 來讓所有 components 都能吃到這個 context,但 root layout 必須是 Server Component,就會出現不能在 Server Components 使用 client-only 的 createContext hook 的錯誤:
/* 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>
)
}
這時就會需要透過前面提到的方法,自己建立一個 Client Component 再 import 進 root layout 中:
/* app/theme-provider.tsx */
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
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>
)
}
還記得昨天有提到,'use client'
不是宣告單一 component 為 Client Component,而是宣告 Server Components 和 Client Components 的分界嗎?所以一旦劃下這個分界,分界下的 components 會自動宣告為 Client Components。所以依照盡量讓 Client Components 是 leaf components 的官方建議,假如要使用 context provider,也要盡量讓 provider 接近 Client Components 的真實進入點。
以上就是幾個關於 Server Components 和 Client Components 使用官方提供的 best practices,目前官方文件也持續在編修中,假如我有看到新的內容會再與大家更新!
謝謝大家耐心的閱讀,我們明天見!