跳到內容
App Router入門資料獲取

資料獲取

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

資料獲取

伺服器元件

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

  1. fetch API
  2. ORM 或資料庫

使用 fetch API

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

app/blog/page.tsx
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 或資料庫客戶端安全地進行資料庫查詢。將您的元件轉換為非同步函式,並等待呼叫:

app/blog/page.tsx
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 鉤子
  2. 一個社群庫,如 SWRReact Query

使用 use 鉤子流式傳輸資料

您可以使用 React 的 use 鉤子 將資料從伺服器流式傳輸到客戶端。首先在您的伺服器元件中獲取資料,並將 Promise 作為 prop 傳遞給您的客戶端元件:

app/blog/page.tsx
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
 
export default function Page() {
  // Don't await the data fetching function
  const posts = getPosts()
 
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

然後,在您的客戶端元件中,使用 use 鉤子讀取 Promise:

app/ui/posts.tsx
'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 解決時將顯示回退內容。瞭解更多關於流式傳輸的資訊。

社群庫

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

app/blog/page.tsx
'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>
  )
}

請求去重和資料快取

去重 fetch 請求的一種方法是使用請求記憶化。透過這種機制,在單個渲染過程中,使用相同 URL 和選項的 GETHEAD 型別的 fetch 呼叫將合併為一個請求。這會自動發生,您可以透過將 Abort 訊號傳遞給 fetch選擇退出

請求記憶化的作用域僅限於請求的生命週期。

您還可以透過使用 Next.js 的資料快取來去重 fetch 請求,例如在您的 fetch 選項中設定 cache: 'force-cache'

資料快取允許在當前渲染通道和傳入請求之間共享資料。

如果您**不**使用 fetch,而是直接使用 ORM 或資料庫,您可以使用 React cache 函式包裝您的資料訪問。

app/lib/data.ts
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)),
  })
})

流式傳輸

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

當您在伺服器元件中獲取資料時,資料會為每個請求在伺服器上獲取並渲染。如果您的資料請求速度較慢,那麼在所有資料都獲取完畢之前,整個路由都將被阻止渲染。

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

How Server Rendering with Streaming Works

您可以透過兩種方式在應用程式中實現流式傳輸:

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

使用 loading.js

您可以在與頁面相同的資料夾中建立一個 loading.js 檔案,以便在資料獲取時流式傳輸**整個頁面**。例如,要流式傳輸 app/blog/page.js,請將檔案新增到 app/blog 資料夾中。

Blog folder structure with loading.js file
app/blog/loading.tsx
export default function Loading() {
  // Define the Loading UI here
  return <div>Loading...</div>
}

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

Loading UI

在幕後,loading.js 將巢狀在 layout.js 中,並將自動將 page.js 檔案和下面的任何子級包裝在 <Suspense> 邊界中。

loading.js overview

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

使用 <Suspense>

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

app/blog/page.tsx
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
 
export default function BlogPage() {
  return (
    <div>
      {/* This content will be sent to the client immediately */}
      <header>
        <h1>Welcome to the Blog</h1>
        <p>Read the latest posts below.</p>
      </header>
      <main>
        {/* Any content wrapped in a <Suspense> boundary will be streamed */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

建立有意義的載入狀態

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

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

示例

順序資料獲取

順序資料獲取發生在樹中巢狀元件各自獲取資料且請求未去重時,導致響應時間延長。

Sequential and Parallel Data Fetching

在某些情況下,您可能需要這種模式,因為一個 fetch 依賴於另一個的結果。

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

app/artist/[username]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // Get artist information
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* Show fallback UI while the Playlists component is loading */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* Pass the artist ID to the Playlists component */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
 
async function Playlists({ artistID }: { artistID: string }) {
  // Use the artist ID to fetch playlists
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

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

並行資料獲取

並行資料獲取發生在路由中的資料請求被主動發起並同時開始時。

預設情況下,佈局和頁面是並行渲染的。因此,每個片段都會盡快開始獲取資料。

然而,在**任何**元件中,如果多個 async/await 請求相互放置,它們仍然可以是順序的。例如,getAlbums 將被阻塞,直到 getArtist 被解析。

app/artist/[username]/page.tsx
import { getArtist, getAlbums } from '@/app/lib/data'
 
export default async function Page({ params }) {
  // These requests will be sequential
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

透過呼叫 fetch 發起多個請求,然後使用 Promise.all 等待它們。請求在呼叫 fetch 後立即開始。

app/artist/[username]/page.tsx
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
 
  // Initiate requests
  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 方法來代替。

預載入資料

您可以透過建立一個實用函式來預載入資料,該函式在阻塞請求之前主動呼叫。<Item> 根據 checkIsAvailable() 函式有條件地渲染。

您可以在 checkIsAvailable() 之前呼叫 preload(),以主動啟動 <Item/> 資料依賴。當 <Item/> 渲染時,其資料已提前獲取。

app/item/[id]/page.tsx
import { getItem, checkIsAvailable } from '@/lib/data'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}
 
export const preload = (id: string) => {
  // void evaluates the given expression and returns undefined
  // https://mdn.club.tw/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 來建立一個可重用的實用函式。這種方法允許您快取資料獲取函式並確保它僅在伺服器上執行。

utils/get-item.ts
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) => {
  // ...
})