TanStack Query 与 Suspense 集成
解释 useSuspenseQuery 如何把 pending 和 error 交给 React Boundary,以及它与条件查询、预取、取消和流式状态的边界。
[!info] related notes
- 前置:TanStack Query 服务端状态
- 所属 MOC:TanStack Query 知识地图
- 预取:预取策略与 Suspense
- 错误:错误处理与重试策略
- 流式 UI:前端流式响应模式
TanStack Query 与 Suspense 集成
[!abstract] 学习目标 能根据数据所有权选择
useQuery或useSuspenseQuery,放置合理的 Suspense / Error Boundary,并识别条件查询、请求瀑布和 SSE 流式状态不适合直接照搬 Suspense 示例的原因。
它改变的是渲染控制流
普通 useQuery 把 pending、error、data 都作为返回状态交给组件判断;useSuspenseQuery 在数据未就绪时暂停当前渲染,把 fallback 交给最近的 Suspense Boundary。
function UserPanel({ userId }: { userId: string }) {
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <h2>{data.displayName}</h2>;
}
function Page({ userId }: { userId: string }) {
return (
<ErrorBoundary fallback={<UserError />}>
<Suspense fallback={<UserSkeleton />}>
<UserPanel userId={userId} />
</Suspense>
</ErrorBoundary>
);
}
类型上的直接收益是 data 保证存在;代价是 pending 与部分 error 分支离开当前组件,进入 Boundary 结构。
与 useQuery 的关键差异
| 问题 | useQuery | useSuspenseQuery |
|---|---|---|
data | `T | undefined` |
| 条件启用 | 支持 enabled | 不提供 enabled |
| 占位数据 | 支持 placeholderData | 不提供 |
| pending UI | 组件读取状态 | Suspense fallback |
| error UI | 组件或 Boundary | 通常使用 Error Boundary |
| 取消 | 可使用 query 取消机制 | 官方文档注明不支持取消 |
需要条件查询时,先在父组件判断参数是否存在,再渲染调用 useSuspenseQuery 的子组件;不要把可能为空的 ID 用非空断言硬塞进去。
Error Boundary 的真实语义
useSuspenseQuery 不允许自定义 throwOnError。默认策略是:缓存里没有可显示数据时将错误抛给最近的 Error Boundary;如果已有旧数据,组件仍可渲染旧数据并获得 error / fetching 状态。
如果产品要求“任何错误都进入 Boundary”,可以在组件中显式判断并 throw error。重试时应配合 QueryErrorResetBoundary 或 useQueryErrorResetBoundary 重置 Query 错误状态,而不是粗暴清空整个 QueryClient。
Boundary 放置决定加载体验
- 放得过高:一个次要卡片会让整个页面退回大 fallback。
- 放得过低:页面出现大量闪烁的小 skeleton,结构复杂。
- 较稳定的做法:按用户可感知的内容块放置,并让 Error Boundary 与恢复按钮覆盖同一责任区域。
预取用于提前启动请求
Suspense 是“等待期间渲染什么”,不是“请求何时开始”。如果父组件先 suspend,子组件的请求可能还没有启动,形成瀑布。
可在路由 loader、导航事件或父层使用 ensureQueryData / prefetchQuery 提前启动请求。多个互不依赖的 Suspense Query 放在同一组件时可能串行,应考虑 useSuspenseQueries 或更早预取。
React use(query.promise) 的实验边界
TanStack Query 允许在开启 experimental_prefetchInRender 后,把 useQuery().promise 交给 React use()。这不是普通 useQuery 的默认行为,也不应因为课程展示就直接写入生产代码。
QueryClient 开启实验选项
-> useQuery 返回稳定 promise
-> React use(promise)
-> Suspense Boundary 接管等待
优先使用稳定的 useSuspenseQuery;只有明确需要并接受实验 API 风险时再评估 use(query.promise)。
BodySense 为什么目前继续使用 useQuery
useConsultationSessionQuery.ts 使用:
conversationId: string | nullenabled: !!conversationId- 自定义
staleTime - 关闭窗口聚焦、重连和挂载时自动重取
这是一份“可能尚无 conversationId 的服务端快照”,useQuery 能直接表达条件启用。若改成 useSuspenseQuery,应由父组件在 ID 存在后才渲染查询组件,而不是保留 enabled。
当前 Active Turn 的 token、tool call、交互中断来自持续事件流,它不是“一个最终 Promise resolve 后得到的快照”。这部分继续由 useSSEProcessor、Reducer 与 Context 处理更自然:
历史会话 / 可重取快照 -> TanStack Query
当前流式增量事件 -> SSE + reducer
页面块初次加载等待 -> 可评估 Suspense
常见错误
- 认为 Suspense 自动预取数据或消除请求瀑布。
- 在
useSuspenseQuery上照抄enabled、placeholderData或可配置throwOnError。 - 把一个无限增量 SSE 流当成一次会完成的普通 Query。
- 没有 Error Boundary 和错误重置路径。
- 用过高的 Boundary 让局部请求阻塞整页。
- 忽略官方注明的取消限制。
从阅读到独立设计
- 预测:把
useConsultationSessionQuery原样改成useSuspenseQuery,哪两个选项首先失效? - 模仿:在父组件判断
conversationId,仅在非空时渲染 Suspense 查询子组件。 - 重建:不看示例,为“用户资料卡”设计 Query、Suspense、Error Boundary 与重试结构。
- 迁移:解释为什么咨询历史适合 Query,而正在追加的 token 不应直接套同一个模型。