在 Next.js 中使用 SWR 获取客户端数据

借助 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。本文为该文档的中文译文,示例代码保留原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容