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

TanStack Query Preact 持久化指南:persistQueryClient 与 PersistQueryClientProvider 完整实战 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回内存缓存实现离线数据复用与秒开体验。读完本文你将掌握四个核心 APIpersistQueryClientSave/persistQueryClientSubscribe/persistQueryClientRestore/persistQueryClient的用法与差异理解gcTime与maxAge的关键配合并能用PersistQueryClientProvider无竞态地把 Preact 应用接入持久化存储。仓库说明本文对应的 Preact 官方文档页面 docs/framework/preact/plugins/persistQueryClient.md 采用 frontmatter 重定向机制ref指向 React 版本并声明react-query → preact-query、React → Preact的文案替换因此本文正文依据其源正文并结合 Preact 侧真实实现展开所有代码示例均按 Preact 适配。一、认识 persister 与 persistQueryClient 的作用persistQueryClient 本质上是一组与persister交互的工具函数persister 负责把 queryClient 及缓存保存到不同的存储层并在需要时取回。整个持久化链路由三层构成存储层Storage如localStorage、IndexedDB 等实际落盘位置Persister持久化器封装persistClient/restoreClient/removeClient三个方法的读写器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应设置为等于或大于persistQueryClient的maxAge选项。例如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()会被调用缓存被立即丢弃过期expired超出maxAge允许的存活时长被 bustbustedbuster字符串不匹配出错error恢复过程抛出异常如反序列化失败为空empty恢复结果为undefined。这一判定逻辑在 packages/query-persist-client-core/src/persist.ts 的persistQueryClientRestore中有完整实现当expired或busted为真时调用removeClient()任何抛错都会在非生产环境打印错误与告警后兜底清除并再次抛出。五、API 全解四个核心函数1.persistQueryClientSave手动保存一次缓存你的 query / mutation 会被dehydrate序列化再交由你提供的 persister 存储。createSyncStoragePersister与createAsyncStoragePersister会把该动作节流为至少每 1 秒最多一次以避免频繁且昂贵的存储写入如需调整节流频率请查看它们各自的文档。在你想主动持久化的时刻例如用户勾选记住我并登录成功后手动调用它persistQueryClientSave({ queryClient, persister, buster , dehydrateOptions undefined, })底层实现见 packages/query-persist-client-core/src/persist.ts把当前buster、Date.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只有当事件类型属于added、removed、updated这类真正的缓存变更时才触发保存从而避免 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组合拳恢复 订阅它一次性完成两件事立即恢复任何已持久化的缓存见persistQueryClientRestore订阅 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—— 用于persistQueryClientSave与persistQueryClientSubscribe不使用hydrateOptionsPersistedQueryClientRestoreOptions—— 用于persistQueryClientRestore不使用dehydrateOptionsPersistQueryClientOptions—— 用于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?: () Promiseunknown | unknown可选初始恢复完成时被调用可用于调用 QueryClient 的 resumePausedMutations 恢复离线期间暂停的 mutation若返回 Promise 则会被等待在此期间恢复状态视为持续中onError?: () Promiseunknown | unknown可选恢复期间抛出错误时被调用若返回 Promise 则会被等待。Preact 侧的真实实现Preact 版 Provider 源码在 packages/preact-query-persist-client/src/PersistQueryClientProvider.tsx核心逻辑清晰可循用useState(true)维护isRestoring并通过useRefuseEffect保持最新的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): Promisablevoid restoreClient(): PromisablePersistedClient | undefined removeClient(): Promisablevoid }持久化的客户端条目即存储到存储层的完整快照结构如下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 APIIndexedDB 更快、可存储超过 5MB 数据、且无需 JSON 序列化——因此能直接保存Date、File等 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 getPersistedClient(idbValidKey) }, removeClient: async () { await del(idbValidKey) }, } satisfies Persister }promisable语义T | PromiseLikeT意味着你既可以用同步实现也可以用异步实现从而让自研 persister 天然兼容不同的底层存储。八、进阶实验性的按查询持久化createPersisterpersistQueryClient体系之外仓库还提供了一套实验性的细粒度持久化方案——createPersister。其实现位于 packages/query-persist-client-core/src/createPersister.ts允许把 persister 直接挂到单个useQuery或queryClient的defaultOptions上仅持久化满足filters的查询useQuery({ queryKey: [myKey], queryFn: fetcher, persister: createPersister({ storage: localStorage, }), })从源码看它围绕storage提供serialize默认JSON.stringify、deserialize默认JSON.parse、buster、maxAge默认 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),仅供参考