Solid Query 与 Solid Suspense 集成指南:开箱即用的数据挂起与 Render-as-you-fetch 预取实践
2026/9/9 23:52:48 网站建设 项目流程

Solid Query 与 Solid Suspense 集成指南:开箱即用的数据挂起与 Render-as-you-fetch 预取实践

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

Solid Query(@tanstack/solid-query)是 TanStack Query 面向 SolidJS 框架的适配层。本指南围绕 docs/framework/solid/guides/suspense.md 讲解如何将 SolidJS 原生的<Suspense>边界与 Solid Query 的查询结合起来:无需任何额外配置,只要在 Suspense 边界内读取查询的data,组件就会在数据就绪前自动挂起并展示 fallback;在此基础上,进一步讨论 Fetch-on-render 与 Render-as-you-fetch 两种数据加载模型,并借助预取把加载时机前移到路由回调与用户交互事件中。读完本文,你将掌握 Solid Query 挂起模式的开箱用法、挂起与错误边界配合的规则,以及从"渲染时才取数"升级到"先取数再渲染"的实战路径。

为什么在 Solid Query 中使用 Suspense

在 SolidJS 中,Suspense是一个用于协调异步资源(resource)的组件边界。当一个位于<Suspense>内的组件在渲染期间读取了尚未就绪的异步资源时,该边界不会输出组件内容,而是渲染fallback;待资源就绪后,Solid 会自动"恢复"出真实内容。这意味着开发者可以把"数据加载中"这一状态完全交给框架处理,组件代码中不再需要手写if (isPending) return ...这类分支。

Solid Query 的查询结果天然就是这种异步资源的形态,因此它与 Solid 的 Suspense 的"Solid Query 与 React Query 的重要差异"一节中,官方直接声明:只要你在<Suspense>边界内访问查询数据,Suspense 对查询就是开箱即用的。这一点与 React Query 中需要显式开启suspense: true的使用方式有本质区别,是迁移或新接入时需要最先建立的认知。

基础用法:用 Suspense 包裹可挂起组件

要让 Solid Query 的查询挂起生效,最核心的一步是把"读取了查询数据"的可挂起组件用 Solid 提供的Suspense组件包裹起来,并为它提供一个fallback。当查询仍在加载时,用户会看到 fallback;一旦data就绪,组件树便会自动渲染。

import { Suspense } from 'solid-js' <Suspense fallback={<LoadingSpinner />}> <SuspendableComponent /> </Suspense>

fallback可以是任意 UI(一个加载动画组件、一段文本甚至null)。实际项目中常把 fallback 设计为LoadingSpinner之类的骨架屏组件,以避免加载期间出现布局跳动。

接下来定义可挂起的组件本身。它与普通useQuery用法一致:查询键、查询函数、以及……直接读取数据。无需设置任何"开启 suspense"之类的选项。

import { useQuery } from '@tanstack/solid-query' const todoFetcher = async () => await fetch('https://jsonplaceholder.cypress.io/todos').then((response) => response.json(), ) function SuspendableComponent() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: todoFetcher, })) // 在 <Suspense> 边界内直接访问 todosQuery.data, // 会自动触发挂起,直到数据就绪 return <div>Data: {JSON.stringify(todosQuery.data)}</div> }

注意上述代码的两个细节:

  1. useQuery的入参是一个函数() => ({ ... }))。这是 Solid Query 与 React Query 的 API 差异:Solid 采用细粒度响应式,参数函数允许查询键等字段在响应式作用域内被追踪。这一点在 quick-start.md 中也有专门对比。
  2. 是否触发挂起取决于data被读取的位置。下面这段来自 quick-start 的对比最能说明问题:
import { For, Suspense } from 'solid-js' function Example() { const query = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, })) return ( <div> {/* ✅ 在 Suspense 边界内读取 data,会触发 loading fallback */} <Suspense fallback={'Loading...'}> <For each={query.data}>{(todo) => <div>{todo.title}</div>}</For> </Suspense> {/* ❌ 在 Suspense 边界外读取 data,不会触发 loading fallback */} <For each={query.data}>{(todo) => <div>{todo.title}</div>}</For> </div> ) }

挂起的底层原理:query.data 即 Solid 资源

"开箱即用"的背后是 Solid Query 的实现设计。从源码 packages/solid-query/src/useBaseQuery.ts 可以清晰看到其挂起机制:

  • useBaseQuery在内部通过 Solid 的createResource把查询观测器(QueryObserver)的结果包装成了一个异步资源(见 useBaseQuery.ts#L237-L270),资源的读取器会在 Promise 中订阅 observer,并在observerResult.isLoading时保持 pending。
  • 最终返回的是一个Proxy(state, handler)(见 useBaseQuery.ts#L371-L386)。当访问data属性时,handler 会读取queryResource
const handler = { get(target, prop) { if (prop === 'data') { if (state.data !== undefined) { return queryResource.latest?.data } return queryResource()?.data } return Reflect.get(target, prop) }, }

也就是说,data就是读一个 Solid 资源:资源处于 pending 时读取会被 Solid 捕获并让最近的<Suspense>边界挂起;这也解释了为什么必须把读取动作放进 Suspense 边界内才有效。

基于这一设计,v5 起的 Solid Query 已经将suspense选项标记为deprecated。在 packages/solid-query/src/types.ts#L38-L44 的类型注释中说明:useQuery/useInfiniteQuerydata本身就是一个 SolidJS resource,数据加载时会自动挂起,因此设置suspense: false也会被当作 no-op。换句话说:只要你在边界内读取数据,挂起行为就始终存在,无须(也无法通过旧选项)关闭。与 React Query 相比,这是两套框架 hook 语义差异的直接体现——React Query 需要把"读取挂起"建模成显式抛出的 Promise,而 Solid 则靠资源读取自然完成。

Suspense 下的错误处理:ErrorBoundary 与 throwOnError

当查询失败时,挂起的资源会把错误向上抛出。因此 Suspense 模式下的错误处理通常由 Solid 的ErrorBoundary承接,而非在组件里判断isError。推荐的嵌套结构是:外层ErrorBoundary负责错误、内层Suspense负责加载中状态:

import { ErrorBoundary, Suspense } from 'solid-js' <ErrorBoundary fallback={(_err, resetSolid) => ( <div> <p>数据加载失败</p> <button onClick={() => resetSolid()}>重试</button> </div> )}> <Suspense fallback="loading..."> <SuspendableComponent /> </Suspense> </ErrorBoundary>

错误是否需要抛出到边界,由throwOnError选项控制。根据 docs/framework/solid/reference/useQuery.md 的文档说明,其取值语义如下:

  • 默认值为false,即错误作为查询状态(isError/error)返回,不抛给边界;
  • 若设置了(已废弃的)suspense: true,默认会变为true
  • 在 SSR 期间,默认强制为true(与服务端渲染需要reject资源的实现一致,见 useBaseQuery.ts#L133-L136 中if (isServer) { ...; defaultOptions.throwOnError = true });
  • 也可以是(error, query) => boolean函数,按错误内容精细决定哪些错误抛给边界。

对应的测试用例见 packages/solid-query/src/tests/suspense.test.tsx,其中系统验证了:

  • 默认(suspense选项置位时)错误抛给ErrorBoundary(第 440-487 行);
  • throwOnError: false时不抛错,正常渲染(第 489-530 行);
  • throwOnError传入函数时,返回值决定是否抛出(第 532-623 行);
  • 配合resetSolid()重置错误边界后能够重新发起请求(第 272-335 行)。

可见 Suspense + ErrorBoundary 的组合并非简单把错误"丢出去",而是与查询重试、边界重置形成了闭环的容错机制。

Fetch-on-render 与 Render-as-you-fetch

默认情况下,Solid Query 的 Suspense 模式表现得像一个典型的Fetch-on-render(渲染时取数)方案,且无需任何额外配置。其含义是:当组件尝试挂载时,它们会触发查询获取并挂起——但前提是这些组件已经被导入并挂载。也就是说,取数的起点仍然是"组件开始渲染"这一时刻,加载动画之前的网络耗时并没有被隐藏。

如果希望进一步降低感知延迟,可以把模型升级为Render-as-you-fetch(边取数边渲染),即在真正渲染之前就让查询先跑起来。官方在 suspense.md 中给出的建议是:在路由回调(routing callbacks)和/或用户交互事件里实现 Prefetching,让查询在对应组件被挂载之前、甚至在开始导入或挂载它们的父组件之前就启动。

Solid Query 的预取实践详见同目录下的 docs/framework/solid/guides/prefetching.md,那里给出了组件生命周期内的三种预取姿势:

  1. 使用useQuery并忽略返回值——把查询当作副作用启动,适合与父查询数据强关联、几乎必然会被消费的子查询:
function Article(props) { const articleQuery = useQuery(() => ({ queryKey: ['article', props.id], queryFn: getArticleById, })) // 预取评论:忽略结果,只为了让查询提前启动 useQuery(() => ({ queryKey: ['article-comments', props.id], queryFn: getArticleCommentsById, // 可选优化:避免该查询变化引发多余重渲染 notifyOnChangeProps: [], })) // ...渲染 articleQuery.data }
  1. 在 queryFn 内部预取——适合"一旦取到文章几乎必然还要评论"的场景,借助queryClient.query发起内层请求:
const articleQuery = useQuery(() => ({ queryKey: ['article', id], queryFn: (...args) => { void queryClient .query({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) .catch(noop) return getArticleById(...args) }, }))
  1. 在 effect 中预取——同样基于queryClient.query,但把启动时机放到createEffect中,按需执行:
import { createEffect } from 'solid-js' const queryClient = useQueryClient() createEffect(() => { void queryClient .query({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) .catch(noop) })

在路由层面做预取是更彻底的方案:为每个路由显式声明其组件树所需的数据,在路由加载器中启动请求。对于关键数据可以await阻塞路由渲染(配合路由的错误处理),对次要数据则用.catch(noop)先发起但不等待。完整的路由集成代码示例可继续阅读 prefetching.md 的 Router Integration 一节。

挂起行为的边界场景与注意事项

结合 useQuery.test.tsx 与 suspense.test.tsx 等测试,可以把挂起行为的关键边界总结如下,便于在真实应用中避免踩坑:

  • Suspense 模式下一个queryKey只会触发一次 queryFn:测试"should not call the queryFn twice when used in Suspense mode"(suspense.test.tsx#L143-L168)验证了挂起等待与资源读取不会导致重复请求。
  • 切换查询键会重新挂起:当组件通过响应式信号切换queryKey时,旧数据对应查询被新查询取代,Suspense 边界会再次进入 loading,直到新数据就绪(第 395-438 行)。
  • 重新挂载会重新取数:将gcTimestaleTime配置为合适值后,隐藏再显示组件会依据新鲜度重新发起请求,期间isFetching为 true(第 337-393 行)。
  • enabled: false的查询不会触发请求:即使位于 Suspense 边界内,被禁用的查询也不会调用 queryFn,而data保持未定义,可结合Show/Switch等渲染真实内容(第 625-663 行)。这与 Solid 资源"pending 且不可用"的语义一致。
  • 不要在组件卸载后无限挂起:源码中为"observer 卸载早于资源加载完成"的场景实现了队列化退订与 resolver 兜底逻辑(见 useBaseQuery.ts#L121-L125、useBaseQuery.ts#L344-L357),注释明确这是为了修复 Suspense 边界被无限期挂起的问题;测试should remove query instance when component unmounted(第 170-211 行)验证了卸载后 observer 数归零。
  • 无限查询(useInfiniteQuery)同样支持挂起:在 Suspense 下切换分页也会触发 loading fallback(第 85-141 行),后续可参考 infinite-queries.md 了解更多。

补充:SSR 与流式渲染视角下的 Suspense

如果你在服务端渲染场景中使用 Suspense,需要注意 Solid Query 针对isServer的默认策略(见 useBaseQuery.ts#L127-L138):服务端会将retry置为falsethrowOnError置为true,避免在服务器上无限重试并确保错误能通过资源 reject 传递。此外,类型定义中还有一个与流式渲染相关的选项deferStream:默认false,置为true后,服务端会等待查询解析完成再冲刷流,从而避免把 loading 状态先发给客户端(见 packages/solid-query/src/types.ts#L31-L38)。这与资源加载、onHydrated时回填 Query Cache 的流程共同支撑了服务端 + Suspense 的完整链路,更深入的讨论可参考 docs/framework/solid/guides/ssr.md。

小结

Solid Query 把查询结果建模为 Solid 资源,使<Suspense>集成成为默认能力:在边界内读取data即自动挂起,失败则交给ErrorBoundary处理。默认的 Fetch-on-render 模型零配置即可用;若要获得更流畅的体验,应结合 Prefetching 在路由回调或交互事件中提前发起请求,过渡到 Render-as-you-fetch 模型。无论选择哪种方式,queryKey切换、enabled禁用、错误边界重置、卸载清理等边界行为都已由 suspense.test.tsx 等测试覆盖,可作为你实现与排错的权威参考。

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询