ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

将 RTK Query 接入 Redux Toolkit:createApi、Store 集成、缓存失效与乐观更新的实战指南

将 RTK Query 接入 Redux Toolkit:createApi、Store 集成、缓存失效与乐观更新的实战指南 前端状态管理【免费下载链接】redux-toolkitThe official, opinionated, batteries-included toolset for efficient Redux development项目地址https://gitcode.com/gh_mirrors/re/redux-toolkit点击查看免费下载RTK Query 是reduxjs/toolkit内置的、面向服务端数据与文档缓存的一体化数据层方案。本篇基于仓库内packages/toolkit/skills/manage-server-data/adopt-rtk-query/SKILL.md这份生命周期型技能指南系统讲解如何用createApi建立 API 切片、如何接入 Store、如何通过 tags 驱动缓存失效以及如何把乐观更新收敛到端点生命周期中同时结合packages/toolkit/src/query/下的真实源码说明这些模式背后的实现原理。读完你将从会写请求代码进阶到能把 RTK Query 用对并避开社区中最常见的高频误用。何时采用 RTK Query文档缓存模型的前提在动手写代码之前需要先明确一个前提判断RTK Query 是一个文档缓存document cache而不是规范化实体图缓存normalized entity graph cache。这一点在技能指南的端点生命周期参考文档中被明确强调。可以放心默认使用 RTK Query 的场景数据来自请求/响应式request/responseAPI文档缓存能够满足业务需要tags 失效与端点生命周期足以解决问题。需要另选工具的场景真实需求是规范化图缓存如大量实体间互相引用、需要全局归一化技术栈里已经存在更合适的领域特定规范化客户端。如果规范化缓存是硬性要求且栈内没有更好的库那么slice thunk的经典流程可以作为兜底方案。这个判断直接决定了后面的架构选择属于动手前的第一步。Setup从零接入 RTK Query技能指南给出了一个完整的接入示例涵盖 API 定义src/services/api.ts、Store 集成src/app/store.ts与 React 消费src/App.tsx三部分。核心依赖是从reduxjs/toolkit/query/react导入的createApi与fetchBaseQuery// file: src/services/api.ts import { createApi, fetchBaseQuery } from reduxjs/toolkit/query/react type Post { id: string; title: string } export const api createApi({ reducerPath: api, baseQuery: fetchBaseQuery({ baseUrl: /api/ }), tagTypes: [Post], endpoints: (build) ({ getPosts: build.queryPost[], void({ query: () posts, providesTags: (result) result ? [...result.map(({ id }) ({ type: Post as const, id })), Post] : [Post], }), addPost: build.mutationPost, PickPost, title({ query: (body) ({ url: posts, method: POST, body, }), invalidatesTags: [Post], }), }), }) export const { useGetPostsQuery, useAddPostMutation } api这个createApi调用使用了四个关键选项reducerPathAPI 切片在 Store 中挂载的 key默认值为api。根据 createApi 源码 的注释如果应用里多次调用createApi每次都必须提供唯一值baseQuery每个端点默认使用的请求函数fetchBaseQuery是 RTK Query 导出的、对原生fetch的轻量封装baseUrl用于拼接相对路径tagTypes声明的标签类型数组用于后续providesTags/invalidatesTags的缓存与失效源码注释见 createApi.tsendpoints使用 builder 语法定义的一组端点分为query查询与mutation变更两类。然后把 reducer 与 middleware 接进 Store// file: src/app/store.ts import { configureStore } from reduxjs/toolkit import { api } from ../services/api export const store configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: (getDefaultMiddleware) getDefaultMiddleware().concat(api.middleware), })最后在 React 组件中使用生成的 hooks// file: src/App.tsx import { Provider } from react-redux import { store } from ./app/store import { useAddPostMutation, useGetPostsQuery } from ./services/api function Posts() { const { data: posts [] } useGetPostsQuery() const [addPost] useAddPostMutation() return ( div button onClick{() addPost({ title: Write docs })}Add/button ul {posts.map((post) ( li key{post.id}{post.title}/li ))} /ul /div ) } export function App() { return ( Provider store{store} Posts / /Provider ) }注意这里的Provider来自react-reduxStore 中必须同时注册api.reducer与api.middleware。reducer 负责维护查询缓存状态与 tag 与缓存条目的映射middleware 负责监听 thunk action、驱动请求生命周期与失效逻辑两者缺一不可详见下文常见错误。核心模式三个应该默认遵循的写法技能指南总结了三个核心模式它们构成了用对 RTK Query的骨架。模式一一个 base URL 对应一个 API slice用injectEndpoints扩展不要把同一后端拆成多个createApi根。正确的组织方式是先创建一个空壳 API再用injectEndpoints按文件拆分端点import { createApi, fetchBaseQuery } from reduxjs/toolkit/query/react export const api createApi({ reducerPath: api, baseQuery: fetchBaseQuery({ baseUrl: /api/ }), endpoints: () ({}), }) export const postsApi api.injectEndpoints({ endpoints: (build) ({ getPosts: build.query{ id: string; title: string }[], void({ query: () posts, }), }), })技能指南给出的理由很直接用injectEndpoints拆分文件而不是为同一后端创建多个createApi根。多个根会破坏失效行为的统一性并造成 middleware 的重复工作详见常见错误一节。模式二把 tags 当作缓存失效的默认路径tags 机制是 RTK Query 自动缓存失效的基石。查询通过providesTags声明我代表了哪些缓存条目mutation 通过invalidatesTags声明我弄脏了哪些条目type Post { id: string; title: string } export const api createApi({ reducerPath: api, baseQuery: fetchBaseQuery({ baseUrl: /api/ }), tagTypes: [Post], endpoints: (build) ({ getPosts: build.queryPost[], void({ query: () posts, providesTags: (result) result ? [...result.map(({ id }) ({ type: Post as const, id })), Post] : [Post], }), updatePost: build.mutationPost, PickPost, id | title({ query: ({ id, title }) ({ url: posts/${id}, method: PATCH, body: { title }, }), invalidatesTags: (_result, _error, { id }) [{ type: Post, id }], }), }), })这里的要点providesTags支持函数形式可以把返回结果中的每一项映射为带 id 的标签如{ type: Post, id }同时追加一个不带 id 的列表级标签Post这样新增一条通过失效列表标签Post让列表整体重取而更新某条通过失效{ type: Post, id }精准重取单条invalidatesTags同样支持函数形式参数是(result, error, arg)可以从 mutation 的参数中拿到 id技能指南的建议是在考虑手动修补缓存之前先把 tags 当作常规失效路径。失效规则本身来自端点生命周期参考文档可以概括为三点有活跃订阅者的查询会重新请求无活跃订阅者的缓存条目会被移除被移除的条目只会在之后有人再次订阅时才重新请求。也就是说失效不是后台全部刷新开关——这一点在常见错误里会进一步展开。模式三在端点生命周期里做乐观更新乐观更新的正确归属是 mutation 的onQueryStarted生命周期配合api.util.updateQueryData和queryFulfilled实现先改 UI、失败回滚type Post { id: string; title: string } export const api createApi({ reducerPath: api, baseQuery: fetchBaseQuery({ baseUrl: /api/ }), tagTypes: [Post], endpoints: (build) ({ getPosts: build.queryPost[], void({ query: () posts, providesTags: [Post], }), updatePostTitle: build.mutationPost, PickPost, id | title({ query: ({ id, title }) ({ url: posts/${id}, method: PATCH, body: { title }, }), async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) { const patch dispatch( api.util.updateQueryData(getPosts, undefined, (draft) { const post draft.find((item) item.id id) if (post) { post.title title } }), ) try { await queryFulfilled } catch { patch.undo() } }, }), }), })这段代码的语义非常清晰dispatch(api.util.updateQueryData(...))用 Immer 风格的 recipe 直接修改缓存中的getPosts结果返回的patch对象携带patches、inversePatches与undo()await queryFulfilled等待请求真正完成一旦失败进入catch调用patch.undo()把缓存还原成修改前的样子。技能指南的总结是让乐观更新与悲观更新都待在端点生命周期处理器内使它们与对应请求保持耦合。这也正是常见错误里从组件里 patch 缓存一节的对照标准。常见错误与正确姿势技能指南把常见错误按严重程度分为 CRITICAL / HIGH / MEDIUM 三档下面逐条给出错误写法与正确写法的对照。CRITICAL为一个后端创建多个 API slice错误写法——两个createApi根共享同一baseQuery和同一个reducerPath: apiimport { createApi, fetchBaseQuery } from reduxjs/toolkit/query/react type User { id: string; name: string } const baseQuery fetchBaseQuery({ baseUrl: /api/ }) const postsApi createApi({ reducerPath: api, baseQuery, endpoints: () ({}), }) const usersApi createApi({ reducerPath: api, baseQuery, endpoints: () ({}), })正确写法——一个createApi根 injectEndpointsimport { createApi, fetchBaseQuery } from reduxjs/toolkit/query/react type User { id: string; name: string } const baseQuery fetchBaseQuery({ baseUrl: /api/ }) const api createApi({ reducerPath: api, baseQuery, endpoints: () ({}), }) const usersApi api.injectEndpoints({ endpoints: (build) ({ getUsers: build.queryUser[], void({ query: () users }), }), })技能指南给出的结论是每个 base URL 只保留一个 API slice这样能保住失效行为的正确性也避免重复的 middleware 工作。从源码上看tags 的提供关系provided-by 映射是挂在 reducerPath 下的内部状态里统一维护的多个根各自维护一套映射跨 slice 的失效根本无法互相感知失效语义必然被破坏。相关讨论源见 createApi 文档。HIGH忘记注册api.reducer或api.middleware错误写法——只配置了空的 reducerimport { configureStore } from reduxjs/toolkit const store configureStore({ reducer: {}, })正确写法——同时挂载 reducer 与 middlewareimport { configureStore } from reduxjs/toolkit const store configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: (getDefaultMiddleware) getDefaultMiddleware().concat(api.middleware), })技能指南解释得很清楚RTK Query 的 hooks 需要 reducer 与 middleware 同时工作才能管理缓存状态和请求生命周期。reducer 提供query/mutation的状态切片与缓存条目middleware 负责监听initiate、fulfilled、rejected等 thunk action 并触发失效与重取。缺失任何一半hooks 要么拿不到状态要么请求发出后缓存永远不更新。相关教程见 rtk-query.mdx。MEDIUM默认把浏览器里的 API 缓存持久化错误写法——无脑用localStorage持久化整个 root stateconst storage window.localStorage const persistConfig { key: root, storage, }正确写法——保持默认的不持久化行为import { createApi, fetchBaseQuery } from reduxjs/toolkit/query/react const api createApi({ reducerPath: api, baseQuery: fetchBaseQuery({ baseUrl: /api/ }), endpoints: () ({}), })技能指南给出的判断是在浏览器中持久化 RTK Query 缓存常常让过期数据存留的时间超出用户预期请把持久化当作特殊情况处理而不是默认选项。这与 RTK Query服务端数据是新鲜度敏感的定位一致。如果确实需要为 SSR / 服务端做水合createApi提供了extractRehydrationInfo选项createApi 源码 里就有为 next-redux-wrapper 场景从HYDRATEaction 中提取reducerPath对应缓存数据的官方示例。更完整的取舍讨论见持久化与再水合文档。HIGH从组件里直接 patch 缓存错误写法——在useEffect中 dispatchupdateQueryDataimport { useEffect } from react import { useAppDispatch } from ../../app/hooks const dispatch useAppDispatch() useEffect(() { dispatch( api.util.updateQueryData(getPosts, undefined, (draft) { draft.push({ id: p3, title: Patched from component }) }), ) }, [dispatch])正确写法——把同样的逻辑放进 mutation 的onQueryStartedupdatePostTitle: build.mutationPost, PickPost, id | title({ query: ({ id, title }) ({ url: posts/${id}, method: PATCH, body: { title }, }), async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) { const patch dispatch( api.util.updateQueryData(getPosts, undefined, (draft) { const post draft.find((item) item.id id) if (post) { post.title title } }), ) try { await queryFulfilled } catch { patch.undo() } }, })技能指南的结论是组件层面的缓存 patch 会与真正应该拥有它的 mutation 生命周期脱节。从源码看updateQueryData的实现buildThunks.ts会通过produceWithPatches生成 patches 与 inversePatchesundo()则是把 inversePatches 通过patchQueryData派发回去。这套可回滚机制只有在生命周期里与queryFulfilled配对使用才发挥完整价值散落在组件里既无法绑定请求成败也难以追踪与测试。详见手动缓存更新文档。HIGH指望失效去重取未订阅的查询错误写法——先initiate再unsubscribe然后期待invalidateTags触发重取import { api } from ./api import { store } from ./store const subscription store.dispatch(api.endpoints.getPosts.initiate()) subscription.unsubscribe() store.dispatch(api.util.invalidateTags([Post]))正确写法——保持订阅存在失效才能触发重取import { api } from ./api import { store } from ./store store.dispatch(api.endpoints.getPosts.initiate()) store.dispatch(api.util.invalidateTags([Post]))技能指南的说明非常关键失效只会重取当前有活跃订阅的查询如果没有组件在使用那条缓存RTK Query 会直接丢弃它等下次真正需要时再重新请求。这与前文引用的失效规则完全一致——失效是按需重取不是后台刷新。想深入理解 tag 计算与失效调度的读者可以直接读自动化重取文档。源码视角失效与生命周期到底怎么跑到这里技能指南的全部核心内容已覆盖。为了让为什么这么写更加扎实下面用仓库源码把两条最重要的机制展开。tags 失效的调度invalidationByTags 中间件失效逻辑集中在packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts。从实现看它会监听三类 actionmutation 成功或rejectedWithValue携带 tags 的 thunk 结束→ 计算并执行invalidateTags任一 query/mutation 结束pending 计数递减显式派发的api.util.invalidateTagsaction。其中pendingRequestCount计数器invalidationByTags.ts是为了支持invalidationBehavior的delayed默认语义只有在所有查询与 mutation 都平静下来之后才真正执行失效从而把并发 mutation 的失效自动批量化。这个行为与 createApi 源码 中对invalidationBehavior: delayed | immediately的注释完全对应。对于无订阅者的缓存条目中间件走的是removeQueryResult分支移除结果这正好印证了失效不重取未订阅查询的规则。updateQueryData的可回滚补丁packages/toolkit/src/query/core/buildThunks.ts中updateQueryData的实现buildThunks.ts值得细读通过endpointDefinition.select(arg)拿到当前缓存条目若状态是STATUS_UNINITIALIZED直接返回空补丁集合对可 draft 的数据用produceWithPatches应用 recipe得到patches与inversePatches把 patches 通过patchQueryData派发落库返回的PatchCollection携带undo()即把 inversePatches 反向派发回去。这就是乐观更新示例中先改后回滚的底层支撑。而onQueryStarted/onCacheEntryAdded等生命周期 API 则分别由 queryLifecycle.ts 与 cacheLifecycle.ts 实现前者提供queryFulfilled这一请求完成/失败的 Promise后者面向缓存条目的长期存活如流式数据订阅。技能指南在端点生命周期参考文档中把常用端点选项归纳为providesTags声明该查询代表了哪些缓存条目invalidatesTags声明该 mutation 弄脏了哪些条目onQueryStarted把乐观/悲观更新绑定到具体请求onCacheEntryAdded长生命周期订阅例如流式数据keepUnusedDataFor非活跃缓存条目保留多久createApi默认值为 60 秒见 createApi.ts。落地清单把这套指南应用到真实项目把技能指南压缩成一张可直接执行的检查清单接入从reduxjs/toolkit/query/react导入createApi与fetchBaseQueryconfigureStore中同时挂载api.reducer与api.middlewarertk-query.mdx组织每个 base URL 只建一个createApi根按文件用api.injectEndpoints拆分端点createApi 文档失效优先用providesTags/invalidatesTags表达缓存依赖别急着手动 patchautomated-refetching.mdx更新乐观/悲观更新一律放进onQueryStarted失败时patch.undo()回滚manual-cache-updates.mdx持久化默认不要持久化浏览器端 API 缓存确有水合需求再借助extractRehydrationInfopersistence-and-rehydration.mdx模型匹配明确文档缓存的前提只有真正需要规范化图缓存时才引入其他方案endpoint-lifecycle.md。按照这份指南落地你的 RTK Query 代码会保持单一 API 根 tags 驱动失效 生命周期内更新的形态既正确又易于长期维护。仓库中可继续深入的材料还包括 RTK Query 的完整官方文档docs/rtk-query/目录以及核心实现源码packages/toolkit/src/query/目录前者给出全部配置项与用法后者提供逐行可读的底层机制。赞分享前端状态管理【免费下载链接】redux-toolkitThe official, opinionated, batteries-included toolset for efficient Redux development项目地址https://gitcode.com/gh_mirrors/re/redux-toolkit点击查看免费下载相关推荐Redux Toolkit RTK Query进阶分页、轮询、无限滚动与乐观更新的实战技巧Redux Toolkit RTK Query进阶分页、轮询、无限滚动与乐观更新的实战技巧 Redux Toolkit 是 Redux 官方的一体化开发工具包前端状态管理TanStack Query 之 Lit Query 缓存失效实战invalidateQueries 精确失效、后台重取与手动缓存更新TanStack Query 之 Lit Query 缓存失效实战invalidateQueries 精确失效、后台重取与手动缓存更新 失效是 TanStac前端缓存状态管理Redux Toolkit RTK Query 端点生命周期与缓存失效机制深度解析Redux Toolkit RTK Query 端点生命周期与缓存失效机制深度解析 RTK Query 是 Redux Toolkit 内置的服务器数据缓存层前端状态管理上一篇Drizzle ORM 0.29.5 新特性实战指南CTE 写操作、自定义迁移表与 SQLite Proxy 批量查询下一篇Lean 4开发者生产力工具链如何构建高效的形式化验证工作流创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表