資料擷取和快取
本指南將引導您了解 Next.js 中資料擷取和快取的基礎知識,並提供實用的範例和最佳實務。
以下是 Next.js 中資料擷取的最小範例
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
此範例示範了在非同步 React 伺服器元件中使用 fetch
API 進行基本的伺服器端資料擷取。
參考
fetch
- React
cache
- Next.js
unstable_cache
範例
使用 fetch
API 在伺服器上擷取資料
此元件將擷取並顯示部落格文章列表。從 fetch
回應的資料將會自動快取。
export default async function Page() {
let data = await fetch('https://api.vercel.app/blog')
let posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
如果您在此路由中的任何其他位置沒有使用任何動態 API,它將會在 next build
期間預先渲染成靜態頁面。然後可以使用增量靜態再生來更新資料。
如果您不想快取 fetch
的回應,您可以執行以下操作
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
使用 ORM 或資料庫在伺服器上擷取資料
此元件將擷取並顯示部落格文章列表。預設情況下,來自資料庫的回應不會被快取,但可以透過額外設定來快取。
import { db, posts } from '@/lib/db'
export default async function Page() {
let allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
如果您在此路由中的任何其他位置沒有使用任何動態 API,它將會在 next build
期間預先渲染成靜態頁面。然後可以使用增量靜態再生來更新資料。
要防止頁面預先渲染,您可以將以下內容添加到您的檔案中
export const dynamic = 'force-dynamic'
然而,您通常會使用像是 cookies
、headers
或從頁面屬性讀取傳入的 searchParams
等函式,這將會自動使頁面動態渲染。在這種情況下,您不需要明確地使用 force-dynamic
。
在客戶端上擷取資料 或 React Query )來進行客戶端擷取。
app/page.tsx'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
let res = await fetch('https://api.vercel.app/blog')
let data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
使用 ORM 或資料庫快取資料
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
let res = await fetch('https://api.vercel.app/blog')
let data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
您可以使用 unstable_cache
API 來快取回應,以便在執行 next build
時預先渲染頁面。
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
export default async function Page() {
const allPosts = await getPosts()
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
此範例將資料庫查詢的結果快取 1 小時(3600 秒)。它還添加了快取標籤 posts
,然後可以使用增量靜態再生使其失效。
在多個函式間重複使用資料
Next.js 使用像是 generateMetadata
和 generateStaticParams
等 API,您需要在 page
中使用相同的已擷取資料。
如果您正在使用 fetch
,請求會自動被記憶體化。這表示您可以安全地使用相同的選項呼叫相同的 URL,且只會發出一個請求。
import { notFound } from 'next/navigation'
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
let res = await fetch(`https://api.vercel.app/blog/${id}`)
let post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
let posts = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return posts.map((post: Post) => ({
id: post.id,
}))
}
export async function generateMetadata({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return {
title: post.title,
}
}
export default async function Page({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
如果您*沒有*使用 fetch
,而是直接使用 ORM 或資料庫,您可以使用 React 的 cache
函式包裝您的資料擷取。這將會去除重複資料,並且只進行一次查詢。
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db' // Example with Drizzle ORM
import { notFound } from 'next/navigation'
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
if (!post) notFound()
return post
})
重新驗證快取的資料
深入瞭解如何使用增量靜態再生重新驗證快取的資料。
模式 平行和循序資料擷取
在元件內擷取資料時,您需要注意兩種資料擷取模式:平行和循序。


- 循序:元件樹中的請求彼此相依。這可能導致較長的載入時間。
- 平行:路由中的請求會被積極地啟動,並同時載入資料。這減少了載入資料所需的總時間。
循序資料擷取
如果您有巢狀元件,且每個元件都擷取自己的資料,那麼如果這些資料請求沒有被記憶體化,資料擷取將會循序發生。
在某些情況下,您可能需要這種模式,因為一個擷取取決於另一個擷取的結果。例如,Playlists
元件只有在 Artist
元件完成資料擷取後才會開始擷取資料,因為 Playlists
取決於 artistID
屬性。
app/artist/[username]/page.tsxexport default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// 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>
)
}
您可以使用loading.js
(適用於路由區段)或React 的 <Suspense>
(適用於巢狀元件)來顯示即時載入狀態,同時 React 串流傳輸結果。
這樣可以避免整個路由被數據請求阻塞,並且用戶可以與頁面中已準備好的部分進行互動。
平行數據獲取
默認情況下,佈局和頁面區段是平行渲染的。這意味著請求將會平行發起。
然而,由於 async
/await
的特性,在同一個區段或組件內被 awaited 的請求將會阻塞其後的任何請求。
要平行獲取數據,您可以透過在使用數據的組件外部定義請求來提前發起請求。這樣可以透過平行發起兩個請求來節省時間,然而,在兩個 promise 都被 resolve 之前,用戶不會看到渲染的結果。
在下面的例子中,getArtist
和 getAlbums
函數定義在 Page
組件之外,並在組件內使用 Promise.all
發起。
app/artist/[username]/page.tsximport 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: { username },
}: {
params: { username: string }
}) {
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// Initiate both requests in parallel
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
此外,您可以添加一個 Suspense Boundary (Suspense 邊界) 來分解渲染工作,並盡快顯示部分結果。
預加載數據
另一種防止瀑布式請求的方法是使用「預加載」模式,方法是創建一個工具函數,在阻塞請求之前提前調用它。例如,checkIsAvailable()
會阻塞 <Item/>
的渲染,因此您可以在它之前調用 preload()
來提前發起 <Item/>
的數據依賴。等到 <Item/>
渲染時,它的數據已經被獲取了。
請注意,preload
函數不會阻塞 checkIsAvailable()
的運行。
components/Item.tsximport { getItem } from '@/utils/get-item'
export const preload = (id: string) => {
// void evaluates the given expression and returns 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)
// ...
}
app/item/[id]/page.tsximport Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
注意事項:「預加載」函數也可以有其他名稱,因為它是一種模式,而不是一個 API。
結合使用 React cache
和 server-only
與預加載模式
您可以結合 cache
函數、preload
模式和 server-only
套件來創建一個可以在整個應用程式中使用的數據獲取工具。
utils/get-item.tsimport { 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 的 taint API,taintObjectReference
和 taintUniqueValue
,以防止整個物件實例或敏感值被傳遞到客戶端。
要在您的應用程式中啟用 taint 功能,請將 Next.js 設定檔中的 experimental.taint
選項設為 true
next.config.jsmodule.exports = {
experimental: {
taint: true,
},
}
然後將您想要 taint 的物件或值傳遞給 experimental_taintObjectReference
或 experimental_taintUniqueValue
函式
app/utils.tsimport { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'Do not pass the whole user object to the client',
data
)
experimental_taintUniqueValue(
"Do not pass the user's address to the client",
data,
data.address
)
return data
}
app/page.tsximport { getUserData } from './data'
export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // this will cause an error because of taintObjectReference
address={userData.address} // this will cause an error because of taintUniqueValue
/>
)
}
在元件內擷取資料時,您需要注意兩種資料擷取模式:平行和循序。


- 循序:元件樹中的請求彼此相依。這可能導致較長的載入時間。
- 平行:路由中的請求會被積極地啟動,並同時載入資料。這減少了載入資料所需的總時間。
循序資料擷取
如果您有巢狀元件,且每個元件都擷取自己的資料,那麼如果這些資料請求沒有被記憶體化,資料擷取將會循序發生。
在某些情況下,您可能需要這種模式,因為一個擷取取決於另一個擷取的結果。例如,Playlists
元件只有在 Artist
元件完成資料擷取後才會開始擷取資料,因為 Playlists
取決於 artistID
屬性。
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// 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>
)
}
您可以使用loading.js
(適用於路由區段)或React 的 <Suspense>
(適用於巢狀元件)來顯示即時載入狀態,同時 React 串流傳輸結果。
這樣可以避免整個路由被數據請求阻塞,並且用戶可以與頁面中已準備好的部分進行互動。
平行數據獲取
默認情況下,佈局和頁面區段是平行渲染的。這意味著請求將會平行發起。
然而,由於 async
/await
的特性,在同一個區段或組件內被 awaited 的請求將會阻塞其後的任何請求。
要平行獲取數據,您可以透過在使用數據的組件外部定義請求來提前發起請求。這樣可以透過平行發起兩個請求來節省時間,然而,在兩個 promise 都被 resolve 之前,用戶不會看到渲染的結果。
在下面的例子中,getArtist
和 getAlbums
函數定義在 Page
組件之外,並在組件內使用 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: { username },
}: {
params: { username: string }
}) {
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// Initiate both requests in parallel
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
此外,您可以添加一個 Suspense Boundary (Suspense 邊界) 來分解渲染工作,並盡快顯示部分結果。
預加載數據
另一種防止瀑布式請求的方法是使用「預加載」模式,方法是創建一個工具函數,在阻塞請求之前提前調用它。例如,checkIsAvailable()
會阻塞 <Item/>
的渲染,因此您可以在它之前調用 preload()
來提前發起 <Item/>
的數據依賴。等到 <Item/>
渲染時,它的數據已經被獲取了。
請注意,preload
函數不會阻塞 checkIsAvailable()
的運行。
import { getItem } from '@/utils/get-item'
export const preload = (id: string) => {
// void evaluates the given expression and returns 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)
// ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
注意事項:「預加載」函數也可以有其他名稱,因為它是一種模式,而不是一個 API。
結合使用 React cache
和 server-only
與預加載模式
您可以結合 cache
函數、preload
模式和 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 的 taint API,taintObjectReference
taintUniqueValue
要在您的應用程式中啟用 taint 功能,請將 Next.js 設定檔中的 experimental.taint
選項設為 true
module.exports = {
experimental: {
taint: true,
},
}
然後將您想要 taint 的物件或值傳遞給 experimental_taintObjectReference
或 experimental_taintUniqueValue
函式
import { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'Do not pass the whole user object to the client',
data
)
experimental_taintUniqueValue(
"Do not pass the user's address to the client",
data,
data.address
)
return data
}
import { getUserData } from './data'
export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // this will cause an error because of taintObjectReference
address={userData.address} // this will cause an error because of taintUniqueValue
/>
)
}
這有幫助嗎?