借助 SWR,可以在客户端组件中获取数据,由服务端组件提供初始数据,并协调浏览器中的修改操作与服务端缓存。关于如何选择模式,参阅 Next.js 客户端数据获取指南。
在客户端获取数据
如果初始界面可以等到水合完成后再发起浏览器请求,就可以让 SWR 完全在浏览器中获取数据。根据加载提示应该出现的位置,选择内联加载状态或 Suspense 加载状态。下面的示例中,query 初始为空,水合后随着客户端状态更新。
如果希望组件自行渲染加载状态与错误状态,使用 useSWR。条件式 key 会把请求延迟到交互产生有效输入之后:
app/product-autocomplete.tsx
'use client'
import useSWR from 'swr'
type Product = { id: string; name: string }
async function fetcher(url: string): Promise<Product[]> {
const response = await fetch(url)
if (!response.ok) throw new Error('Failed to fetch products')
return response.json()
}
export function ProductAutocomplete({ query }: { query: string }) {
const {
data = [],
error,
isLoading,
} = useSWR(
query ? `/api/products?query=${encodeURIComponent(query)}` : null,
fetcher
)
if (!query) return null
if (error) return <p>Failed to load products.</p>
if (isLoading) return <p>Loading products...</p>
return (
<ul>
{data.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
对客户端数据使用 Suspense
如果希望最近的 Suspense 边界定义加载界面,设置 suspense: true。应将可交互的外层界面放在边界之外,使结果加载期间仍能操作。
app/product-autocomplete.tsx
'use client'
import { Suspense } from 'react'
import useSWR from 'swr'
type Product = { id: string; name: string }
async function fetcher(url: string): Promise<Product[]> {
const response = await fetch(url)
if (!response.ok) throw new Error('Failed to fetch products')
return response.json()
}
export function ProductAutocomplete({ query }: { query: string }) {
if (!query) return null
return (
<Suspense fallback={<p>Loading products...</p>}>
<ProductResults query={query} />
</Suspense>
)
}
function ProductResults({ query }: { query: string }) {
const { data } = useSWR(
`/api/products?query=${encodeURIComponent(query)}`,
fetcher,
{ suspense: true }
)
return (
<ul>
{data.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
使用无条件 key 时,Suspense 结束后 SWR 会提供已定义的 data。请求错误应由最近的错误边界处理。
请求正在执行,而且尚无已加载数据可展示时,isLoading 为 true。只要有请求正在执行,包括后台重新验证,isValidating 就为 true。
设置 suspense: true 后,Suspense 处理初始无数据状态。同一个 key 后续重新验证时,会保留当前已渲染的数据,而不是再次显示 Suspense 的 fallback。可以用 isValidating 提供后台刷新提示。更多说明见 SWR 加载状态和数据获取文档。
补充说明:独立的 Suspense 数据读取放在兄弟组件中渲染时,可以并行开始;放在同一个组件中的多个 Suspense 读取则会顺序执行。参阅网络瀑布与 SWR Suspense。
由服务端组件提供初始数据
如果初次渲染就需要数据,而后续仍要由 SWR 在浏览器中管理它,使用服务端提供数据的模式。在 SWR 2.3.0 与 React 19 中,服务端组件可以在客户端接管之前提供 fallback 数据。
将 <SWRConfig> 的作用范围限制在拥有该数据的路由段。这样 provider 中的 fallback 更靠近使用它的组件,也避免把具体功能的数据放到共享布局中。
TypeScript 版本:
app/products/[id]/page.tsx
import { Suspense } from 'react'
import { SWRConfig } from 'swr'
import { getProduct } from './data' // some server-side function
import { productCache } from './product-cache'
import { ProductView } from './product-view'
export default function Page({ params }: PageProps<'/products/[id]'>) {
return (
<Suspense fallback={<p>Loading…</p>}>
{params.then(({ id }) => (
<ProductData id={id} />
))}
</Suspense>
)
}
function ProductData({ id }: { id: string }) {
return (
<SWRConfig
value={{
fallback: {
// Not awaited: only components that read this key suspend
[productCache.key(id)]: getProduct(id),
},
}}
>
<ProductView id={id} />
</SWRConfig>
)
}
JavaScript 版本:
app/products/[id]/page.js
import { Suspense } from 'react'
import { SWRConfig } from 'swr'
import { getProduct } from './data' // some server-side function
import { productCache } from './product-cache'
import { ProductView } from './product-view'
export default function Page({ params }) {
return (
<Suspense fallback={<p>Loading…</p>}>
{params.then(({ id }) => (
<ProductData id={id} />
))}
</Suspense>
)
}
function ProductData({ id }) {
return (
<SWRConfig
value={{
fallback: {
// Not awaited: only components that read this key suspend
[productCache.key(id)]: getProduct(id),
},
}}
>
<ProductView id={id} />
</SWRConfig>
)
}
fallback 与客户端组件必须使用相同的 SWR key。将 key 定义一次,让两处调用共享同一标识。
app/products/[id]/product-cache.ts
export const productCache = {
key: (id: string) => `/api/products/${id}`,
}
app/products/[id]/product-cache.js
export const productCache = {
key: (id) => `/api/products/${id}`,
}
在这个页面示例中,params.then() 返回的 Promise 会让 fallback 保持显示,直到路由参数解析完成。随后 ProductData 为 SWR fallback 创建另一个未被 await 的 getProduct(id) Promise。
React 将该 Promise 通过 React Server Component 载荷传递给客户端;读取对应 key 的组件会挂起,直到数据解析完成。
客户端组件使用相同的 key,通过 useSWR 读取数据:
app/products/[id]/product-view.tsx
'use client'
import useSWR from 'swr'
import { productCache } from './product-cache'
type Product = { id: string; name: string }
async function fetcher(url: string): Promise<Product> {
const response = await fetch(url)
if (!response.ok) throw new Error('Failed to fetch product')
return response.json()
}
export function ProductView({ id }: { id: string }) {
const { data } = useSWR(productCache.key(id), fetcher, { suspense: true })
return <h1>{data.name}</h1>
}
app/products/[id]/product-view.js
'use client'
import useSWR from 'swr'
import { productCache } from './product-cache'
async function fetcher(url) {
const response = await fetch(url)
if (!response.ok) throw new Error('Failed to fetch product')
return response.json()
}
export function ProductView({ id }) {
const { data } = useSWR(productCache.key(id), fetcher, { suspense: true })
return <h1>{data.name}</h1>
}
补充说明:fallback 的 key 与 useSWR 的 key 必须完全一致。如果二者不一致,SWR 会忽略 fallback,转而在客户端发起请求。
fallback 提供 hook 的初始值。默认情况下,SWR 将 fallback 数据视为过期数据,并在水合后从浏览器发起重新验证。
SWR 不为 fallback 提供基于时间的新鲜度窗口。设置 revalidateIfStale: false,可在 hook 挂载时已有缓存数据的情况下跳过重新验证。与 TanStack Query 的 staleTime 不同,这一设置会作用于每次挂载。
窗口重新获得焦点、重新联网、轮询,以及 mutate,仍然可以触发该 key 的重新验证。如果希望按固定周期刷新,设置 refreshInterval。
SWR key 指向一个实现 GET 方法的 Route Handler。这个处理程序可以调用用于提供 fallback 的同一个 getProduct 函数,而浏览器使用相应 URL 执行重新验证和轮询。
进一步参阅 SWR 参数与 key以及 SWR 与 Next.js App Router。
使用 Cache Components 缓存服务端提供的数据
使用这一模式前,先在 next.config.ts 中启用 cacheComponents。随后就可以缓存作为 SWR fallback 的服务端数据。
添加 use cache,选择 cacheLife 配置,并通过 cacheTag 添加标签,使修改操作能够使缓存失效。
app/products/[id]/data.ts
import { cacheLife, cacheTag } from 'next/cache'
export async function getProduct(id: string) {
'use cache'
cacheLife('max')
cacheTag(`product:${id}`)
const product = await db.product.findUnique({ where: { id } })
if (!product) throw new Error('Product not found')
return product
}
app/products/[id]/data.js
import { cacheLife, cacheTag } from 'next/cache'
export async function getProduct(id) {
'use cache'
cacheLife('max')
cacheTag(`product:${id}`)
const product = await db.product.findUnique({ where: { id } })
if (!product) throw new Error('Product not found')
return product
}
示例使用 cacheLife('max'),因为写入操作会使产品标签对应的缓存失效。缓存配置中的 stale 控制 Next.js 客户端缓存可复用预取载荷的时间,revalidate 和 expire 则控制服务端缓存。如果服务端值需要随时间刷新,应选择更短的配置。
SWR 拥有独立的浏览器缓存,因此其重新验证选项不必与 cacheLife 一致。
当同一个修改操作既更新浏览器缓存,又使服务端缓存失效时,可以把两层缓存的标识放在同一份共享定义中:
app/products/[id]/product-cache.ts
export const productCache = {
key: (id: string) => `/api/products/${id}`,
+ tag: (id: string) => `product:${id}`,
}
app/products/[id]/product-cache.js
export const productCache = {
key: (id) => `/api/products/${id}`,
+ tag: (id) => `product:${id}`,
}
服务端函数随后可以调用 cacheTag(productCache.tag(id))。这份共享定义不应导入仅服务端或仅客户端的模块,使两层缓存都能复用它。
修改数据后协调服务端与客户端缓存
fallback 提供初始值;水合完成后,SWR 管理浏览器缓存和重新验证。对于需要复用的数据,将 SWR key 与服务端标签放在同一份缓存定义中:
app/activity/activity-cache.ts
export const activityCache = {
key: '/api/activity/unread',
tag: (userId: string) => `activity:${userId}`,
}
app/activity/activity-cache.js
export const activityCache = {
key: '/api/activity/unread',
tag: (userId) => `activity:${userId}`,
}
fallback、客户端组件的数据读取、以及修改操作都使用相同 key。在 SWR 中,把写入操作传给 mutate,并提供 optimisticData。
SWR 会立即展示乐观更新值;写入失败时再回滚。下面的例子已经知道最终结果,因此 Server Action 成功后继续保留乐观更新的值。
app/activity/mark-read-button.tsx
'use client'
import { useSWRConfig } from 'swr'
import { markActivityReadAction } from './actions'
import { activityCache } from './activity-cache'
export function MarkReadButton() {
const { mutate } = useSWRConfig()
function markRead() {
return mutate(
activityCache.key,
async () => {
await markActivityReadAction()
return { count: 0 }
},
{
optimisticData: { count: 0 },
revalidate: false,
rollbackOnError: true,
throwOnError: false,
}
)
}
return <button onClick={markRead}>Mark read</button>
}
app/activity/mark-read-button.js
'use client'
import { useSWRConfig } from 'swr'
import { markActivityReadAction } from './actions'
import { activityCache } from './activity-cache'
export function MarkReadButton() {
const { mutate } = useSWRConfig()
function markRead() {
return mutate(
activityCache.key,
async () => {
await markActivityReadAction()
return { count: 0 }
},
{
optimisticData: { count: 0 },
revalidate: false,
rollbackOnError: true,
throwOnError: false,
}
)
}
return <button onClick={markRead}>Mark read</button>
}
Server Action 将变更写入数据库,再使用 updateTag 使带标签的服务端缓存过期。服务端的缓存查询必须使用 cacheTag 添加对应标签,且标签应一致:
app/activity/actions.ts
'use server'
import { updateTag } from 'next/cache'
import {
getCurrentUserId,
markActivityRead as markActivityReadInDatabase,
} from './data'
import { activityCache } from './activity-cache'
export async function markActivityReadAction() {
const userId = await getCurrentUserId()
await markActivityReadInDatabase(userId)
updateTag(activityCache.tag(userId))
}
app/activity/actions.js
'use server'
import { updateTag } from 'next/cache'
import {
getCurrentUserId,
markActivityRead as markActivityReadInDatabase,
} from './data'
import { activityCache } from './activity-cache'
export async function markActivityReadAction() {
const userId = await getCurrentUserId()
await markActivityReadInDatabase(userId)
updateTag(activityCache.tag(userId))
}
SWR 的乐观值更新当前界面;updateTag 确保下次读取服务端缓存时拿到新的活动数据。
补充说明:如果 Server Action 修改了一项缓存读取依赖的数据,并且该读取必须立即反映写入结果,就调用 updateTag。未缓存的数据读取没有服务端标签可更新。其他失效行为见客户端数据获取指南中的修改协调说明。
可查看 next-spa-patterns 在线演示及其源代码。进一步学习见 SWR 修改与乐观更新、SWR 文档。
相关指南与参考
- 获取数据:获取数据并流式传输依赖数据的内容。
- 单页应用:Next.js 对 SPA 的完整支持。
- 缓存:在 Next.js 中缓存数据和界面。
- use cache:使用该指令缓存应用数据。
- route.js:特殊路由文件的 API 参考。
- updateTag:缓存标签更新函数参考。
原文:How to fetch client-side data with SWR。本文为该文档的中文译文,示例代码保留原文。











暂无评论内容