如何獲取資料並進行串流
本頁將引導您了解如何在 伺服器與客戶端元件 中獲取資料,以及如何串流依賴資料的元件。
獲取資料
伺服器元件
您可以在伺服器元件中使用以下方式獲取資料:
使用 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>
)
}
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>
)
}
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>
)
}
客戶端元件
在客戶端元件中有兩種獲取資料的方式:
- 使用 React 的
use
鉤子 - 使用社群函式庫如 SWR 或 React Query
使用 use
鉤子串流資料
您可以使用 React 的 use
鉤子 從伺服器 串流 資料到客戶端。首先在伺服器元件中獲取資料,然後將 promise 作為 prop 傳遞給客戶端元件:
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// 不要等待資料獲取函式
const posts = getPosts()
return (
<Suspense fallback={<div>載入中...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// 不要等待資料獲取函式
const posts = getPosts()
return (
<Suspense fallback={<div>載入中...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
然後,在客戶端元件中使用 use
鉤子讀取 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>
)
}
'use client'
import { use } from 'react'
export default function Posts({ posts }) {
const posts = use(posts)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
在上面的範例中,<Posts>
元件被包裹在 <Suspense>
邊界 中。這意味著在 promise 解析期間會顯示 fallback。了解更多關於 串流 的資訊。
社群函式庫
您可以使用社群函式庫如 SWR 或 React 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>載入中...</div>
if (error) return <div>錯誤: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'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>載入中...</div>
if (error) return <div>錯誤: {error.message}</div>
return (
<ul>
{data.map((post) => (
<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)),
})
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
串流
警告: 以下內容假設您的應用程式中啟用了
dynamicIO
配置選項。該標誌在 Next.js 15 canary 中引入。
在伺服器元件中使用 async/await
時,Next.js 會選擇 動態渲染。這意味著資料將在伺服器上為每個使用者請求獲取和渲染。如果有任何慢速資料請求,整個路由將被阻止渲染。
為了改善初始載入時間和使用者體驗,您可以使用串流將頁面的 HTML 分成較小的區塊,並逐步從伺服器發送到客戶端。

在您的應用程式中實現串流有兩種方式:
- 使用
loading.js
檔案 包裹頁面 - 使用
<Suspense>
包裹元件
使用 loading.js
您可以在與頁面相同的資料夾中建立 loading.js
檔案,以便在獲取資料時串流 整個頁面。例如,要串流 app/blog/page.js
,請在 app/blog
資料夾中添加該檔案。

export default function Loading() {
// 在此定義載入 UI
return <div>載入中...</div>
}
export default function Loading() {
// 在此定義載入 UI
return <div>載入中...</div>
}
在導航時,使用者會立即看到佈局和 載入狀態,同時頁面正在渲染。一旦渲染完成,新內容將自動替換。

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

這種方法適用於路由區段(佈局和頁面),但對於更細粒度的串流,您可以使用 <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>
)
}
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 預覽和檢查元件的載入狀態。
範例
順序資料獲取
順序資料獲取發生在樹中的嵌套元件各自獲取其資料且請求未被 去重複 時,導致回應時間更長。

有時您可能需要這種模式,因為一個 fetch 依賴於另一個的結果。
例如,<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>載入中...</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>
)
}
export default async function Page({ params }) {
const { username } = await params
// 獲取藝人資訊
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* 在 Playlists 元件載入時顯示 fallback UI */}
<Suspense fallback={<div>載入中...</div>}>
{/* 將藝人 ID 傳遞給 Playlists 元件 */}
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }) {
// 使用藝人 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) 會平行渲染。因此每個區段都會盡快開始獲取資料。
然而,在_任何_元件中,如果將多個 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} />
</>
)
}
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params }) {
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)
// ...
}
import { getItem } from '@/lib/data'
export default async function Page({ params }) {
const { id } = await params
// 開始載入項目資料
preload(id)
// 執行另一個非同步任務
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id) => {
// void 會評估給定的表達式並返回 undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }) {
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) => {
// ...
})
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})