如何獲取資料並進行串流

本頁將引導您了解如何在伺服器和客戶端元件中獲取資料,以及如何串流依賴於資料的元件。

獲取資料

伺服器元件

您可以在伺服器元件中使用以下方式獲取資料:

  1. fetch API
  2. ORM 或資料庫

使用 fetch API

要使用 fetch API 獲取資料,請將您的元件轉換為非同步函數,並等待 fetch 呼叫。例如:

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

須知:

  • fetch 回應預設不會被快取。然而,Next.js 會預渲染路由,並且輸出會被快取以提升效能。如果您想選擇動態渲染,請使用 { cache: 'no-store' } 選項。請參閱 fetch API 參考
  • 在開發過程中,您可以記錄 fetch 呼叫以便更好地進行可見性和除錯。請參閱 logging API 參考

使用 ORM 或資料庫

由於伺服器元件是在伺服器上渲染的,您可以安全地使用 ORM 或資料庫客戶端進行資料庫查詢。將您的元件轉換為非同步函數,並等待呼叫:

import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

客戶端元件

在客戶端元件中有兩種方式可以獲取資料:

  1. 使用 React 的 use hook
  2. 使用社群函式庫如 SWRReact Query

使用 use hook 串流資料

您可以使用 React 的 use hook 從伺服器串流資料到客戶端。首先在伺服器元件中獲取資料,然後將 promise 作為 prop 傳遞給您的客戶端元件:

import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // 不要等待資料獲取函數
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

然後,在您的客戶端元件中,使用 use hook 來讀取 promise:

'use client'
import { use } from 'react'

export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

在上面的範例中,<Posts> 元件被包裹在 <Suspense> 邊界中。這意味著在 promise 被解析時會顯示 fallback。了解更多關於串流的資訊。

社群函式庫

您可以使用社群函式庫如 SWRReact Query 在客戶端元件中獲取資料。這些函式庫有自己關於快取、串流和其他功能的語意。例如,使用 SWR:

'use client'
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((r) => r.json())

export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

使用 React.cache 去重複請求

去重複是防止在渲染過程中對同一資源進行重複請求的過程。它允許您在不同的元件中獲取相同的資料,同時防止對資料來源進行多次網路請求。

如果您使用 fetch,可以通過添加 cache: 'force-cache' 來去重複請求。這意味著您可以安全地使用相同的 URL 和選項呼叫,並且只會發出一個請求。

如果您不使用 fetch,而是直接使用 ORM 或資料庫,則可以使用 React cache 函數來包裹您的資料獲取。

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'

export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})

串流

警告: 以下內容假設您的應用程式中啟用了 dynamicIO 配置選項。該標誌在 Next.js 15 canary 中引入。

在伺服器元件中使用 async/await 時,Next.js 會選擇動態渲染。這意味著資料將在伺服器上為每個使用者請求進行獲取和渲染。如果有任何慢速資料請求,整個路由將被阻止渲染。

為了改善初始載入時間和使用者體驗,您可以使用串流將頁面的 HTML 分成較小的塊,並逐步將這些塊從伺服器發送到客戶端。

伺服器渲染與串流的工作原理

有兩種方式可以在您的應用程式中實現串流:

  1. 使用 loading.js 檔案包裹頁面
  2. 使用 <Suspense> 包裹元件

使用 loading.js

您可以在與您的頁面相同的資料夾中創建一個 loading.js 檔案,以在資料獲取時串流整個頁面。例如,要串流 app/blog/page.js,請在 app/blog 資料夾中添加該檔案。

包含 loading.js 檔案的部落格資料夾結構
export default function Loading() {
  // 在此定義載入 UI
  return <div>Loading...</div>
}

在導航時,使用者將立即看到佈局和載入狀態,同時頁面正在渲染。一旦渲染完成,新內容將自動替換。

載入 UI

在後台,loading.js 將被嵌套在 layout.js 中,並自動將 page.js 檔案及其下方的任何子元件包裹在 <Suspense> 邊界中。

loading.js 概述

這種方法適用於路由段(佈局和頁面),但對於更細粒度的串流,您可以使用 <Suspense>

使用 <Suspense>

<Suspense> 允許您更細粒度地控制頁面的哪些部分要串流。例如,您可以立即顯示 <Suspense> 邊界之外的任何頁面內容,並在邊界內串流部落格文章列表。

import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'

export default function BlogPage() {
  return (
    <div>
      {/* 此內容將立即發送到客戶端 */}
      <header>
        <h1>歡迎來到部落格</h1>
        <p>閱讀以下最新文章。</p>
      </header>
      <main>
        {/* 任何包裹在 <Suspense> 邊界中的內容將被串流 */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

創建有意義的載入狀態

即時載入狀態是在導航後立即向使用者顯示的 fallback UI。為了獲得最佳使用者體驗,我們建議設計有意義的載入狀態,幫助使用者理解應用程式正在響應。例如,您可以使用骨架和旋轉器,或未來螢幕的一小部分但有意義的部分,如封面照片、標題等。

在開發過程中,您可以使用 React Devtools 預覽和檢查元件的載入狀態。

範例

順序資料獲取

順序資料獲取發生在樹中的嵌套元件各自獲取其自己的資料且請求未被去重複時,導致更長的回應時間。

順序和平行資料獲取

有時您可能希望使用這種模式,因為一個獲取依賴於另一個的結果。

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

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // 獲取藝術家資訊
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      {/* 在 Playlists 元件載入時顯示 fallback UI */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* 將藝術家 ID 傳遞給 Playlists 元件 */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

async function Playlists({ artistID }: { artistID: string }) {
  // 使用藝術家 ID 獲取播放列表
  const playlists = await getArtistPlaylists(artistID)

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

為了改善使用者體驗,您應該使用 React <Suspense> 來顯示 fallback 當資料正在獲取時。這將啟用串流並防止整個路由被順序資料請求阻塞。

平行資料獲取 (Parallel data fetching)

當路由中的資料請求被積極初始化並同時開始時,就會發生平行資料獲取。

預設情況下,版面配置 (layouts) 和頁面 (pages) 是平行渲染的。因此每個區段 (segment) 都會盡快開始獲取資料。

然而,在_任何_元件中,多個 async/await 請求如果彼此前後放置,仍然可能是順序執行的。例如,getAlbums 將會被阻塞,直到 getArtist 解析完成:

import { getArtist, getAlbums } from '@/app/lib/data'

export default async function Page({ params }) {
  // 這些請求將會是順序執行
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

您可以透過將請求定義在使用資料的元件之外,並一起解析它們來平行初始化請求,例如使用 Promise.all

import Albums from './albums'

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

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

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  // 平行初始化兩個請求
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

小知識: 當使用 Promise.all 時,如果一個請求失敗,整個操作都會失敗。要處理這種情況,可以使用 Promise.allSettled 方法替代。

預載資料 (Preloading data)

您可以透過建立一個工具函數來預載資料,並在阻塞請求之前積極呼叫它。<Item> 會根據 checkIsAvailable() 函數的結果條件式渲染。

您可以在 checkIsAvailable() 之前呼叫 preload() 來積極初始化 <Item/> 的資料依賴。當 <Item/> 渲染時,其資料已經被獲取完成。

import { getItem } from '@/lib/data'

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // 開始載入項目資料
  preload(id)
  // 執行另一個非同步任務
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

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

此外,您可以使用 React 的 cache 函數server-only 套件 來建立可重複使用的工具函數。這種方法可以快取資料獲取函數,並確保它只在伺服器端執行。

import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

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

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