TanStack Query Preact 持久化指南:persistQueryClient 与 PersistQueryClientProvider 完整实战
2026/9/9 23:45:17 网站建设 项目流程

TanStack Query Preact 持久化指南:persistQueryClient 与 PersistQueryClientProvider 完整实战

【免费下载链接】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

导读

persistQueryClient是 TanStack Query 提供给 Preact 应用的"查询缓存持久化"方案:它把QueryClient中已被dehydrate序列化的 query / mutation 缓存交给一个persister(持久化器)保存到本地存储层(localStorage、IndexedDB 等),下次启动时再把缓存hydrate回内存缓存,实现离线数据复用与秒开体验。读完本文你将掌握四个核心 API(persistQueryClientSave/persistQueryClientSubscribe/persistQueryClientRestore/persistQueryClient)的用法与差异,理解gcTimemaxAge的关键配合,并能用PersistQueryClientProvider无竞态地把 Preact 应用接入持久化存储。

仓库说明:本文对应的 Preact 官方文档页面 docs/framework/preact/plugins/persistQueryClient.md 采用 frontmatter 重定向机制(ref指向 React 版本,并声明react-query → preact-queryReact → Preact的文案替换),因此本文正文依据其源正文并结合 Preact 侧真实实现展开,所有代码示例均按 Preact 适配。

一、认识 persister 与 persistQueryClient 的作用

persistQueryClient 本质上是一组与persister交互的工具函数,persister 负责把 queryClient 及缓存保存到不同的存储层,并在需要时取回。整个持久化链路由三层构成:

  1. 存储层(Storage):如localStorage、IndexedDB 等实际落盘位置;
  2. Persister(持久化器):封装persistClient/restoreClient/removeClient三个方法的读写器;
  3. Persist 工具函数(@tanstack/query-persist-client-core:负责调用dehydrate/hydrate完成序列化与反序列化,并与 persister 协作。

构建 Persister 的三种途径

  • 同步存储:使用 createSyncStoragePersister,适用于localStorage/sessionStorage等同步StorageAPI;
  • 异步存储:使用 createAsyncStoragePersister,适用于 IndexedDB、React Native 的 AsyncStorage 等异步存储;
  • 完全自定义:按照本文第六节的Persister接口手写自己的持久化器(如基于 IndexedDB、文件系统等)。

Preact 框架下运行时相关代码在 packages/preact-query-persist-client/src/index.ts,该入口把核心逻辑整体从@tanstack/query-persist-client-core转发导出,并额外导出了 Preact 专用的PersistQueryClientProvider

二、原理详解:gcTime、maxAge 与缓存生命周期

重要前提:持久化恢复依赖的是"未过期"的缓存,因此在创建QueryClient时,最好显式传入一个gcTime值来覆盖默认的垃圾回收策略。

为什么 gcTime 直接影响持久化

如果不设置gcTimeQueryClient会采用默认值300000毫秒(即5 分钟):一份没有活跃订阅者的缓存会在 5 分钟不活跃后被垃圾回收丢弃。这意味着即使你从存储中恢复了缓存,只要超过 5 分钟没有使用,恢复出来的数据也会被当作垃圾清掉——这就是"存了等于白存"的常见陷阱。

因此gcTime应设置为等于或大于persistQueryClientmaxAge选项。例如maxAge默认是 24 小时,那么gcTime也应设为 24 小时或更长。若gcTime小于maxAge,垃圾回收会抢先触发,把本应仍可用的持久化缓存提前丢弃,与预期不符:

const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, })

你还可以把gcTime设为Infinity,从而完全禁用垃圾回收行为。

时间上限与绕过方案

由于 JavaScript 引擎的限制(setTimeout的最大延时约为 24 天),gcTime的最大允许值大约是24 天。如果业务上确实需要更长的持久化有效期,可以通过timeoutManager.setTimeoutProvider自定义定时器实现来突破该上限,具体见 docs/reference/timeoutManager.md。这一机制在 packages/query-core 的 timeoutManager 中实现。

三、Cache Busting:主动让旧缓存全部失效

有时你发布的新版本或数据结构变更会立刻使所有已缓存数据失效。此时可传入一个buster字符串:当从存储中找到的缓存不携带相同的 buster 字符串时,该缓存会被直接丢弃。以下函数都接受该选项:

persistQueryClient({ queryClient, persister, buster: buildHash }) persistQueryClientSave({ queryClient, persister, buster: buildHash }) persistQueryClientRestore({ queryClient, persister, buster: buildHash })

典型用法是把构建产物指纹(如 webpack/vite 的构建 hash)作为buster,每次发版构建 hash 变化后,旧版本持久化的缓存将自动失效,避免脏数据穿透上线。

四、Removal:什么情况下缓存会被立即清除

当从存储中读到的数据命中以下任一情况时,persister 的removeClient()会被调用,缓存被立即丢弃

  1. 过期(expired):超出maxAge允许的存活时长;
  2. 被 bust(busted)buster字符串不匹配;
  3. 出错(error):恢复过程抛出异常(如反序列化失败);
  4. 为空(empty):恢复结果为undefined

这一判定逻辑在 packages/query-persist-client-core/src/persist.ts 的persistQueryClientRestore中有完整实现:当expiredbusted为真时调用removeClient(),任何抛错都会在非生产环境打印错误与告警后兜底清除并再次抛出。

五、API 全解:四个核心函数

1.persistQueryClientSave:手动保存一次缓存

  • 你的 query / mutation 会被dehydrate序列化,再交由你提供的 persister 存储。
  • createSyncStoragePersistercreateAsyncStoragePersister会把该动作节流为至少每 1 秒最多一次,以避免频繁且昂贵的存储写入;如需调整节流频率,请查看它们各自的文档。

在你想主动持久化的时刻(例如用户勾选"记住我"并登录成功后)手动调用它:

persistQueryClientSave({ queryClient, persister, buster = '', dehydrateOptions = undefined, })

底层实现见 packages/query-persist-client-core/src/persist.ts:把当前busterDate.now()时间戳以及dehydrate(queryClient, dehydrateOptions)的结果组装成一个PersistedClient再交给persister.persistClient()

2.persistQueryClientSubscribe:订阅缓存变更自动保存

只要queryClient的缓存发生变化就执行persistQueryClientSave。例如在用户登录并勾选"记住我"时启动订阅,让缓存与存储持续保持同步。

  • 它返回一个unsubscribe函数,调用即可停止监控、终止对持久化缓存的更新;
  • 若想在unsubscribe之后擦除已持久化的缓存,可以给persistQueryClientRestore传入一个新的buster,从而触发 persister 的removeClient并丢弃持久化缓存。
persistQueryClientSubscribe({ queryClient, persister, buster = '', dehydrateOptions = undefined, })

从源码看,该函数同时订阅了 QueryCache 与 MutationCache(见 packages/query-persist-client-core/src/persist.ts),只有当事件类型属于addedremovedupdated这类真正的缓存变更时才触发保存,从而避免 Observer 层面的无关事件造成无效写入。

3.persistQueryClientRestore:手动恢复一次缓存

  • 尝试把先前持久化的 dehydrated query / mutation 缓存从 persister 中hydrate回传入 queryClient 的查询缓存;
  • 如果找到的缓存比maxAge(默认24 小时=1000 * 60 * 60 * 24)更旧,它会被丢弃。该时长可按需自定义。
persistQueryClientRestore({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, // 24 hours buster = '', hydrateOptions = undefined, })

4.persistQueryClient:组合拳(恢复 + 订阅)

它一次性完成两件事:

  1. 立即恢复任何已持久化的缓存(见persistQueryClientRestore);
  2. 订阅 query cache,并返回unsubscribe函数(见persistQueryClientSubscribe)。

该功能从 3.x 版本保留至今:

persistQueryClient({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, // 24 hours buster = '', hydrateOptions = undefined, dehydrateOptions = undefined, })

源码中该函数返回一个元组[unsubscribe, restorePromise](见 packages/query-persist-client-core/src/persist.ts):先发起恢复,只有恢复成功且尚未被取消订阅时才建立订阅,以此避免恢复失败后还持续写入空缓存。

Options:完整配置项

interface PersistQueryClientOptions { /** The QueryClient to persist */ queryClient: QueryClient /** The Persister interface for storing and restoring the cache * to/from a persisted location */ persister: Persister /** The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be **silently** discarded * (defaults to 24 hours) */ maxAge?: number /** A unique string that can be used to forcefully * invalidate existing caches if they do not share the same buster string */ buster?: string /** The options passed to the hydrate function * Not used on `persistQueryClientSave` or `persistQueryClientSubscribe` */ hydrateOptions?: HydrateOptions /** The options passed to the dehydrate function * Not used on `persistQueryClientRestore` */ dehydrateOptions?: DehydrateOptions }

实际上框架提供了三个类型化接口,分别约束不同函数:

  • PersistedQueryClientSaveOptions—— 用于persistQueryClientSavepersistQueryClientSubscribe(不使用hydrateOptions);
  • PersistedQueryClientRestoreOptions—— 用于persistQueryClientRestore(不使用dehydrateOptions);
  • PersistQueryClientOptions—— 用于persistQueryClient,是前两者的并集。

这些类型定义与默认值均可在 packages/query-persist-client-core/src/persist.ts 中逐一核对。

六、Preact 中的正确接入:PersistQueryClientProvider

裸用 persistQueryClient 的风险

persistQueryClient会尝试恢复缓存并自动订阅后续变更,从而把 client 同步到存储。但恢复是异步的(所有 persister 本质都是异步的),若在恢复的同时渲染 App,挂载的 query 与恢复动作并发执行就会产生竞态条件;此外,若在组件生命周期之外订阅,你将无法优雅退订:

// 🚨 never unsubscribes from syncing persistQueryClient({ queryClient, persister: localStoragePersister, }) // 🚨 happens at the same time as restoring // createRoot(rootElement).render(<App />)

PersistQueryClientProvider:框架级解决方案

针对上述问题,Preact 侧提供了PersistQueryClientProvider。它会依据Preact 组件生命周期正确订阅 / 退订,并且保证在恢复完成前不会让 query 开始拉取。恢复期间 query 仍然可以渲染,只是被置为fetchingState: 'idle';恢复完成后,除非已恢复的数据足够新鲜,否则它们会触发重新拉取,且initialData也会被尊重。在 Preact 中它可取代常规的 QueryClientProvider 使用:

import { PersistQueryClientProvider } from '@tanstack/preact-query-persist-client' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import { QueryClient } from '@tanstack/preact-query' const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) const persister = createAsyncStoragePersister({ storage: window.localStorage, }) // Preact 中渲染示例(示意) // render( // <PersistQueryClientProvider // client={queryClient} // persistOptions={{ persister }} // > // <App /> // </PersistQueryClientProvider>, // rootElement, // )

注意示例中 provider 的persistOptions只需传persister等配置,不需要再传queryClient——它由clientprop 提供。

Props 说明

PersistQueryClientProvider接受与 QueryClientProvider 相同的 props,并额外支持:

  • persistOptions: PersistQueryClientOptions
    • 即传给persistQueryClient的全部 options,去掉 QueryClient 本身
  • onSuccess?: () => Promise<unknown> | unknown
    • 可选;初始恢复完成时被调用;
    • 可用于调用 QueryClient 的 resumePausedMutations 恢复离线期间暂停的 mutation;
    • 若返回 Promise 则会被等待,在此期间恢复状态视为持续中;
  • onError?: () => Promise<unknown> | unknown
    • 可选;恢复期间抛出错误时被调用;
    • 若返回 Promise 则会被等待。

Preact 侧的真实实现

Preact 版 Provider 源码在 packages/preact-query-persist-client/src/PersistQueryClientProvider.tsx,核心逻辑清晰可循:

  • useState(true)维护isRestoring,并通过useRef+useEffect保持最新的persistOptions/onSuccess/onError
  • 挂载时调用persistQueryClientRestore(options),随后依次触发onSuccess(失败则onError),并在finally中把isRestoring置为false
  • 只有当isRestoring变为false后,才返回persistQueryClientSubscribe(options)建立缓存同步订阅,从而保证恢复期间绝不写入、恢复完成才自动保存后续变更;
  • 渲染时用QueryClientProvider包裹,并向子树提供IsRestoringProvider,其值与恢复状态一致。

仓库中的测试 packages/preact-query-persist-client/src/tests/PersistQueryClientProvider.test.tsx 与配套的 testPersistProvider.tsx 正是对这一生命周期行为(先恢复、恢复后订阅)的验证。

useIsRestoring:感知恢复中的状态

使用PersistQueryClientProvider时,你还可以搭配useIsRestoring钩子判断当前是否正在恢复。useQuery等钩子内部也会检查该状态(参见 packages/preact-query/src/useBaseQuery.ts 与 useQueries.ts),以规避恢复与挂载 query 之间的竞态。该 Context 的定义位于 packages/preact-query/src/IsRestoringProvider.ts,由@tanstack/preact-query统一导出。

七、Persister 接口与自定义持久化器

Persister 与 PersistedClient 接口

export interface Persister { persistClient(persistClient: PersistedClient): Promisable<void> restoreClient(): Promisable<PersistedClient | undefined> removeClient(): Promisable<void> }

持久化的客户端条目(即存储到存储层的完整快照)结构如下:

export interface PersistedClient { timestamp: number buster: string clientState: DehydratedState }

你可以在 Preact 项目中这样导入它们(用于构建 persister 的类型标注):

import { PersistedClient, Persister, } from '@tanstack/preact-query-persist-client'

动手构建一个 IndexedDB Persister

持久化的形式完全由你决定。这里演示如何基于 IndexedDB 编写一个 persister:相比Web Storage API,IndexedDB 更快、可存储超过 5MB 数据、且无需 JSON 序列化——因此能直接保存DateFile等 JavaScript 原生类型。

import { get, set, del } from 'idb-keyval' import { PersistedClient, Persister, } from '@tanstack/preact-query-persist-client' /** * Creates an Indexed DB persister * @see https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API */ export function createIDBPersister(idbValidKey: IDBValidKey = 'preactQuery') { return { persistClient: async (client: PersistedClient) => { await set(idbValidKey, client) }, restoreClient: async () => { return await get<PersistedClient>(idbValidKey) }, removeClient: async () => { await del(idbValidKey) }, } satisfies Persister }

promisable语义(T | PromiseLike<T>)意味着你既可以用同步实现,也可以用异步实现,从而让自研 persister 天然兼容不同的底层存储。

八、进阶:实验性的按查询持久化(createPersister)

persistQueryClient体系之外,仓库还提供了一套实验性的细粒度持久化方案——createPersister。其实现位于 packages/query-persist-client-core/src/createPersister.ts,允许把 persister 直接挂到单个useQueryqueryClientdefaultOptions上,仅持久化满足filters的查询:

useQuery({ queryKey: ['myKey'], queryFn: fetcher, persister: createPersister({ storage: localStorage, }), })

从源码看,它围绕storage提供serialize(默认JSON.stringify)、deserialize(默认JSON.parse)、bustermaxAge(默认 24 小时)、prefix(默认tanstack-query)、refetchOnRestore(默认true,也支持'always')与filters等选项,存储键为${prefix}-${queryHash};恢复时会保持数据的dataUpdatedAt,并在数据过期时通过persisterGc清理存储条目。若存储未实现entries()方法则无法遍历所有条目,GC 与批量恢复能力将不可用——这是选择存储层时需要留意的前提。该方向与整包持久化互补,适合只需要缓存部分查询的场景。

总结

把 TanStack Query 的缓存落到本地,关键在于三件事的协同:合理的gcTime配置(必须不小于maxAge)、正确的恢复时机控制(交给PersistQueryClientProvider而不是在组件外裸调),以及贴合场景的 persister 选型(同步存储、异步存储或自定义)。基于 packages/query-persist-client-core 的底层实现与 packages/preact-query-persist-client 的 Preact 封装,你可以为应用建立一套启动免加载、离线可读、发版可失效的健壮缓存体系。相关配套实现(同步 / 异步 persister 的节流细节与存储适配)可进一步查阅 createSyncStoragePersister 与 createAsyncStoragePersister。

【免费下载链接】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),仅供参考

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

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

立即咨询