iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Modern Web

30 天淺入淺出 Next.js 13系列 第 11

Day11 - Component Composition Pattern

  • 分享至 

  • xImage
  •  

前兩天講了 server component 與 client component,Next官方文章也有列出一些使用 component pattern,今天就來看看有哪些需要注意的地方。

今日大綱

  • 在 server component 之間傳遞資料
  • 避免 server-only 的東西傳入 client component
  • 使用第三方套件及 context provider
  • 盡可能把 client component 推到末端

在 server component 之間傳遞資料

在 client component 當中,我們可以使用 props、context 或是其他第三方的狀態管理來共享資料。但在 server component 只能使用 props 一層一層傳,如果需要傳遞的結構很深,就會有 props drilling 的問題。

我們可以在每一個需要資料的 server component 都使用內建的 fetch 或是使用被 React cache 包裝過的 function 取得資料,這樣就不需要將結果傳遞給其他 component 了,因為每個 component 都有 fetch 資料。

你可能會想問,這樣不就需要打很多次一模一樣的 api?如果有三個 component 都要 user api 資料,不就要 fetch 三次?

這就要歸功於 Next 的 Request Memoization 功能,使用 fetch 或是 cache 包裝過的 function 取得資料,如果傳進來的參數都一樣,那 Next 就會自動把 request 合併,所有 request 都會取得同一個結果。

避免 server-only 的東西傳入 client component

昨天的文章有提到要避免 server component 傳入 client component,事實上不只 component,只要是你希望它運行在 server 上的所有東西,都要避免傳入 client component。

可以看看底下這段 code 有什麼需要注意的地方

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

答案是 authorization: process.env.API_KEY 這段。因為這段環境變數沒有要共享給 client 端使用,如果要開放給 client 端使用,則需要加上 NEXT_PUBLIC 當作前綴。

雖然 fetch 可以在 server 及 client 使用,但會有一些情境是只想在某一端使用的。

server-only package

這時候就可以使用 server-only 這個套件,將這個套件 import 進只想運行在 server 的檔案。它會在 build time 時檢查是否有 server-only 的檔案被 import 進了 client component,如果有個話就會直接噴錯。

npm install server-only
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-only package

當然同樣也會有只想執行在 client 的檔案,像是有操作 dom 或 browser API,這時也可以像上面的 server-only 的做法一樣,只是換成 import 'client-only'

使用第三方套件及 context provider

昨天的文章有提到如何轉換第三方套件,可以使用 use client 重新 export 套件,讓 Next 知道該套件是 client component,避免引入至 server component 時報錯。

使用 Context API

使用 client component 很有可能會使用到 React 的 context api 傳遞資料,最有可能引入 context api 的地方就是 layout.tsx,因為這是一個共享的 UI 佈局,layout.tsx 不會隨著路由切換消失。

當我們在 layout.tsx 使用 createContext 會報錯,因為 layout.tsx 是一個 server component。雖然可以加上 'use client' 讓它正常運行,但我們最好保持 layout.tsx 是 server component,理由在下方會談到的「盡可能把 client component 推到末端」這點有關

  • 無法在 server component 中使用 createContext
import { createContext } from 'react'
 
// createContext 不支援使用在 server component
export const ThemeContext = createContext({})
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}

所以這時可以把 providers 全部抽出來當成一個 client component,再把這個 component 引入到 layout.tsx

  • providers.tsx
// 把所有 provider 都丟來這就對了
'use client'
 
import { createContext } from 'react'
import { ThirdPartyProvider } from 'third-party'
 
export const ThemeContext = createContext({})
 
export default function Providers({ children }) {
  return (
      <ThemeContext.Provider value="dark">
          <ThirdPartyProvider>
              {children}
          <ThirdPartyProvider>
      </ThemeContext.Provider>
  )
}
  • layout.tsx
import Providers from './providers'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

盡可能把 client component 推到末端

昨天的鐵人賽 有講到,client component 因為要傳到 client 端運行,是會有 js bundle 的,所以我們要盡可能地把 client 的邊界往末端去推。

什麼意思呢?我們先來看渲染出來的樹長怎樣

  • 橘色:server component
  • 藍色:client component

以這張圖來看,下方就代表末端,Next 建議我們把 client component 儘可能地往下擺放。

實作上就是盡量把 server component 就可以渲染的 UI 從 client component 搬出去

以上方講到的 layout.tsx 這支範例來看,我們會希望 providers.tsx 盡量減少 server component 就可以渲染的 UI,只包含一定得在 client 才能運行的 code。

  • good
    這個 client component 定義的很好,因為這個 client component 只包含了 client only 的 code,沒有半行程式碼可以再抽到 server component 當中。
import Providers from './providers'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
  • bad
    html 與 body 的部分可以由 server component 渲染,但卻被當成 client component,增加了 js bundle 的大小
import Providers from './providers'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
      <Providers>
        <html>
          <body>
            {children}
          </body>
        </html>
       </Providers>
  )
}

參考資料


上一篇
Day10 - Client Component
下一篇
Day12 - 導航(Linking & Navigating)
系列文
30 天淺入淺出 Next.js 1321
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言