Solid Query 查询函数(Query Functions)实战指南:Promise 契约、错误处理与 QueryFunctionContext
【免费下载链接】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
本篇指南围绕 TanStack Query 在 Solid 生态的官方实现@tanstack/solid-query(本仓库 packages/solid-query)中的Query Functions概念展开。查询函数是useQuery获取数据的核心引擎,理解"函数必须返回 Promise、要么 resolve 数据要么抛错"这一契约,并掌握QueryFunctionContext的用法,是正确写出可缓存、可取消、可重试查询的基础。读完本文你将能写出规范的查询函数、优雅地处理各类请求错误,并利用queryKey、signal等上下文信息实现可复用的查询逻辑。Solid 与 React 版本的此篇指南共享同一份原始素材(见 docs/framework/react/guides/query-functions.md),但本文将完全围绕 Solid Query 的函数式选项语法(options 以函数形式传入)与 SolidJS 响应式特性展开。
一、查询函数的基本规则:一个"Promise 契约"
在 Solid Query 中,query function(查询函数)可以是任何"返回 Promise 的函数",其返回值遵循两个硬性约定:
- 成功时 resolve 数据——Promise 成功解析出来的值会写入查询缓存,并成为
data; - 失败时抛错或 reject——函数抛出的任何异常都会被捕获,持久化到该查询的
error状态上。
此外,成功解析的值不能是undefined。解析为undefined的查询会被当作失败处理(历史上undefined是成功查询的非法缓存值)。如果你确实想缓存"空"这一成功结果,请显式 resolvenull。
从类型定义上看,这一契约被直接编码在核心层(packages/query-core/src/types.ts#L102-L106):
export type QueryFunction< T = unknown, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > = (context: QueryFunctionContext<TQueryKey, TPageParam>) => T | Promise<T>也就是说,查询函数既可以同步返回数据(T),也可以异步返回 Promise(Promise<T>),最终都会由底层以 Promise 语义统一处理。
二、Solid Query 的独特语法:options 以 Accessor 函数传入
在使用示例前,必须先强调 Solid Query 与 React Query 的一个关键差异:useQuery接收的不是一个普通对象,而是一个返回 options 对象的函数(Accessor)。这与 React 版useQuery({ queryKey, queryFn })的写法不同。
看 useQuery.ts 的实现即可理解原因:options 被包装进createMemo(() => options()),因此其中引用到的信号(signal,即 SolidJS 响应式状态)发生变化时,查询选项会自动重建并触发重取(refetch)。这是 SolidJS 细粒度响应式在查询层面的体现。
import { useQuery } from '@tanstack/solid-query' const todoId = 1 useQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }))以上四种写法都是合法且等价的配置:直接引用函数、闭包捕获参数、async/await包装、以及从queryKey上下文解构参数。
在真实项目中(参考 examples/solid/basic/src/index.tsx#L26-L34),典型用法是把queryKey作为依赖跟踪信号,让todoId变化时自动重新拉取:
import { useQuery } from '@tanstack/solid-query' import { createSignal } from 'solid-js' function createPost(postId: () => number) { return useQuery(() => ({ queryKey: ['post', postId()], queryFn: () => getPostById(postId()), enabled: !!postId(), })) }注意,若queryFn中通过闭包引用了外部参数,而queryKey没有同步纳入这些参数,底层将无法正确识别查询变化。关于 key 的设计原则可参考 Query Keys 指南,关于响应式入参的更多细节可结合 Queries 基础指南 阅读。
三、错误抛出与处理:throw 与 rejected Promise
要让 Solid Query 判断一个查询出错,查询函数必须主动抛出异常(throw)或返回一个 rejected 的 Promise。任何在查询函数内部被抛出的错误,都会进入查询的error状态,并可通过解构error读取:
const todosQuery = useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }))在 UI 层,@tanstack/solid-query返回的查询结果对象是响应式的 store,你可以直接基于status/isError/isPending等字段做渲染分支(见 examples/solid/basic/src/index.tsx#L43-L77 中的<Switch>/<Match>用法):
const state = createPosts() // ... <Switch> <Match when={state.status === 'pending'}>Loading...</Match> <Match when={state.status === 'error'}> <span>Error: {(state.error as Error).message}</span> </Match> <Match when={state.data !== undefined}> <For each={state.data}>{(post) => <p>{post.title}</p>}</For> </Match> </Switch>值得留意的是:错误必须由查询函数主动抛出。Solid Query 无法替你把"非 2xx 的 HTTP 响应"翻译成异常——这取决于你使用的请求工具本身,这正是下一节要解决的问题。
四、配合fetch使用:处理默认不抛错的客户端
axios、graphql-request等工具库通常会对不成功的 HTTP 调用自动抛错;但原生fetch默认不会对 4xx / 5xx 响应抛错,它只会在网络层面失败(如断网、DNS 解析失败)时 reject。因此当你使用fetch时必须自己手动抛出错误。一种常见的写法是检查response.ok:
useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }))这条规则很容易被忽略:如果漏掉response.ok检查,接口返回 404 时查询仍会被当作成功,数据层将拿到空内容而 UI 层毫无感知。把"HTTP 状态码错误"显式转换为抛错,才能让 Solid Query 的retry、error、错误边界等机制完整生效。请求重试策略详见 Query Retries 指南(若需引用可参见 solid 同名目录),此处不再展开。
五、Query Function Variables:QueryKey 如何"顺路"进入查询函数
查询键(queryKey)不仅用于唯一标识所获取的数据,还会作为QueryFunctionContext的一部分被自动传入查询函数。这意味着你可以把查询函数抽离成独立模块,而不必依赖闭包捕获外部变量:
function Todos(props) { const todosQuery = useQuery(() => ({ queryKey: ['todos', { status: props.status, page: props.page }], queryFn: fetchTodoList, })) } // 直接在查询函数里解构 key、status 和 page! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() }这一模式的工程意义在于"查询函数与组件解耦":
- 组件侧只需声明
queryKey,把它当作 UI 参数的"投影"; - 查询函数侧通过结构一致的
queryKey数组还原出参数,天然可单独测试、可复用; - 由于
queryKey同时参与缓存寻址,这样写还能保证参数与缓存键永不脱节。
配合 default-query-function.md 中介绍的默认查询函数(可以做到完全不传queryFn),上述解耦价值会被进一步放大。
六、QueryFunctionContext 详解
QueryFunctionContext是传给每个查询函数的唯一参数对象,也是"查询函数变量"机制的底层来源。其构成字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
queryKey | QueryKey | 本次查询的键,即定义查询时传入的queryKey,详见 Query Keys 指南 |
client | QueryClient | 当前使用的QueryClient实例,API 参考见 QueryClient |
signal | AbortSignal | 由 TanStack Query 提供的AbortSignal实例,可用于实现查询取消 |
meta | Record<string, unknown> \| undefined | 可选字段,可在其中填充关于该查询的附加信息 |
在核心层的类型定义(packages/query-core/src/types.ts#L144-L171)中,上述字段被明确建模为QueryFunctionContext<TQueryKey, TPageParam>:常规查询时pageParam与direction以可选形式存在;而在泛型参数TPageParam被具体化(即无限查询场景)时,则变为必选字段。
此外,Infinite Queries(无限查询)的查询函数还会额外收到两个字段:
pageParam: TPageParam—— 获取当前页所使用的页码参数;direction: 'forward' | 'backward'—— 当前页抓取的方向;- 已废弃(deprecated):如需感知抓取方向,官方建议在
getNextPageParam/getPreviousPageParam返回的pageParam中自行带上方向信息,而不要依赖此字段。
- 已废弃(deprecated):如需感知抓取方向,官方建议在
基于signal字段,可以实现真正的请求可取消性:例如把context.signal透传给原生fetch,组件卸载或缓存失效时请求即被中止(详见 Query Cancellation 指南)。client字段则让你在查询函数内也能调用client.getQueryData()等命令式 API 做数据联动。
七、源码级延伸:查询函数在 Solid Query 内部如何被驱动
理解上述 API 后,再看一眼@tanstack/solid-query的实现能帮你更准确地建立心智模型。
1. 选项作为 Accessor 求值。useQuery.ts 的实体会把options()结果包装为createMemo后交给useBaseQuery,这意味着 options 内的每个响应式依赖变化都会驱动 observer 更新选项并重取:
export function useQuery<TQueryFnData, TError, TData, TQueryKey extends QueryKey>( options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: Accessor<QueryClient>, ) { return useBaseQuery(createMemo(() => options()), QueryObserver, queryClient) }顺带一提,Solid 版 options 类型即Accessor<QueryOptions<...>>(见 packages/solid-query/src/types.ts#L60-L65),这正是"传函数而非对象"语法约束的类型来源。
2. Promise 被接入 SolidJS 的createResource。在 useBaseQuery.ts 中,查询函数产生的异步结果被包装进createResource,data因此具备 SolidJS 资源的响应式与 Suspense 语义(这也是 Solid 版没有独立suspense开关、data会自动触发挂起的原因)。查询结果最终以一个 Proxy store 的形式返回,读取state.data时实际走的是 resource 通道。
3. 服务端渲染下的默认差异。同文件 useBaseQuery.ts 表明,在服务端(isServer)会把默认retry强制置为false、throwOnError置为true——查询失败会直接抛错给 SSR 流,而不是无限重试拖垮首屏。此外客户端默认关闭structuralSharing(useBaseQuery.ts),改用 store 的reconcile机制(配合可选的structuredClone)做数据合并,这是为 SolidJS 细粒度更新做的适配。
如果只关心结果状态,而不关心底层驱动细节,直接使用state.status/state.isPending/state.isError/state.data等字段即可——返回对象是响应式的,模板中直接读取即可自动更新。
八、与其他概念的关系小结
查询函数并非孤立概念,它与以下主题紧密咬合,建议按需延伸阅读(均为本仓库 Solid 框架指南,路径统一从仓库根目录出发):
- Queries(查询基础)——
useQuery的完整返回值与status/fetchStatus状态机; - Query Keys(查询键)——
queryKey的结构化设计规范; - Query Cancellation(查询取消)——利用
QueryFunctionContext.signal实现请求中止; - Infinite Queries(无限查询)——
pageParam与方向字段的实战场景; - Default Query Function(默认查询函数)——当多个查询共享同一套取数逻辑时的收口方式;
- Query Retries(查询重试)——查询函数抛错后的重试语义。
掌握本文的 Promise 契约、主动抛错原则与QueryFunctionContext之后,你便拥有了写出健壮、可复用、可取消查询函数所需的全部核心知识。
【免费下载链接】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),仅供参考