NextJS App Router中Suspense下客户端组件静态渲染疑问
关于Next.js App Router中Suspense与React Query的渲染机制疑问
我为此问题准备了复现仓库。
背景
我正在探索Next.js App Router与Suspense的结合使用,编写了两个基于React Query实现数据获取的客户端组件:一个使用useQuery,另一个使用useSuspenseQuery。
组件代码
使用useQuery的组件
export function TodosRq() { const query = useQuery<Array<{id: number, title: string}>>({ queryKey: ['todos'], queryFn: async () => { await new Promise((res) => setTimeout(res, 10000)); const res = await fetch("https://jsonplaceholder.typicode.com/todos") return res.json(); } }) return <div> {query.data?.map((v) => { return <div> RQ {v.id} {v.title} </div> })} </div> }
使用useSuspenseQuery的组件
export function TodosRqSuspense() { const query = useSuspenseQuery<Array<{id: number, title: string}>>({ queryKey: ['todos'], queryFn: async () => { await new Promise((res) => setTimeout(res, 10000)); const res = await fetch("https://jsonplaceholder.typicode.com/todos") return res.json(); } }) return <div> {query.data.map((v) => { return <div> RQ {v.id} {v.title} </div> })} </div> }
页面渲染方式
在App Router页面中,我可以选择渲染其中任意一个组件:
渲染TodosRq
{/* 注:这个Suspense边界不会生效 */} <Suspense fallback={'rq loading'}> <h2>Todos RQ</h2> <TodosRq/> </Suspense>
渲染TodosRqSuspense
<Suspense fallback={'rq loading'}> <h2>Todos RQ</h2> <TodosRqSuspense/> </Suspense>
预期与实际行为
预期行为
服务端渲染应用的加载状态并流式传输到客户端,之后由客户端发起API请求获取数据。
实际现象
使用useSuspenseQuery的TodosRqSuspense组件在Next.js中被应用了静态渲染,即在生产构建中直接返回预渲染的HTML,无需等待10秒的请求延迟。
开发服务器与生产构建的行为差异
| 组件 | 开发服务器 | 生产构建 |
|---|---|---|
| TodosRq | 无法看到Suspense边界,需等待10秒才显示内容,内容不在根文档中 | 无法看到Suspense边界,需等待10秒才显示内容,内容不在根文档中 |
| TodosRqSuspense | 可看到Suspense边界,等待10秒后显示内容,内容在根文档中(开发服务器似乎能修改网络请求响应体) | 立即显示内容,内容在根文档中 |
我对相关机制的理解
- 静态与动态渲染:服务端组件默认会被静态渲染(构建时渲染),即使涉及数据获取,除非使用
cookies或connection等动态API才会触发动态渲染。 - 客户端组件:客户端组件会在服务端预渲染(首次渲染在服务端),之后在客户端接管执行。
- Suspense:允许首次渲染显示加载骨架,若使用RSC则会流式加载内容,但我原本以为客户端组件仍会在客户端进行数据获取并在完成后显示。
- Next.js建议:Next.js建议在服务端组件中进行数据获取,不过在迁移场景下保留客户端组件的写法是合理的,我希望理解其中的细节。
注:若在客户端组件中添加useSearchParams,则行为会符合我的预期。
疑问
请问我对这一机制的理解存在哪些偏差?
内容的提问来源于stack exchange,提问作者dwjohnston
相关产品推荐
相关产品推荐

