Convex + TanStack Query:useQuery 与 useSuspenseQuery 的选择、原理与场景

8月 12, 2026
Convex, React, Frontend, TypeScript, ByAI

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-query adapter 的组合。文中的 useQueryuseSuspenseQuery 默认都从 @tanstack/react-query 导入;它们不是 convex/react 的同名原生 Hook。示例假设项目已经有可调用的 public Convex query,并以 api.projects.get 为占位名称;替换为自己的 generated API、参数与权限模型后再运行。

示例运行说明:文中的 ProjectViewProjectSkeletonMessagesRoute 等是应用自己的 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 的组合里,useQueryuseSuspenseQuery 不是两种实时性等级,也不是“一个缓存、一个不缓存”。对于同一个 convexQuery(api.foo.bar, args),它们使用同一套 Convex subscription:数据库相关数据改变后,Convex 会把新结果推到客户端,adapter 再更新 TanStack Query cache。二者的核心差异是:数据暂时不可用时,组件继续渲染并自行处理状态,还是让 React Suspense 暂停该子树并由 boundary 接管。

可以先用下面四个问题判断:

  1. 没有最终数据,这块 UI 还能否正确、有价值地显示? 能,例如筛选器、页面框架、侧栏或可选卡片:优先 useQuery。不能,例如详情主体、会话正文:可考虑 useSuspenseQuery
  2. 参数和身份是否已经确定? projectId、搜索词、认证 token、tab 是否打开等任一项还未就绪:优先 useQuery,用 "skip" 条件订阅;或在外层先 gate,等确定后再挂载 Suspense 子组件。
  3. 是否有恰当的 <Suspense><ErrorBoundary> 没有明确的加载/错误边界设计时,不要仅为了 data 类型不再带 undefined 而改用 useSuspenseQuery
  4. 是否在 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-queryConvexQueryClientconvexQuery(...);不要把它当成 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 --> B

ConvexQueryClient 监听 TanStack QueryClient 的 query 生命周期,为 Convex query 建立 WebSocket subscription,并把服务器推送的结果写回 cache。convexQuery() 负责构造兼容的 query key 和 options;它会把 staleTime 设为 Infinityadapter 介绍 ConvexQueryClient API

这带来几个与普通 fetch query 不同的后果:

  • 对 reactive convexQuery,结果不是靠轮询“过期后再取”;相关数据变更时服务器主动推送,因此 isStale 始终为 false
  • 对这类 query,retryrefetchOnWindowFocusrefetchInterval、写后 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 同时交给 ConvexProviderConvexQueryClient。这样,必要时仍可使用原生 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. useQueryuseSuspenseQuery:同一数据源,不同渲染契约#

维度useQuery(convexQuery(...))useSuspenseQuery(convexQuery(...))
冷启动、没有数据正常返回 result object;组件自行看 dataisPendingisErrorrender 阶段 suspend,由最近 <Suspense fallback> 接管
data 类型通常是 TData | undefinedTData,保证已定义
Loading 的归属组件内部可做局部 skeleton、禁用按钮、渐进显示boundary 统一决定整块区域的 fallback
Error 的归属可就地显示 error,或通过 throwOnError 交给边界初始无数据的 error 会到 ErrorBoundary;需要设计重试边界
条件查询支持;adapter 可用 convexQuery(fn, "skip")不支持 enabledplaceholderDataskipToken
key 改变时保留旧内容可以用 placeholderData 或显式局部 UI没有 placeholderData;若新 key 冷缓存,通常会再次触发 fallback
同组件多个独立 query多个 Hook 可在同一次 render 中并行启动连续多个 Hook 容易串行;改用 useSuspenseQueries、预取或拆分 boundary
TanStack Start 初始 SSR官方组合中不会因它主动拉取 Convex 数据会在初始 SSR pass 发起获取,可直接把数据渲染进 HTML
实时性、一致性相同相同

useSuspenseQuerydata 能省掉 undefined 分支,并不是因为服务器永远成功:当数据还没到时,组件的本次 render 不会继续执行,而是由 <Suspense> 显示 fallback;有初始 error 时,则由 ErrorBoundary 接手。TanStack 文档明确列出它不支持 enabledplaceholderDatathrowOnError 选项,且 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 useQueryundefined 也是首次加载与 "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 时,若更新不在 startTransitionuseDeferredValue 中,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,更可靠的控制顺序是:

  1. 不满足条件时用 convexQuery(fn, "skip"),根本不创建 subscription;
  2. 参数稳定后再开启普通 useQuery,或挂载接受确定参数的 Suspense 子组件;
  3. 根据页面来回切换频率与 query 成本设置 gcTime
  4. 对 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/reactuseQueryConvex 已提供订阅与一致更新;无需为了“缓存”额外叠 adapter。
已全面采用 TanStack Query,需要统一 Devtools/Router/外部 APIuseQuery(convexQuery(...)) 起步先保持局部 loading/error,逐块引入 Suspense。
route 参数、认证、tab、搜索词未就绪useQuery + "skip"不创建无效 subscription;Suspense 子组件只应接收确定参数。
项目详情、会话正文等“没有数据就无页面主体”useSuspenseQuery给这块主体配置相称的 <Suspense> + ErrorBoundary。
dashboard 有多个可独立展示的卡片多个 useQuery可并行、可局部失败、可渐进显示。
一组独立数据必须整块出现useSuspenseQueries 或 loader 预取避免连续 useSuspenseQuery 造成 waterfall。
分页/筛选切换,希望旧数据暂留useQuery使用 placeholderData 或自定义过渡 UI;先评估旧数据是否会误导。
TanStack Start 关键首屏 SSRuseSuspenseQuery / ensureQueryData官方 adapter 能在 SSR 获取并在浏览器接续 subscription。
登录态正在初始化外层 auth gate 后再挂载,或 useQuery + "skip"不要让必须认证的 Suspense query 先以错误身份运行。
Convex cursor pagination视能力使用原生 usePaginatedQueryadapter 不是原生 React client 全部 API 的替代;可以在同一 Convex client 下并用。

10. 发布前如何验证自己的选择#

不必先争论“哪个 Hook 更现代”,可以按下面的用例验证实际行为:

  1. 冷启动: 清除浏览器 cache/重新打开页面,确认 useQuery 只替换预期的局部区域,useSuspenseQuery 只触发设计好的 fallback。
  2. 条件:projectId、登录态或 tab 未准备好时,确认 adapter 没有创建 subscription;条件满足后再开始读取。
  3. 实时更新: 在另一标签页执行 Convex mutation,确认当前 query 自动更新;不要为了这个测试再加入 invalidateQueries()
  4. 切换 key: 快速换项目、筛选或搜索词,确认是否出现不希望的 fallback、旧数据误导或无意义的离屏订阅;据此调整 startTransitionplaceholderData"skip"gcTime
  5. 并行: 在网络 throttling 下观察多个关键 query 是否串行;必要时改 useSuspenseQueries、route loader 或后端聚合 query。
  6. SSR: 若用 TanStack Start,分别检查初始 HTML、hydration 后订阅以及认证路径;若是其他 SSR 框架,先验证预取/hydration 链路,再启用 Suspense。

结语#

最终的选择并不复杂:先问组件在没有数据时能否诚实而有用地存在,再问参数是否已经确定,最后问边界和 SSR 是否真的被设计与验证过。

useQuery 让组件自己表达 loading、error、条件与渐进显示;useSuspenseQuery 把“没有数据就不能显示”提升为 React 树的边界协议。Convex 为两者提供同一套实时推送与一致视图,TanStack Query 则提供 lifecycle、预取、SSR/hydration 和 UI 协调能力。理解这个职责划分后,Hook 的选择会从风格偏好变成可解释的产品和架构决策。

相关阅读与一手资料#

本文共 8194 字,上次修改于 Aug 12, 2026,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» TanStack Query v5 基础:从 Server State 到 Query、Mutation 与缓存

» Expo 技术原理与交付:从 React Native 项目到 EAS 发布

» Expo、React Native 与 Flutter:概念、架构、上架与选型

» React Native 技术原理:从 TypeScript 到原生界面、Fabric 与 Hermes

» Convex Components:概念、隔离模型与使用方式