在 React Server Components 中获取数据
在 React Server Components 中用 async 函数获取数据,理解 Next.js 15/16 的缓存默认值,并用 Promise.all 避免请求瀑布。
在 React Server Component 中,获取数据的方式是将组件声明为 async 函数,并在函数体中直接 await 请求——无需 useEffect,无需加载状态,也无需客户端 API 往返。这是 Next.js App Router 的默认模型,它颠覆了从客户端 React 沿袭下来的两个习惯:数据获取移入渲染阶段,而最大的”坑”在于缓存机制——Next.js 15 对此进行了方向性调整。本文将介绍当前的使用模式、Next.js 16 中的缓存现状、为何 "use server" 与 Server Components 毫无关联,以及如何避免请求瀑布流。
核心要点
- 在 Server Component 中,通过将组件声明为
async并 await 请求来获取数据;由于 Server Components 在服务端渲染,凭据和查询逻辑不会包含在客户端 bundle 中,因此可以直接使用 ORM 查询数据库。 - 在当前版本的 Next.js 中,fetch 请求默认不缓存——这与许多教程中描述的 Next.js 13/14 行为恰好相反。
- 如需缓存,须显式选择启用:在旧模型中使用
{ cache: 'force-cache' }或{ next: { revalidate } },或在设置cacheComponents: true后使用use cache指令。 - Server Components 没有对应的指令;“use server” 指令用于 Server Functions(即 Server Actions),这是一个独立的功能特性。
- 对于相互独立的请求,不加
await直接发起,再用Promise.all统一解析,以避免串行瀑布流。
如何在 Server Component 中获取数据?
核心模式只需一步:将组件改为 async 函数并 await 请求。在 App Router 中,layouts 和 pages 默认都是 Server Components,因此无需任何指令即可使用此特性——async 组件是 Server Components 的内置能力,允许在渲染阶段使用 await。
// app/blog/page.tsx
export default async function Page() {
const res = await fetch('https://api.vercel.app/blog')
const posts = await res.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
由于代码仅在服务端运行,可以跳过 API 层,直接查询数据源。使用 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>
)
}
与之对比的是你正在替换的客户端模式:在 useEffect 中发起请求,将结果存入 useState,并将获取逻辑(以及可能的密钥)一并打包发送到浏览器。这种方式会在页面加载后产生额外的客户端-服务端往返;而在服务端直接 await,则彻底消除了这一开销。
Discover how at OpenReplay.com.
fetch 默认不缓存——Next.js 15/16 的变化
在当前版本的 Next.js 中,fetch 默认不缓存——这一点值得明确指出,因为大多数旧版内容的说法恰恰相反。根据当前 Next.js fetch 参考文档,默认行为是 auto no cache:Next.js 在每次请求时都会从远程服务器重新获取资源。旧版缓存模型的行为与此相同——默认情况下,fetch 请求不缓存,如需缓存单个请求,需将 cache 选项设置为 'force-cache'。在 Next.js 13/14 中,不带任何选项的 fetch 默认启用缓存(force-cache),因此依赖该行为的教程均已过时。
在旧版模型下,按需选择缓存方式:
// 缓存直至手动重新验证
await fetch('https://api.example.com/posts', { cache: 'force-cache' })
// 提供缓存数据,最多每 3600 秒重新验证一次
await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } })
Next.js 16 还引入了第二种模型——Cache Components,其启用方式是 use cache 指令。容易让人困惑的地方在于:use cache 是 Cache Components 的功能;要启用它,需在 next.config.ts 文件中添加 cacheComponents 选项。若未设置该标志,use cache 不会生效,此时仍需使用上述 fetch 选项。
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig
import { cacheLife } from 'next/cache'
export async function getPosts() {
'use cache'
cacheLife('hours')
const res = await fetch('https://api.example.com/posts')
return res.json()
}
此处 use cache 指令缓存异步函数和组件的返回值,cacheLife() 用于设置缓存时长。需要注意的是,在 Next.js 16 中,unstable_cache 已被 use cache 指令取代——新代码中请勿再使用它。
| Cache Components 关闭(旧版模型) | Cache Components 开启(cacheComponents: true) | |
|---|---|---|
fetch 默认行为 | 不缓存 | 不缓存 |
| 缓存 fetch 请求 | { cache: 'force-cache' } | use cache 指令 |
| 基于时间的重新验证 | { next: { revalidate: n } } | cacheLife() 配置 |
| 缓存非 fetch 数据 | React.cache / 路由配置 | 函数上使用 use cache |
有两种行为与缓存无关。第一,在一次服务端渲染过程中,使用相同 URL 和选项的 GET fetch 请求会自动进行记忆化(memoize)——如果多个组件调用同一 fetch,Next.js 只会执行一次并共享结果。第二,对于非 fetch 数据源,可将调用包裹在 React 的 cache() 中;React.cache 的作用域仅限于当前请求——每个请求拥有独立的记忆化作用域,请求之间不共享。
“use server” 不能将组件变为 Server Component
如果说有什么东西最容易让 RSC 新手犯错,那就是这个指令。一个常见的误解是,Server Components 通过 “use server” 来标识,但实际上Server Components 没有对应的指令;“use server” 指令用于 Server Functions。Server Components 在 App Router 中就是默认值——一个没有任何指令的文件本身就是 Server Component。"use server" 用于标记 Server Functions(即 Server Actions),根据 React 官方文档,Server Functions 专为更新服务端状态的变更操作而设计,不推荐用于数据获取。读取数据请使用普通的 async Server Component;写入数据才使用 "use server"。
避免瀑布流,流式传输慢速数据
逐一 await 请求会造成串行瀑布流。即便在同一个组件内,如果多个 async/await 请求依次排列,它们仍然是串行执行的;应通过调用 fetch 启动多个请求,再用 Promise.all 统一等待。不加 await 直接调用函数,即可立即发起请求:
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>
}
对于确实较慢的数据请求,不要阻塞整个路由。如果存在耗时较长的数据请求,整个路由将被阻塞,直到所有数据获取完毕才能渲染;为提升加载速度,应将页面拆分为多个块,逐步推送到客户端。将慢速子树包裹在 <Suspense> 中(或添加 loading.js),对于优先级较低的数据,可在服务端启动 Promise,再通过客户端的 use API 读取:
// Server Component — 注意:commentsPromise 未被 await
import { Suspense } from 'react'
import Comments from './comments'
export default async function Page({ id }: { id: string }) {
const commentsPromise = getComments(id)
return (
<Suspense fallback={<p>Loading comments…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
// Client Component
'use client'
import { use } from 'react'
export default function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return comments.map((c) => <p key={c.id}>{c.text}</p>)
}
你在服务端启动 Promise,再通过 use API 在客户端等待它;由于客户端不支持 async 组件,需使用 use 来 await Promise。当出现问题时,故障只会在用户侧可见——Suspense fallback 始终未能解析为内容,或流式传输的块在 hydration 后才到达——而会话回放(session replay)能让你精确还原浏览器在那一时刻的渲染状态。
将密钥保留在服务端边界内
在服务端获取的数据以 props 形式传递给 Client Components,这些 props 必须是可序列化的——只传递普通数据,不传函数或类实例。密钥默认保留在服务端,因为 Server Component 的代码不会被打包发送,但共享模块可能导致泄漏。在 Next.js 中,只有以 NEXT_PUBLIC_ 为前缀的环境变量才会包含在客户端 bundle 中;若变量没有该前缀,Next.js 会将其替换为空字符串。为了加固含有密钥的模块,可在其顶部导入 server-only 包,这样一旦在客户端意外导入,构建时就会报错。React Context 同样无法在 Server Component 中使用——需将其包裹在 'use client' Provider 中,再在服务端 layout 内渲染该 Provider。
思维转变虽小,却需彻底:将 fetch 移入 async 组件并 await,将缓存视为主动选择而非自动发生的事情。从将一个 useEffect 数据获取迁移到 async Server Component 开始,确认请求在服务端执行,然后根据每个路由的实际需求,决定是否通过 { next: { revalidate } } 或 use cache 指令对数据进行缓存。
常见问题
Next.js 16 中 fetch 默认是否缓存?
不缓存。从 Next.js 15 开始并延续至 16,fetch 默认不缓存,每次请求都会访问源服务器。这与 Next.js 13/14 的行为相反——在那些版本中,不带任何选项的 fetch 默认使用 force-cache 进行缓存。在旧版模型下,需按请求显式选择缓存方式:将 cache 设置为 force-cache,或提供 next revalidate 值;声称默认缓存的旧版教程均已过时。
use cache 与 next revalidate 在缓存 fetch 时有何区别?
两者属于不同的缓存模型。next revalidate 选项和 force-cache 适用于旧版默认缓存模型,无需额外配置。use cache 指令是 Cache Components 的功能,只有在 next.config.ts 中将 cacheComponents 设置为 true 时才会生效,其缓存时长通过 cacheLife 控制。若未设置该标志,use cache 不会生效,原本期望缓存的 fetch 请求将在每次请求时静默地访问源服务器。
能否在 Server Component 中使用 useEffect 获取数据?
不能。useEffect 只在浏览器中运行,因此无法在 Server Component 内执行——Server Component 仅在服务端渲染。在 Server Component 中,需将组件声明为 async 并在渲染阶段直接 await 请求,无需加载状态,也无需客户端往返。如果确实需要客户端数据获取,可添加 use client 指令将其变为 Client Component,或在服务端启动 Promise 并通过 React 的 use API 在客户端读取。
use server 指令能将组件变为 Server Component 吗?
不能。Server Components 没有对应的指令;在 App Router 中,它们就是默认值,没有任何指令的文件本身就已经是 Server Component。use server 指令用于标记 Server Functions(也称为 Server Actions),这些函数专为更新服务端状态的变更操作而设计,不推荐用于数据获取。读取数据请使用普通的 async Server Component,写入数据才使用 use server。
使用 Promise.all 并行请求时,若某个 fetch 失败会发生什么?
只要有任意一个请求失败,Promise.all 就会立即拒绝(reject),丢弃其他请求的结果,导致整个渲染失败。如果希望某个请求失败时不影响其他请求,应改用 Promise.allSettled——它会等待所有请求完成并返回每个请求的状态,允许你单独处理各个失败。发起请求时不加 await 以实现并行执行,然后根据失败处理策略选择 Promise.all 或 Promise.allSettled。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k