模式與最佳實踐

在 React 和 Next.js 中,有幾種推薦的資料獲取模式和最佳實踐。本頁將介紹一些最常見的模式及其使用方法。

在伺服器端獲取資料

只要可能,我們建議使用伺服器元件 (Server Components) 在伺服器端獲取資料。這讓您可以:

  • 直接存取後端資料資源(例如資料庫)。
  • 防止敏感資訊(如存取令牌和 API 金鑰)暴露給客戶端,從而提高應用程式的安全性。
  • 在同一個環境中獲取資料並渲染,減少客戶端與伺服器之間的來回通訊,同時也減少客戶端主執行緒的工作量
  • 通過單次往返執行多次資料獲取,而不是在客戶端發送多個獨立請求。
  • 減少客戶端-伺服器的瀑布式請求
  • 根據您所在的區域,資料獲取可以更靠近資料源,從而減少延遲並提高效能。

之後,您可以使用伺服器操作 (Server Actions) 來變更或更新資料。

在需要的地方獲取資料

如果您需要在樹狀結構中的多個元件中使用相同的資料(例如當前使用者),您不必全域獲取資料,也不需要在元件之間傳遞 props。相反,您可以在需要資料的元件中使用 fetch 或 React 的 cache,而無需擔心多次請求相同資料對效能的影響。

這是因為 fetch 請求會自動被記憶化。了解更多關於請求記憶化的資訊。

小提示:這也適用於佈局 (layouts),因為無法在父佈局和其子元件之間傳遞資料。

串流 (Streaming)

串流和 Suspense 是 React 的功能,允許您逐步渲染並增量串流 UI 單元到客戶端。

透過伺服器元件和嵌套佈局,您可以立即渲染不需要特定資料的頁面部分,並為正在獲取資料的部分顯示載入狀態。這意味著使用者無需等待整個頁面載入完畢即可開始與其互動。

Server Rendering with Streaming

要了解更多關於串流和 Suspense 的資訊,請參閱載入 UI串流與 Suspense 頁面。

並行與順序資料獲取

在 React 元件中獲取資料時,需要注意兩種資料獲取模式:並行 (Parallel) 和順序 (Sequential)。

Sequential and Parallel Data Fetching
  • 順序資料獲取:路由中的請求相互依賴,因此會產生瀑布式請求。在某些情況下,您可能需要這種模式,因為一個請求依賴於另一個請求的結果,或者您希望在滿足某個條件後再進行下一個請求以節省資源。然而,這種行為也可能是無意的,導致載入時間變長。
  • 並行資料獲取:路由中的請求會立即啟動,並同時載入資料。這減少了客戶端-伺服器的瀑布式請求以及載入資料的總時間。

順序資料獲取

如果您有嵌套元件,且每個元件都獲取自己的資料,那麼當這些資料請求不同時(這不適用於相同資料的請求,因為它們會自動記憶化),資料獲取將按順序進行。

例如,Playlists 元件只有在 Artist 元件完成資料獲取後才會開始獲取資料,因為 Playlists 依賴於 artistID prop:

// ...

async function Playlists({ artistID }: { artistID: string }) {
  // 等待播放列表
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // 等待藝人資料
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

在這種情況下,您可以使用 loading.js(用於路由段)或 React <Suspense>(用於嵌套元件)來顯示即時載入狀態,同時 React 會串流傳入結果。

這將防止整個路由被資料獲取阻塞,使用者可以與頁面中未被阻塞的部分互動。

阻塞式資料請求:

防止瀑布式請求的另一種方法是在應用程式的根目錄全域獲取資料,但這會阻塞其下方所有路由段的渲染,直到資料載入完成。這可以描述為「全有或全無」的資料獲取方式。要麼您擁有整個頁面或應用程式的資料,要麼什麼都沒有。

任何使用 await 的請求都會阻塞其下方整個樹狀結構的渲染和資料獲取,除非它們被包裹在 <Suspense> 邊界中或使用了 loading.js。另一種選擇是使用並行資料獲取預載入模式

並行資料獲取

要並行獲取資料,您可以在使用資料的元件外部定義請求,然後在元件內部呼叫它們。這樣可以通過同時啟動兩個請求來節省時間,但使用者只有在兩個 Promise 都解析後才能看到渲染結果。

在下面的範例中,getArtistgetArtistAlbums 函數在 Page 元件外部定義,然後在元件內部呼叫,我們等待兩個 Promise 解析:

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // 同時啟動兩個請求
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // 等待 Promise 解析
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

為了提升使用者體驗,您可以添加 Suspense 邊界來分割渲染工作,並盡快顯示部分結果。

預載入資料

防止瀑布式請求的另一種方法是使用預載入模式。您可以選擇性地建立一個 preload 函數來進一步優化並行資料獲取。透過這種方法,您無需將 Promise 作為 props 傳遞。preload 函數也可以有任何名稱,因為它是一種模式,而不是 API。

import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
  // void 會評估給定的表達式並返回 undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}

使用 React cacheserver-only 和預載入模式

您可以結合 cache 函數、預載入模式和 server-only 套件來建立一個可以在整個應用程式中使用的資料獲取工具。

import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})

透過這種方法,您可以提前獲取資料、快取回應,並確保這些資料獲取僅在伺服器端進行

utils/get-item 的匯出可以被佈局、頁面或其他元件使用,從而讓它們控制何時獲取項目的資料。

小提示:

  • 我們建議使用 server-only 套件 來確保伺服器端資料獲取函數永遠不會在客戶端使用。

防止敏感資料暴露給客戶端

我們建議使用 React 的污染 API taintObjectReferencetaintUniqueValue,以防止整個物件實例或敏感值被傳遞給客戶端。

要在您的應用程式中啟用污染功能,請將 Next.js 配置中的 experimental.taint 選項設為 true

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

然後將您想要污染的物件或值傳遞給 experimental_taintObjectReferenceexperimental_taintUniqueValue 函數:

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    '不要將整個使用者物件傳遞給客戶端',
    data
  )
  experimental_taintUniqueValue(
    "不要將使用者的地址傳遞給客戶端",
    data,
    data.address
  )
  return data
}

了解更多關於安全性與伺服器操作的資訊。

On this page