TanStack Preact Query 实战指南:在 Preact 中构建强大的异步数据获取与缓存层
【免费下载链接】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/preact-query是 TanStack Query 为 Preact 生态提供的官方数据层方案,它把"获取、缓存与更新异步数据"这套能力以 Hooks 的形式带入 Preact 应用。本指南以本仓库 packages/preact-query/README.md 声明的 Quick Features 为骨架,结合包内源码(如 useBaseQuery.ts、useMutation.ts、suspense.ts)与 examples/preact/simple 可运行示例,逐项讲解:如何接入 Provider 与 QueryClient、如何使用查询/变更/无限查询三类核心 Hooks,以及自动缓存、stale-while-revalidate、并行与依赖查询、无限滚动、请求取消、Suspense 预取等特性背后的实现原理。读完本文,你将能直接在 Preact 项目中搭建一套完整的服务端状态管理方案。
一、认识 @tanstack/preact-query
1.1 它是什么
在 Preact 应用中,服务端状态(server state)与本地状态(client state)有着本质区别:服务端数据是异步到达的、可能随时过期、由远程拥有并可能被他人修改。@tanstack/preact-query提供了一系列 Hooks,用于在 Preact 中"获取、缓存和更新异步数据",把上述复杂性收敛到声明式的 Hook 调用中。包的描述与定位可以在 package.json 中确认:"Hooks for managing, caching and syncing asynchronous and remote data in preact",当前仓库版本为 5.102.8,其运行时唯一依赖是核心包@tanstack/query-core(workspace 引用),Peer 依赖为preact ^10.0.0。
1.2 安装与前置条件
从 package.json 可以看到其发布名为@tanstack/preact-query,属于 ESM 模块("type": "module"),并同时提供 modern 与 legacy 两种构建产物与完整类型声明:
npm install @tanstack/preact-query preact # 或使用 pnpm pnpm add @tanstack/preact-query preact前提条件:需要 Preact 10 及以上版本。包本身
sideEffects: false,可安全进行 tree-shaking;类型层面包内提供了基于 TS 56/57/58/59/6.0/7.0 多版本编译矩阵的类型测试(见 package.json 的test:types:*脚本),说明其类型定义在多 TS 版本下均被持续验证。
1.3 包的导出全景
从入口文件 src/index.ts 可以看到,该包对外暴露的 API 分为三大部分:
- 核心包整体再导出:
export * from '@tanstack/query-core',因此QueryClient、keepPreviousData、skipToken、dehydrate、hashQueryKey等基础设施全部可用; - 查询类 Hooks:
useQuery、useInfiniteQuery、useQueries,以及配套的queryOptions、infiniteQueryOptions类型化工具; - Suspense 与预取:
useSuspenseQuery、useSuspenseInfiniteQuery、useSuspenseQueries、usePrefetchQuery、usePrefetchInfiniteQuery; - 变更类 Hooks:
useMutation、useMutationState、useIsMutating与mutationOptions; - 基础设施:
QueryClientProvider/useQueryClient、HydrationBoundary、QueryErrorResetBoundary、useIsFetching、useIsRestoring/IsRestoringProvider。
下文将以 README 的 Quick Features 为主线逐项展开,并在每节给出对应的源码与测试依据。
二、30 秒快速上手
仓库中的 examples/preact/simple/src/index.tsx 提供了一个最小可运行示例,完整演示了从创建QueryClient、挂载QueryClientProvider到使用useQuery的完整链路:
import { render } from 'preact' import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/preact-query' const queryClient = new QueryClient() export function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) } const Example = () => { const { isPending, error, data, isFetching } = useQuery({ queryKey: ['repoData'], queryFn: async () => { const response = await fetch('https://api.github.com/repos/TanStack/query') return await response.json() }, }) if (isPending) return 'Loading...' if (error !== null) return 'An error has occurred: ' + error.message return ( <div> <h1>{data.full_name}</h1> <p>{data.description}</p> <strong>👀 {data.subscribers_count}</strong>{' '} <strong>✨ {data.stargazers_count}</strong>{' '} <strong>🍴 {data.forks_count}</strong> <div>{isFetching ? 'Updating...' : ''}</div> </div> ) } const app = document.getElementById('app') if (!app) throw new Error('Missing #app element') render(<App />, app)这段代码里已经蕴含了三个关键点:
QueryClient是全局数据中心,通常在整个应用生命周期内只创建一次;QueryClientProvider通过 Context 向子树注入 client——实现见 QueryClientProvider.tsx,它还会在挂载/卸载时调用client.mount()/client.unmount(),从而订阅窗口焦点、网络在线等全局事件(下文第四节详述);useQuery的第一个参数是"选项对象"。这是 v5 起唯一合法的调用形式,若传入非对象参数,开发环境下 useBaseQuery.ts 会直接抛错提示迁移到单一对象签名。
useQuery返回结果中的isPending、isError、isSuccess是status字段(pending/error/success)派生的布尔标记;isFetching则表示"即使已有数据展示,仍在后台重新获取"——这正是 stale-while-revalidate 的直观体现。
三、传输层无关的数据获取
README 列出的第一条特性是Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)——即数据获取与具体传输协议完全解耦。
这体现在类型定义上:queryFn被定义为QueryFunction,其返回值只需要是一个 Promise(Promise<TQueryFnData>),至于这个 Promise 内部是fetch、axios、GraphQL client 还是纯内存模拟,Query 层完全不关心。看 useQuery.ts 的泛型签名:
useQuery<TQueryFnData, TError, TData, TQueryKey>(options, queryClient?)TQueryFnData:queryFn的原始返回类型;TData:经过select派生后组件实际拿到的数据类型(默认为TQueryFnData);TError:错误类型,默认DefaultError。
正因为这一抽象,同一个useQuery既可以拉取 REST JSON,也可以执行 GraphQL 查询,甚至直接解析一个本地 Promise,业务组件无需关心底层客户端。此外,@tanstack/query-core被完整再导出,因此queryKey的序列化、queryFn的执行时机都由核心层统一调度,多框架(React/Solid/Svelte/Vue/Preact)共享同一套缓存语义。
四、自动缓存 + 智能重新获取
README 的第二条特性是Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)。这一条值得重点展开,因为它是 TanStack Query 体系的核心价值。
4.1 stale-while-revalidate:先展示旧数据,后台再更新
只要查询成功过一次,结果就会进入QueryCache。之后再次渲染同一个queryKey时:
- 如果数据未过期(在
staleTime之内),直接同步返回缓存,不发起请求; - 如果数据已过期(超过
staleTime),先立即返回缓存数据(保证 UI 不闪白屏),同时在后台触发重新获取,完成后用新数据驱动重新渲染——这就是isFetching与isPending的区别:isPending表示"没有任何数据可展示",而isFetching表示"有数据但在后台刷新"。
staleTime的默认值为 0,即数据一经获取便立即视为过期(stale),但仍会先展示再刷新;将其调大(如staleTime: 30_000)可显著减少重复请求。
4.2 Window Refocus:窗口聚焦自动重取
当窗口重新获得焦点时,Query 会自动重新获取"过期"的查询。这一机制由两处配合实现:
QueryClientProvider挂载时调用client.mount()(见 QueryClientProvider.tsx),订阅全局的 focus/online 事件;@tanstack/query-core中的focusManager/onlineManager负责事件监听与回调分发(其实现位于 packages/query-core/src/focusManager.ts 与 packages/query-core/src/onlineManager.ts)。
默认行为下,用户切走再切回页面时,所有 stale 查询会被批量重新验证;refetchOnWindowFocus可全局或按查询关闭此行为。
4.3 Polling / Realtime:轮询与实时刷新
通过refetchInterval选项可开启轮询:例如refetchInterval: 5_000会让查询每 5 秒自动重新获取一次,实现接近"实时"的数据同步;配合refetchIntervalInBackground可控制页面不可见时是否继续轮询。结合上面的窗口聚焦机制,可以搭建"聚焦即刷新 + 定时轮询"的组合实时方案。
4.4 源码视角:Observer 与 useSyncExternalStore
在 useBaseQuery.ts 中可以观察到上述行为的底层实现:
const [observer] = useState(() => new Observer(client, defaultedOptions)) const result = observer.getOptimisticResult(defaultedOptions) useSyncExternalStore( useCallback( (onStoreChange) => { const unsubscribe = shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop observer.updateResult() return unsubscribe }, [observer, shouldSubscribe], ), () => observer.getCurrentResult(), )- 每个
useQuery在内部对应一个QueryObserver,通过useState惰性创建并保持稳定; - 渲染时先调用
getOptimisticResult取得乐观结果(即使订阅尚未建立,也能立刻基于缓存给出数据); - 通过
useSyncExternalStore订阅 observer,任何缓存变化经notifyManager.batchCalls批量化后触发 Preact 重新渲染; useEffect中再调用observer.setOptions(defaultedOptions)同步最新选项。
此外,默认开启属性级追踪:当未显式设置notifyOnChangeProps时,返回observer.trackResult(result),组件只会在其实际读取到的属性变化时重渲染,从而避免无关更新造成的不必要渲染。查询组件自身的挂载/卸载、缓存驱逐等行为,则由QueryCache的订阅机制统一驱动。
五、并行查询与依赖查询
README 特性第三条:Parallel + Dependent Queries。
5.1 并行查询:useQueries
useQueries用于一次性发起多个相互独立的查询并保持并行执行,适合仪表盘等需要同时加载多组数据的场景。它内部使用QueriesObserver统一管理一组子 observer(见 useQueries.ts 的导入与 useQueries.test.tsx 的用例)。基本用法:
const results = useQueries({ queries: [ { queryKey: ['repo', 'A'], queryFn: fetchRepoA }, { queryKey: ['repo', 'B'], queryFn: fetchRepoB }, ], })返回的results是一个数组,每个元素与useQuery的返回结构一致,可单独读取status/data/error。此外该函数还支持元组形式传入每个查询的显式类型参数,并有配套的类型级工具(QueriesOptions/QueriesResults),保证推断出的data类型与每个子查询一一对应。
5.2 依赖查询:enabled 与 skipToken
当某个查询需要依赖另一个查询的结果作为参数时,用enabled控制其执行时机即可,例如 useQuery.ts 中 JSDoc 给出的经典模式:
function Post({ postId }: { postId: number | undefined }) { const { data, isLoading, isError, error } = useQuery({ queryKey: ['post', postId], queryFn: () => fetchPost(postId!), enabled: postId != null, }) if (postId == null) return 'Select a post' if (isLoading) return 'Loading...' if (isError) return <span>Error: {error.message}</span> return <h1>{data?.title}</h1> }要点:
- 使用
isLoading而非isPending判断加载态——isLoading等价于isPending && isFetching,查询被禁用(disabled)时不会误显示加载状态; - v5 还提供了更类型安全的替代:把
queryFn传为skipToken(如queryFn: postId != null ? () => fetchPost(postId) : skipToken),从而免去非空断言postId!,编译器能保证queryFn只在postId有值时被调用。注意skipToken模式下refetch()不可用,需要手动触发时请改用enabled: false。
依赖查询可以进一步串联:查询 B 的queryKey中包含查询 A 的data,并设置enabled: !!dataA,即可实现"先取用户、再取该用户的帖子"这类链式依赖。
六、Mutation 变更与响应式查询重取
README 特性第四条:Mutations + Reactive Query Refetching。查询(query)负责读取,变更(mutation)负责写入——典型的 create/update/delete 操作或服务端副作用都应走useMutation。
6.1 基础用法
见 useMutation.ts 的 JSDoc 示例:
import { useMutation, useQueryClient } from '@tanstack/preact-query' function AddTodo() { const queryClient = useQueryClient() const addMutation = useMutation({ mutationFn: addTodo, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) return ( <div> {addMutation.isPending ? ( 'Adding todo...' ) : ( <> {addMutation.isError ? ( <div>An error occurred: {addMutation.error.message}</div> ) : null} <button onClick={() => addMutation.mutate('Item')}>Add</button> </> )} </div> ) }useMutation返回mutate/mutateAsync两个触发函数,以及isPending/isError/error/data等状态。两者的区别:
mutate(variables, callbacks?):触发后不返回 Promise,错误只能通过回调或状态捕获;mutateAsync(variables):返回 Promise,可用await/try-catch/Promise.all等待结果,适合批量提交等场景(源码见 useMutation.ts,mutate内部实际调用observer.mutate(...)并.catch(noop)吞掉未处理拒绝)。
6.2 响应式查询重取:invalidateQueries 与乐观更新
"Reactive Query Refetching"指的就是变更成功后让相关查询重新获取。最直接的方式是invalidateQueries——把目标查询标记为 stale 并触发重取,如上例。更进一步的模式是乐观更新(optimistic update):在请求发出前先修改本地缓存,失败时回滚。这正是仓库中 examples/optimistic-updates(React 版)与 preact 包内 useMutation.test.tsx 所验证的完整链路:
const addMutation = useMutation({ mutationFn: addTodo, onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }) const previousTodos = queryClient.getQueryData<Array<string>>(['todos']) queryClient.setQueryData<Array<string>>(['todos'], (old) => [ ...(old ?? []), newTodo, ]) // 返回快照,失败时用于回滚 return { previousTodos } }, onError: (_err, _newTodo, onMutateResult) => { queryClient.setQueryData(['todos'], onMutateResult?.previousTodos) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })回调的语义值得注意(源码 JSDoc 有明确说明):
- Hook 级回调(传入
options的onSuccess/onError/onSettled)对每一次mutation 生效; - 每次调用
mutate时传入的"单次回调"只对最近一次调用生效,且仅在该组件仍挂载时才会触发——组件卸载会使订阅失效,从而阻止这些回调执行。
useMutation底层使用MutationObserver并同样通过useSyncExternalStore订阅(见 useMutation.ts);若设置了throwOnError,错误会在渲染阶段被抛出(见同文件末尾)。
七、多层缓存与自动垃圾回收
README 特性第五条:Multi-layer Cache + Automatic Garbage Collection。
7.1 多层缓存结构
@tanstack/query-core将数据存放在多个层级:
QueryCache:保存所有查询的键值状态,提供按queryHash查找与全局订阅;MutationCache:保存所有 mutation 状态(useMutationState/useIsMutating正是从它读取全局变更状态);- 每个
Query:内部维护state.data、state.status、state.fetchStatus、dataUpdatedAt、errorUpdatedAt等字段,是缓存的基本单元。
这种分层让"一个 queryKey 一处缓存、多处组件共享"成为可能:两个组件使用相同queryKey时共享同一份数据与获取状态,不会重复请求。
7.2 垃圾回收(GC)
查询在没有活跃订阅者(即没有任何挂载的 observer 引用它)后,进入可回收状态:默认gcTime(v5 中替代旧的cacheTime)为 5 分钟,计时结束且期间未重新被订阅时,该查询会被从缓存中清除。可全局或按查询调整gcTime,例如:
useQuery({ queryKey: ['heavy-data'], queryFn: fetchHeavyData, gcTime: 10 * 60 * 1000, // 10 分钟无订阅后回收 })值得注意的是,Suspense 模式下 suspense.ts 会对staleTime和gcTime施加下限(MIN_SUSPENSE_TIME_MS = 1000,即至少 1000ms),避免组件挂起后重新挂载时触发不必要的立即重取。
八、分页查询与游标查询
README 特性第六条:Paginated + Cursor-based Queries。分页有两种常见实现:
- Key-based 分页(页码切换):把页码放进
queryKey,每次切换页码就是切换一个新的查询。配合placeholderData: keepPreviousData(从@tanstack/query-core导出),切换页时上一页数据会作为占位数据保留展示,isPlaceholderData为true,直至新页数据到达,页面不会闪空白:
import { keepPreviousData, useQuery } from '@tanstack/preact-query' import { useState } from 'preact/hooks' function Posts() { const [page, setPage] = useState(0) const { data, isPlaceholderData, isError, error } = useQuery({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), placeholderData: keepPreviousData, }) if (isError) return <span>Error: {error.message}</span> return ( <div> <ul> {data?.map((post) => <li key={post.id}>{post.title}</li>)} </ul> <button disabled={isPlaceholderData} onClick={() => setPage((old) => old + 1)} > Next Page </button> </div> ) }- Cursor-based(游标)分页:使用
useInfiniteQuery,通过pageParam携带游标(如nextId),见 useInfiniteQuery.ts 中的完整示例。它支持向前(fetchNextPage/hasNextPage)与向后(fetchPreviousPage/hasPreviousPage)两个方向,数据以data.pages与data.pageParams两个平行数组组织:
const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, })useInfiniteQuery的选项与useQuery完全相同,仅额外增加initialPageParam、getNextPageParam、getPreviousPageParam、maxPages四个字段;其返回结果也等价于useQuery加data.pages、data.pageParams、fetchNextPage、fetchPreviousPage、hasNextPage、hasPreviousPage、isFetchingNextPage、isFetchingPreviousPage。内部实现上,它通过useBaseQuery传入InfiniteQueryObserver(见 useInfiniteQuery.ts),由核心层的InfiniteQueryObserver统一管理分页状态机。
源码注释中的提醒:命令式分页调用(如
fetchNextPage)可能与默认的重取行为相互干扰,导致数据过期。因此建议仅在用户操作触发时调用,或加上hasNextPage && !isFetching之类的守卫条件。
九、加载更多与无限滚动
README 特性第七条:Load-More + Infinite Scroll Queries w/ Scroll Recovery。在第八节useInfiniteQuery的基础上,无限滚动只需把"点击按钮"替换为"监听滚动到达底部":
const sentinelRef = useRef<HTMLDivElement>(null) useEffect(() => { const sentinel = sentinelRef.current if (sentinel == null || !hasNextPage || isFetching) return const observer = new IntersectionObserver(([entry]) => { if (entry?.isIntersecting) fetchNextPage() }) observer.observe(sentinel) return () => observer.disconnect() }, [hasNextPage, isFetching, fetchNextPage])要点:
- 在列表末尾放置一个哨兵元素(sentinel),用
IntersectionObserver监测其进入视口; - 只有
hasNextPage && !isFetching时才建立观察,避免滚动到底部后重复触发; - 按钮版则用
disabled={!hasNextPage || isFetching}并依据isFetchingNextPage显示 "Loading more...",这是 useInfiniteQuery.ts JSDoc 提供的两种标准写法。
Scroll Recovery(滚动恢复)指回到之前的页面/列表位置时恢复滚动位置与已加载的分页数据。因为每页数据都以独立 page 缓存,配合queryKey的稳定标识,框架级路由(如 TanStack Router)可以轻松恢复状态;即使纯手工实现,也只需在重建列表后依据已加载 page 数量恢复滚动偏移。
十、请求取消
README 特性第八条:Request Cancellation。当查询因组件卸载、queryKey变化或竞态条件而不需要旧结果时,Query 会中止对应的在途请求。
queryFn通过AbortSignal感知取消。在 useQuery.ts 与相关类型定义中,QueryFunctionContext携带signal字段,典型写法:
useQuery({ queryKey: ['posts'], queryFn: ({ signal }) => fetch('/api/posts', { signal }), })支持AbortSignal的客户端(原生fetch、axios 的signal选项等)会自动中止网络请求;对于无法原生取消的请求,可以监听signal的abort事件做清理。取消对用户是无感的——被取消的查询不会进入错误状态,也不会污染缓存。
需要注意的一个已知取舍(源码 useSuspenseQuery.ts JSDoc 明确标注):Suspense 模式下取消(cancellation)不可用,使用useSuspenseQuery时应了解这一限制。
十一、Preact Suspense 与 Fetch-As-You-Render 预取
README 特性第九条:Preact Suspense + Fetch-As-You-Render Query Prefetching,并附注"Not recommended because of the bulk preact/compat adds"——即该能力依赖preact/compat的 Suspense 实现,会引入额外的兼容层体积,因此 README 明确提示不推荐在体积敏感的场景使用。
11.1 useSuspenseQuery
useSuspenseQuery.ts 是useQuery的 Suspense 变体,其实现本质是对useBaseQuery的固定封装:
return useBaseQuery( { ...options, enabled: true, suspense: true, throwOnError: defaultThrowOnError, placeholderData: undefined, }, QueryObserver, queryClient, )与useQuery的差异集中体现为:
enabled被强制为true;suspense: true使数据未就绪时渲染被挂起(suspends)而不是返回pending;throwOnError使用默认实现defaultThrowOnError(见 suspense.ts):仅在查询没有缓存数据(query.state.data === undefined)时抛错,即"首次加载失败抛给错误边界,后台刷新失败则继续展示旧数据";placeholderData被清空。
用法上,返回的data保证已定义,无需isPending判断;错误通过最近的错误边界呈现,因此必须把组件包在<ErrorBoundary>之内,并配合QueryErrorResetBoundary实现"重试":
import { Suspense } from 'preact/compat' import { QueryErrorResetBoundary, useSuspenseQuery } from '@tanstack/preact-query' function Posts() { const { data, isFetching } = useSuspenseQuery({ queryKey: ['posts'], queryFn: fetchPosts, }) // data 保证存在 return ( <div> <h1>Posts {isFetching ? '(refreshing...)' : null}</h1> <ul> {data.map((post) => <li key={post.id}>{post.title}</li>)} </ul> </div> ) } function App() { return ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset}> <Suspense fallback={<h1>Loading posts...</h1>}> <Posts /> </Suspense> </ErrorBoundary> )} </QueryErrorResetBoundary> ) }QueryErrorResetBoundary组件及其useQueryErrorResetBoundaryHook 均从本包导出(见 src/index.ts),对应的错误边界行为由 errorBoundaryUtils.ts 与 QueryErrorResetBoundary.tsx 实现,并有 QueryResetErrorBoundary.test.tsx 覆盖。
11.2 组件内多个 Suspense 查询的注意点
源码 JSDoc 特别提醒:在同一个组件里写多个useSuspenseQuery会串行挂起,形成请求瀑布(waterfall)——第一个挂起会阻塞渲染,第二个查询直到第一个 resolve 后才开始发起。需要并行时请改用useSuspenseQueries(对应导出见 src/index.ts),它一次性并发发起多个 Suspense 查询。
11.3 Fetch-As-You-Render 预取:usePrefetchQuery
usePrefetchQuery.tsx 实现了"渲染即预取"模式:在进入<Suspense>边界之前的父组件渲染期间触发数据获取,使子组件挂起后能立刻读到已就绪的数据,从而缩短等待时间:
import { Suspense } from 'preact/compat' import { usePrefetchQuery } from '@tanstack/preact-query' function App() { // 在 Suspense 边界之前于渲染期触发预取 usePrefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts, }) return ( <Suspense fallback={<h1>Loading posts...</h1>}> <Posts /> </Suspense> ) }其实现非常精简(usePrefetchQuery.tsx):
if (!client.getQueryState(options.queryKey)) { void client.query(options).catch(noop) }- 若该
queryKey已存在任何缓存状态(包括上一次遗留的pending/error状态),则跳过预取,因此每次渲染调用它都足够廉价,不会重复请求已存在或已在途的数据; - 无返回值(
void),只负责"点火"。
配套的usePrefetchInfiniteQuery提供无限查询的渲染期预取。该模式与 Suspense、SSR 水合配合,构成 TanStack Query 的"数据在渲染前就绪"哲学。
十二、SSR 水合与持久化
虽然不在 README 的 Quick Features 列表中,但作为数据层的核心基建值得一提:本包导出的HydrationBoundary用于把服务端dehydrate(queryClient)得到的状态注入客户端缓存(见 HydrationBoundary.tsx)。其关键行为(源码注释与 ssr-hydration.test.tsx 均有覆盖):
- 新查询在渲染阶段直接水合到缓存,保证 SSR 首屏数据在客户端渲染前就位;
- 已存在的查询仅在脱水数据更新(按
dataUpdatedAt比较,并考虑带 promise 的流式脱水)时,于提交后的 effect 中水合,避免在过渡(transition)期间意外覆盖当前页面数据; - 只能水合
queries,不能水合mutations。
此外,IsRestoringProvider/useIsRestoring用于持久化恢复期间跳过订阅,避免恢复过程中触发不必要重取;@tanstack/query-persist-client-core则被作为 devDependency 用于相关测试(见 fine-grained-persister.test.tsx)。
十三、专用 Devtools
README 特性最后一条:Dedicated Devtools。@tanstack/preact-query提供了与框架深度集成的调试工具,其源码位于 packages/preact-query-devtools,内含基于 Preact 的实现(packages/preact-query-devtools/src)。Devtools 面板可以:
- 查看全部缓存的查询与变更,包括
queryKey、状态、dataUpdatedAt、fetchStatus; - 逐条检查/刷新/删除缓存条目,模拟失焦重取与在线/离线切换;
- 定位查询的活跃观察者与最近更新,辅助排查重渲染与过期问题。
面板与QueryClient的通信同样建立在@tanstack/query-core的缓存订阅机制之上,因此在本地开发时能直接观察到第四节描述的缓存与重取行为。
十四、小结:从 README 到源码的能力全景
回到 packages/preact-query/README.md 的 Quick Features,我们可以把每一条与仓库实现一一对应:
| README 特性 | 对应 Hook / 机制 | 仓库依据 |
|---|---|---|
| Transport agnostic data fetching | useQuery+QueryFunction | useQuery.ts、src/index.ts |
| Auto Caching + Refetching | QueryCache、staleTime、refetchOnWindowFocus、refetchInterval | useBaseQuery.ts、QueryClientProvider.tsx |
| Parallel + Dependent Queries | useQueries、enabled、skipToken | useQueries.ts、useQuery.ts |
| Mutations + Reactive Refetching | useMutation、invalidateQueries、乐观更新 | useMutation.ts |
| Multi-layer Cache + Auto GC | QueryCache/MutationCache、gcTime | useMutationState.ts |
| Paginated + Cursor-based Queries | placeholderData: keepPreviousData、useInfiniteQuery | useInfiniteQuery.ts |
| Load-More + Infinite Scroll | fetchNextPage+IntersectionObserver | useInfiniteQuery.ts 示例 |
| Request Cancellation | AbortSignal | useQuery.ts |
| Preact Suspense + Fetch-As-You-Render | useSuspenseQuery、useSuspenseQueries、usePrefetchQuery | useSuspenseQuery.ts、usePrefetchQuery.tsx、suspense.ts |
| Dedicated Devtools | @tanstack/preact-query-devtools | packages/preact-query-devtools |
通过上述对照可以看到:@tanstack/preact-query的所有面向开发者的特性,底层都由统一的@tanstack/query-core状态机与 observer 订阅模型支撑,Preact 层只负责"用 Preact 的方式把核心能力暴露为 Hooks"。这意味着只要理解了QueryClient+queryKey+ observer 三元组,就能在 Preact 中稳定地构建出可缓存、可取消、可轮询、可无限滚动的数据获取层,同时保持组件代码的简洁与声明式。更进一步,若你需要把数据方案接入 SSR,可结合 HydrationBoundary.tsx 与dehydrate;需要持久化时,可查看@tanstack/query-persist-client-core(packages/query-persist-client-core)与对应的 fine-grained-persister.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),仅供参考