Convex + TanStack Query:useQuery 与 useSuspenseQuery 的选择、原理与场景
8月 12, 2026
AI 参与说明(Agent:/root、/root/tanstack_suspense_docs、/root/convex_react_research、/root/blog_preflight):本文由 Agent 根据 Convex、TanStack Query、React 的官方文档及公开源码辅助整理,重点核对实时订阅、Suspense、SSR、条件查询与缓存生命周期的边界。资料核验于 2026-08-12;
@convex-dev/react-query仍是 beta,TanStack Start 仍处于 Release Candidate,实施前请以项目 lockfile、目标版本和文末一手资料为准。
适用范围:本文讨论 React、TanStack Query v5 与
@convex-dev/react-queryadapter 的组合。文中的useQuery与useSuspenseQuery默认都从@tanstack/react-query导入;它们不是convex/react的同名原生 Hook。示例假设项目已经有可调用的 public Convex query,并以api.projects.get为占位名称;替换为自己的 generated API、参数与权限模型后再运行。
示例运行说明:文中的
ProjectView、ProjectSkeleton、MessagesRoute等是应用自己的 UI 组件,未在本文重复实现。要复现行为,需要让api.projects.get接受{ projectId: v.id("projects") }并返回项目 document 或null,设置 deployment URL,运行npx convex dev生成 API,再把片段放进已完成 Provider 配置的 React 应用。预期行为是:"skip"不建立订阅;冷 cache 下常规 Hook 显示局部 loading,而 Suspense Hook 显示最近 boundary 的 fallback;相关 Convex mutation 后两者都会收到推送更新。
先给结论:先判断“谁负责缺数据时的 UI”#
在 Convex + TanStack Query 的组合里,useQuery 与 useSuspenseQuery 不是两种实时性等级,也不是“一个缓存、一个不缓存”。对于同一个 convexQuery(api.foo.bar, args),它们使用同一套 Convex subscription:数据库相关数据改变后,Convex 会把新结果推到客户端,adapter 再更新 TanStack Query cache。二者的核心差异是:数据暂时不可用时,组件继续渲染并自行处理状态,还是让 React Suspense 暂停该子树并由 boundary 接管。
可以先用下面四个问题判断:
- 没有最终数据,这块 UI 还能否正确、有价值地显示? 能,例如筛选器、页面框架、侧栏或可选卡片:优先
useQuery。不能,例如详情主体、会话正文:可考虑useSuspenseQuery。 - 参数和身份是否已经确定?
projectId、搜索词、认证 token、tab 是否打开等任一项还未就绪:优先useQuery,用"skip"条件订阅;或在外层先 gate,等确定后再挂载 Suspense 子组件。 - 是否有恰当的
<Suspense>和<ErrorBoundary>? 没有明确的加载/错误边界设计时,不要仅为了data类型不再带undefined而改用useSuspenseQuery。 - 是否在 TanStack Start 做首屏 SSR,且这份数据必须进入初始 HTML? 是:
useSuspenseQuery是 Convex 官方给出的直接路径;关键数据也可由 route loader 的ensureQueryData提前取得。
一句口诀:
能局部降级、需要条件订阅、需要保留旧内容时用
useQuery;数据是组件成立的前提、输入已确定、边界清晰且 SSR 流程已验证时用useSuspenseQuery。多个彼此独立且都必须的数据,使用useSuspenseQueries或提前预取,不要连续写多个useSuspenseQuery。
1. 先消除三个同名 API 的混淆#
“Convex + TanStack Query”至少涉及三组 API。没有先区分它们,很容易把文档和代码混在一起:
// 1. Convex 原生 React Hook:不经过 TanStack Query。
import { useQuery as useConvexQuery } from "convex/react";
// 2. 本文比较的两个 Hook:由 TanStack Query 提供。
import {
useQuery as useTanStackQuery,
useSuspenseQuery,
} from "@tanstack/react-query";
// 3. adapter:把一个 Convex public query 变成 TanStack Query options。
import { convexQuery } from "@convex-dev/react-query";convex/react 的原生 useQuery 在首次加载时返回 undefined,之后返回 query 结果;它会自动建立并维护订阅。它本身不会因为被包在 <Suspense> 中就抛出 Promise,因此不会自动激活 Suspense fallback。Convex React
useSuspenseQuery 则是 TanStack Query v5 的专用 Suspense Hook。要让它订阅 Convex query,需要 @convex-dev/react-query 的 ConvexQueryClient 与 convexQuery(...);不要把它当成 Convex 原生 API。Convex with TanStack Query TanStack useSuspenseQuery
如果项目没有 TanStack Router/Start、TanStack Devtools、统一管理外部 HTTP query 等明确需求,直接使用 convex/react 原生 useQuery 往往已经足够:它已有订阅、自动更新和客户端一致性。引入 adapter 的理由应是 TanStack 的路由、SSR/hydration、Devtools 或统一状态模型,而不是希望把 Convex 数据“变得更实时”。
2. Adapter 的原理:TanStack cache 是订阅的投影层#
下面这张图描述的是客户端的概念流程;不是另一套数据库同步协议:
flowchart TD
A["React component"] --> B{"TanStack Hook"}
B --> C["QueryClient cache\nquery key / observer / gcTime"]
C --> D["ConvexQueryClient"]
D --> E["ConvexReactClient watch subscription\nWebSocket"]
E --> F["Convex public query"]
F --> G["Convex database transaction state"]
G --> F
F -->|"initial result / changed result"| E
E -->|"push result"| D
D -->|"setQueryData"| C
C --> BConvexQueryClient 监听 TanStack QueryClient 的 query 生命周期,为 Convex query 建立 WebSocket subscription,并把服务器推送的结果写回 cache。convexQuery() 负责构造兼容的 query key 和 options;它会把 staleTime 设为 Infinity。adapter 介绍 ConvexQueryClient API
这带来几个与普通 fetch query 不同的后果:
- 对 reactive
convexQuery,结果不是靠轮询“过期后再取”;相关数据变更时服务器主动推送,因此isStale始终为false。 - 对这类 query,
retry、refetchOnWindowFocus、refetchInterval、写后invalidateQueries()不应被当作数据新鲜度方案。Convex 负责连接重试和推送,mutation 后相关 query 会自动得到新结果。 gcTime不只是内存回收时间:最后一个 observer 卸载后,adapter 仍会保留 subscription 直到gcTime到期。默认是 5 分钟;短一些能减少离屏 query 活动,长一些能让页面切换回来时立即有最新数据。- 这个结论只适用于
convexQuery这种 reactive query。外部 HTTP query 仍遵循普通 TanStack Query 规则;convexAction()也不是 reactive query,应按它自己的 refetch/失效需求设计。
因此,TanStack cache 在这里是渲染状态、生命周期、预取与 SSR 的适配层,不是需要人工维护为“第二份真相”的缓存。 不要把 REST 项目里“mutation 成功后必定 invalidateQueries”的习惯原样搬过来。与 fetch-based query 的差异
3. 正确接入:两个 Provider、一个 Convex client#
先安装 adapter。若项目已经安装 convex 与 @tanstack/react-query,npm 会复用已有依赖版本;应将三者固定在 lockfile 中。
npm install convex @tanstack/react-query @convex-dev/react-query以下是纯 CSR 入口的最小配置:
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { App } from "./App";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
const convexQueryClient = new ConvexQueryClient(convex);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
// 对同一个 QueryClient 只连接一次。
convexQueryClient.connect(queryClient);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ConvexProvider client={convex}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ConvexProvider>
</StrictMode>,
);必须把同一个 ConvexReactClient 同时交给 ConvexProvider 和 ConvexQueryClient。这样,必要时仍可使用原生 Convex Hook(例如某些 pagination API),且整个应用只有一份客户端连接上下文。connect() 必须只执行一次;重复连接会报错。官方 setup API 约束
上例仅适用于浏览器入口。SSR 框架不能把这个 QueryClient 作为跨请求的模块级单例;应按框架的 server/client provider 生命周期创建它。TanStack Start 的处理方式见后文。
也不要为了接入 TanStack 自己写下面的 queryFn:它是一次性读取,不会得到 adapter 建立的持续 subscription。
// 不要用它替代 convexQuery();这里只会得到一次性结果。
queryFn: () => convex.query(api.projects.get, { projectId })4. useQuery 与 useSuspenseQuery:同一数据源,不同渲染契约#
| 维度 | useQuery(convexQuery(...)) | useSuspenseQuery(convexQuery(...)) |
|---|---|---|
| 冷启动、没有数据 | 正常返回 result object;组件自行看 data、isPending、isError | render 阶段 suspend,由最近 <Suspense fallback> 接管 |
data 类型 | 通常是 TData | undefined | TData,保证已定义 |
| Loading 的归属 | 组件内部可做局部 skeleton、禁用按钮、渐进显示 | boundary 统一决定整块区域的 fallback |
| Error 的归属 | 可就地显示 error,或通过 throwOnError 交给边界 | 初始无数据的 error 会到 ErrorBoundary;需要设计重试边界 |
| 条件查询 | 支持;adapter 可用 convexQuery(fn, "skip") | 不支持 enabled、placeholderData 或 skipToken |
| key 改变时保留旧内容 | 可以用 placeholderData 或显式局部 UI | 没有 placeholderData;若新 key 冷缓存,通常会再次触发 fallback |
| 同组件多个独立 query | 多个 Hook 可在同一次 render 中并行启动 | 连续多个 Hook 容易串行;改用 useSuspenseQueries、预取或拆分 boundary |
| TanStack Start 初始 SSR | 官方组合中不会因它主动拉取 Convex 数据 | 会在初始 SSR pass 发起获取,可直接把数据渲染进 HTML |
| 实时性、一致性 | 相同 | 相同 |
useSuspenseQuery 的 data 能省掉 undefined 分支,并不是因为服务器永远成功:当数据还没到时,组件的本次 render 不会继续执行,而是由 <Suspense> 显示 fallback;有初始 error 时,则由 ErrorBoundary 接手。TanStack 文档明确列出它不支持 enabled、placeholderData、throwOnError 选项,且 cancellation 不工作。useSuspenseQuery reference Suspense guide
5. 优先 useQuery 的场景#
5.1 参数、身份或显示条件尚未就绪#
这是最常见也最明确的选择。比如 URL 或父级状态尚未给出 projectId,用户没有选择 tab,认证仍在解析,或者搜索词太短。不要条件调用 Hook;始终调用 useQuery,只把 args 切换为 adapter 支持的 "skip"。
import { useQuery } from "@tanstack/react-query";
import { convexQuery } from "@convex-dev/react-query";
import type { Id } from "../convex/_generated/dataModel";
import { api } from "../convex/_generated/api";
type Props = {
projectId?: Id<"projects">;
};
export function ProjectPanel({ projectId }: Props) {
const projectQuery = useQuery(
convexQuery(
api.projects.get,
projectId ? { projectId } : "skip",
),
);
if (!projectId) return <p>请选择一个项目。</p>;
if (projectQuery.isError) {
return <p>加载失败:{projectQuery.error.message}</p>;
}
// 对已启用的 Convex query,undefined 表示结果尚未可用。
if (projectQuery.data === undefined) return <ProjectSkeleton />;
// 此分支假设 api.projects.get 用 db.get(),因而用 null 表示记录不存在。
if (projectQuery.data === null) return <p>项目不存在或无权访问。</p>;
return <ProjectView project={projectQuery.data} />;
}这里应特别区分 undefined 和业务值。对原生 Convex useQuery,undefined 也是首次加载与 "skip" 的返回值;因此 Convex query 的领域层最好用 null 或显式状态对象表达“未找到”,不要把 undefined 作为正常业务结果。Convex React 的 loading/skip 语义
"skip" 时不会创建 subscription,也不会发请求。若它没有 cache,TanStack result 可能仍是 pending,但并不代表正在网络加载;UI 应先依据 projectId 或自身的条件显示“请选择/尚未准备好”,而不是只看 isPending 就转圈。
5.2 页面要渐进显示,而不是整体挡住#
例如项目页的导航、标题、编辑表单已经能出现,但“活动统计”“推荐成员”“审计日志”只是增强信息。让每张卡使用常规 useQuery,可各自展示局部 skeleton 或错误,而不会因为一个慢 query 把整个页面换成 fallback。
这也是搜索、可选抽屉、tooltip、按需 tab 的合理默认选择。它们常常有高频 key 变化或不一定会被打开,优先控制是否订阅、何时展示 loading、何时保留旧结果。对昂贵的 query,再根据实际导航频率把 gcTime 调低,而不是试图用 refetchInterval 控制 Convex 新鲜度。
5.3 切换 key 时,旧内容短暂保留比 fallback 更自然#
分页、非敏感筛选和列表排序经常希望保留上一页内容并显示“正在更新”。useQuery 能使用 placeholderData 或自己的局部 loading 方案;useSuspenseQuery 没有 placeholderData。
但“保留旧数据”是产品决策,不是性能默认项:在项目、租户、账户、权限范围切换时,展示旧主体可能误导甚至泄露信息。此类路径应清空、使用明确 fallback 或为 boundary 设置与身份范围一致的 key。
5.4 同一组件有多个相互独立的读取#
当 teamId 已知时,团队详情和成员列表可以并行启动:
const team = useQuery(convexQuery(api.teams.get, { teamId }));
const members = useQuery(convexQuery(api.members.list, { teamId }));组件可以先显示 team,成员区继续 loading。若用户体验允许分块出现,这个写法直接且高效。
6. 优先 useSuspenseQuery 的场景#
6.1 数据是该组件成立的前提#
如果一个组件没有项目详情就无法解释自己在展示什么,或一个对话正文没有消息就不能正确渲染,把它放在明确的 Suspense boundary 内会更贴合 UI 语义。父组件先处理可选参数,子组件只接收已确定的参数:
import { Suspense } from "react";
import {
QueryErrorResetBoundary,
useSuspenseQuery,
} from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";
import { convexQuery } from "@convex-dev/react-query";
import type { Id } from "../convex/_generated/dataModel";
import { api } from "../convex/_generated/api";
function RequiredProject({ projectId }: { projectId: Id<"projects"> }) {
const { data: project, error, isFetching } = useSuspenseQuery(
convexQuery(api.projects.get, { projectId }),
);
// 可选策略:把“已有旧数据后的最终错误”也交给 ErrorBoundary。
if (error && !isFetching) throw error;
if (project === null) return <p>项目不存在或无权访问。</p>;
return <ProjectView project={project} />;
}
export function ProjectScreen({
projectId,
}: {
projectId?: Id<"projects">;
}) {
if (!projectId) return <p>请选择一个项目。</p>;
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<button onClick={resetErrorBoundary}>
重试:{error.message}
</button>
)}
>
<Suspense fallback={<ProjectSkeleton />}>
<RequiredProject projectId={projectId} />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}上例的 ErrorBoundary 来自额外依赖 react-error-boundary,可先安装:
npm install react-error-boundary最重要的不是这个包,而是边界归属:RequiredProject 内的参数永远存在,loading 由 <Suspense> 承担,错误有可点击的重试路径。不要写 enabled: !!projectId 来绕过这个设计;useSuspenseQuery 不支持 enabled。参数尚未确定时,要么保留在外层,要么改用常规 useQuery。
6.2 有完整的 TanStack Start + Convex SSR 方案#
这是 useSuspenseQuery 最有说服力的场景。Convex 官方明确说明:在 TanStack Start 的初始 SSR pass 中,useSuspenseQuery(convexQuery(...)) 会发起 Convex 数据获取,而普通 useQuery 不会。浏览器接管后,Convex client 会从 SSR 的结果继续实时 subscription,避免首屏 loading flash 和重复取数;同页 query 还能使用同一逻辑时间戳,降低服务端多次读取出现不一致视图的风险。Convex + TanStack Start
页面主体的必要数据可以在组件内 co-locate,也可以在路由 loader 中提前阻塞:
// 这是 TanStack Start route 的结构示意。
// 请替换 api.messages.list 为自己的 public query。
import { createFileRoute } from "@tanstack/react-router";
import { convexQuery } from "@convex-dev/react-query";
import { api } from "../convex/_generated/api";
export const Route = createFileRoute("/messages")({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(
convexQuery(api.messages.list, {}),
);
},
component: MessagesRoute,
});ensureQueryData 适合“没有数据就不能渲染该路由主体”。如果数据只是尽早拿到更好,但页面可先显示,loader 应调用但不 await prefetchQuery(...),组件再使用普通 useQuery 处理局部 loading。Convex 的 loader 示例
6.3 已经设计好一组一起出现的 loading sequence#
React 的 Suspense boundary 是产品 UI 的分段工具:它决定“哪些内容一起等待、哪些内容可以渐进出现”。不要给每个很小的组件机械套一层 boundary;应按设计稿中希望用户一起看到的区域划分。React 也说明,当已显示的内容因 key 改变再次 suspend 时,若更新不在 startTransition 或 useDeferredValue 中,fallback 会重新替换它。React <Suspense>
例如切换非敏感项目时,若希望保留已显示的内容直到下一个项目可展示,可将改 key 的 state update 放进 transition:
import { useTransition } from "react";
const [isPending, startTransition] = useTransition();
function selectProject(nextProjectId: Id<"projects">) {
startTransition(() => {
setProjectId(nextProjectId);
});
}这只是在改善 UI 连续性,不会改变 Convex 的权限或一致性语义;涉及账户/租户切换时,仍应优先保证不会继续展示错误范围的数据。
7. Suspense 最容易踩的两个坑#
7.1 多个独立 query 写成连续的 useSuspenseQuery#
冷 cache 下,第一处 useSuspenseQuery 会 suspend 当前 render,后面的 Hook 来不及执行;等第一个完成、React 重试 render 后,第二个才开始。这会形成请求 waterfall:
// 冷 cache 时可能串行,不推荐。
const team = useSuspenseQuery(convexQuery(api.teams.get, { teamId }));
const members = useSuspenseQuery(convexQuery(api.members.list, { teamId }));如果两者相互独立且参数都准备好了,用 useSuspenseQueries 一起启动:
import { useSuspenseQueries } from "@tanstack/react-query";
const [{ data: team }, { data: members }] = useSuspenseQueries({
queries: [
convexQuery(api.teams.get, { teamId }),
convexQuery(api.members.list, { teamId }),
],
});另一种办法是在 route loader、hover 或导航事件预取,或拆成具有各自 boundary 的独立组件。TanStack 官方也提醒:useSuspenseQueries 同样不支持每项 enabled / placeholderData,并且 cancellation 不工作。Parallel Queries useSuspenseQueries reference
若第二个 query 的参数必须等待第一个结果才得到,瀑布是业务数据依赖,不是 Hook 的 bug。优先考虑把前端真正需要的组合数据放进一个 Convex query,或在已知参数时提前预取;仅换成 Suspense 不会消除这个因果顺序。
7.2 把 useSuspenseQuery 当成可取消的条件请求#
useSuspenseQuery 没有 cancellation 支持;而 Convex adapter 的实时 lifecycle 还会受 gcTime 控制。不要依据“我希望中止请求”来选它或普通 useQuery。
对于 Convex reactive query,更可靠的控制顺序是:
- 不满足条件时用
convexQuery(fn, "skip"),根本不创建 subscription; - 参数稳定后再开启普通
useQuery,或挂载接受确定参数的 Suspense 子组件; - 根据页面来回切换频率与 query 成本设置
gcTime; - 对 typeahead 之类高频输入,先 debounce / defer 输入值,再决定是否创建 subscription。
这比依赖 cancelQueries() 更贴近 Convex 的推送模型。
8. SSR 不是一个开关:按框架分开判断#
| 运行方式 | 推荐判断 |
|---|---|
| 纯 CSR | 两个 Hook 没有 SSR 差异;按 loading boundary、参数确定性和 UI 连续性选择。没有强理由时,useQuery 是更保守的默认。 |
| TanStack Start + adapter | 对要进入初始 HTML 的必需数据,优先 useSuspenseQuery 或 loader ensureQueryData;可选内容用不 await 的预取 + useQuery。这是 Convex 官方明确说明的组合。 |
| 通用 TanStack Query SSR/hydration | 不要因为使用了 useSuspenseQuery 就假定服务器会安全预取和 hydrate。TanStack 文档建议:只有完整预取/dehydrate/hydrate 或受支持 streaming integration 已验证时才采用,否则可能重复取数或出现 hydration 不一致。 |
| Next.js App Router(未特意采用 TanStack Start 流程) | Convex 官方提供另一条原生路径:Server Component 用 preloadQuery,Client Component 用 usePreloadedQuery,初屏 SSR 后继续实时更新。不要把 TanStack Start 的结论直接套到所有 Next.js 页面。 |
Next.js 的原生预加载路径目前也是 beta,且服务端认证需要显式传递 token;是否选它取决于项目是否真的需要 TanStack Query 作为这条读模型的 owner。Convex Next.js Server Rendering
9. 一张场景决策表#
| 场景 | 首选 | 原因与替代方案 |
|---|---|---|
| 简单纯 React 客户端,只有 Convex 数据 | 原生 convex/react 的 useQuery | Convex 已提供订阅与一致更新;无需为了“缓存”额外叠 adapter。 |
| 已全面采用 TanStack Query,需要统一 Devtools/Router/外部 API | useQuery(convexQuery(...)) 起步 | 先保持局部 loading/error,逐块引入 Suspense。 |
| route 参数、认证、tab、搜索词未就绪 | useQuery + "skip" | 不创建无效 subscription;Suspense 子组件只应接收确定参数。 |
| 项目详情、会话正文等“没有数据就无页面主体” | useSuspenseQuery | 给这块主体配置相称的 <Suspense> + ErrorBoundary。 |
| dashboard 有多个可独立展示的卡片 | 多个 useQuery | 可并行、可局部失败、可渐进显示。 |
| 一组独立数据必须整块出现 | useSuspenseQueries 或 loader 预取 | 避免连续 useSuspenseQuery 造成 waterfall。 |
| 分页/筛选切换,希望旧数据暂留 | useQuery | 使用 placeholderData 或自定义过渡 UI;先评估旧数据是否会误导。 |
| TanStack Start 关键首屏 SSR | useSuspenseQuery / ensureQueryData | 官方 adapter 能在 SSR 获取并在浏览器接续 subscription。 |
| 登录态正在初始化 | 外层 auth gate 后再挂载,或 useQuery + "skip" | 不要让必须认证的 Suspense query 先以错误身份运行。 |
| Convex cursor pagination | 视能力使用原生 usePaginatedQuery | adapter 不是原生 React client 全部 API 的替代;可以在同一 Convex client 下并用。 |
10. 发布前如何验证自己的选择#
不必先争论“哪个 Hook 更现代”,可以按下面的用例验证实际行为:
- 冷启动: 清除浏览器 cache/重新打开页面,确认
useQuery只替换预期的局部区域,useSuspenseQuery只触发设计好的 fallback。 - 条件: 在
projectId、登录态或 tab 未准备好时,确认 adapter 没有创建 subscription;条件满足后再开始读取。 - 实时更新: 在另一标签页执行 Convex mutation,确认当前 query 自动更新;不要为了这个测试再加入
invalidateQueries()。 - 切换 key: 快速换项目、筛选或搜索词,确认是否出现不希望的 fallback、旧数据误导或无意义的离屏订阅;据此调整
startTransition、placeholderData、"skip"与gcTime。 - 并行: 在网络 throttling 下观察多个关键 query 是否串行;必要时改
useSuspenseQueries、route loader 或后端聚合 query。 - SSR: 若用 TanStack Start,分别检查初始 HTML、hydration 后订阅以及认证路径;若是其他 SSR 框架,先验证预取/hydration 链路,再启用 Suspense。
结语#
最终的选择并不复杂:先问组件在没有数据时能否诚实而有用地存在,再问参数是否已经确定,最后问边界和 SSR 是否真的被设计与验证过。
useQuery 让组件自己表达 loading、error、条件与渐进显示;useSuspenseQuery 把“没有数据就不能显示”提升为 React 树的边界协议。Convex 为两者提供同一套实时推送与一致视图,TanStack Query 则提供 lifecycle、预取、SSR/hydration 和 UI 协调能力。理解这个职责划分后,Hook 的选择会从风格偏好变成可解释的产品和架构决策。
相关阅读与一手资料#
- 站内基础阅读:TanStack Query v5 基础:从 Server State 到 Query、Mutation 与缓存。历史 v4+ 参考见:全面掌握 TanStack Query:现代 React 应用的数据管理利器。
- 站内源码背景:Convex 源码导读:开源仓库版图、核心架构与阅读路线
- Convex with TanStack Query · Convex + TanStack Start · Convex React
- TanStack
useSuspenseQuery· TanStack Suspense guide · Request Waterfalls - React
<Suspense>·@convex-dev/react-querysource