将 RTK Query 接入 Redux Toolkit:createApi、Store 集成、缓存失效与乐观更新的实战指南
2026/9/23 13:11:53 网站建设 项目流程
  • 前端
  • 状态管理

【免费下载链接】redux-toolkit

The 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/response)API;
  • 文档缓存能够满足业务需要;
  • tags 失效与端点生命周期足以解决问题。

需要另选工具的场景:

  • 真实需求是规范化图缓存(如大量实体间互相引用、需要全局归一化);
  • 技术栈里已经存在更合适的领域特定规范化客户端。

如果规范化缓存是硬性要求且栈内没有更好的库,那么"slice + thunk"的经典流程可以作为兜底方案。这个判断直接决定了后面的架构选择,属于动手前的第一步。

Setup:从零接入 RTK Query

技能指南给出了一个完整的接入示例,涵盖 API 定义(src/services/api.ts)、Store 集成(src/app/store.ts)与 React 消费(src/App.tsx)三部分。核心依赖是从@reduxjs/toolkit/query/react导入的createApifetchBaseQuery

// 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.query<Post[], void>({ query: () => 'posts', providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), 'Post'] : ['Post'], }), addPost: build.mutation<Post, Pick<Post, 'title'>>({ query: (body) => ({ url: 'posts', method: 'POST', body, }), invalidatesTags: ['Post'], }), }), }) export const { useGetPostsQuery, useAddPostMutation } = api

这个createApi调用使用了四个关键选项:

  • reducerPath:API 切片在 Store 中挂载的 key,默认值为'api'。根据 createApi 源码 的注释,如果应用里多次调用createApi,每次都必须提供唯一值;
  • baseQuery:每个端点默认使用的请求函数,fetchBaseQuery是 RTK Query 导出的、对原生fetch的轻量封装,baseUrl用于拼接相对路径;
  • tagTypes:声明的标签类型数组,用于后续providesTags/invalidatesTags的缓存与失效(源码注释见 createApi.ts);
  • endpoints:使用 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-redux,Store 中必须同时注册api.reducerapi.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.query<Post[], void>({ query: () => 'posts', providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), 'Post'] : ['Post'], }), updatePost: build.mutation<Post, Pick<Post, '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.updateQueryDataqueryFulfilled实现"先改 UI、失败回滚":

type Post = { id: string; title: string } export const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), tagTypes: ['Post'], endpoints: (build) => ({ getPosts: build.query<Post[], void>({ query: () => 'posts', providesTags: ['Post'], }), updatePostTitle: build.mutation<Post, Pick<Post, '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() } }, }), }), })

这段代码的语义非常清晰:

  1. dispatch(api.util.updateQueryData(...))用 Immer 风格的 recipe 直接修改缓存中的getPosts结果;
  2. 返回的patch对象携带patchesinversePatchesundo()
  3. await queryFulfilled等待请求真正完成;一旦失败进入catch,调用patch.undo()把缓存还原成修改前的样子。

技能指南的总结是:让乐观更新与悲观更新都待在端点生命周期处理器内,使它们与对应请求保持耦合。这也正是"常见错误"里"从组件里 patch 缓存"一节的对照标准。

常见错误与正确姿势

技能指南把常见错误按严重程度分为 CRITICAL / HIGH / MEDIUM 三档,下面逐条给出"错误写法"与"正确写法"的对照。

CRITICAL:为一个后端创建多个 API slice

错误写法——两个createApi根共享同一baseQuery和同一个reducerPath: 'api'

import { 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根 +injectEndpoints

import { 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.query<User[], void>({ query: () => 'users' }), }), })

技能指南给出的结论是:每个 base URL 只保留一个 API slice,这样能保住失效行为的正确性,也避免重复的 middleware 工作。从源码上看,tags 的提供关系(provided-by 映射)是挂在 reducerPath 下的内部状态里统一维护的,多个根各自维护一套映射,跨 slice 的失效根本无法互相感知,失效语义必然被破坏。相关讨论源见 createApi 文档。

HIGH:忘记注册api.reducerapi.middleware

错误写法——只配置了空的 reducer:

import { configureStore } from '@reduxjs/toolkit' const store = configureStore({ reducer: {}, })

正确写法——同时挂载 reducer 与 middleware:

import { 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 负责监听initiatefulfilledrejected等 thunk action 并触发失效与重取。缺失任何一半,hooks 要么拿不到状态,要么请求发出后缓存永远不更新。相关教程见 rtk-query.mdx。

MEDIUM:默认把浏览器里的 API 缓存持久化

错误写法——无脑用localStorage持久化整个 root state:

const 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中 dispatchupdateQueryData

import { 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 的onQueryStarted

updatePostTitle: build.mutation<Post, Pick<Post, '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 与 inversePatches,undo()则是把 inversePatches 通过patchQueryData派发回去。这套"可回滚"机制只有在生命周期里与queryFulfilled配对使用才发挥完整价值;散落在组件里既无法绑定请求成败,也难以追踪与测试。详见手动缓存更新文档。

HIGH:指望失效去重取未订阅的查询

错误写法——先initiateunsubscribe,然后期待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。从实现看,它会监听三类 action:

  • mutation 成功或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.tsupdateQueryData的实现(buildThunks.ts)值得细读:

  • 通过endpointDefinition.select(arg)拿到当前缓存条目;若状态是STATUS_UNINITIALIZED,直接返回空补丁集合;
  • 对可 draft 的数据用produceWithPatches应用 recipe,得到patchesinversePatches
  • 把 patches 通过patchQueryData派发落库;
  • 返回的PatchCollection携带undo(),即把 inversePatches 反向派发回去。

这就是乐观更新示例中"先改后回滚"的底层支撑。而onQueryStarted/onCacheEntryAdded等生命周期 API 则分别由 queryLifecycle.ts 与 cacheLifecycle.ts 实现:前者提供queryFulfilled这一"请求完成/失败"的 Promise,后者面向缓存条目的长期存活(如流式数据订阅)。技能指南在端点生命周期参考文档中把常用端点选项归纳为:

  • providesTags:声明该查询代表了哪些缓存条目;
  • invalidatesTags:声明该 mutation 弄脏了哪些条目;
  • onQueryStarted:把乐观/悲观更新绑定到具体请求;
  • onCacheEntryAdded:长生命周期订阅,例如流式数据;
  • keepUnusedDataFor:非活跃缓存条目保留多久(createApi默认值为 60 秒,见 createApi.ts)。

落地清单:把这套指南应用到真实项目

把技能指南压缩成一张可直接执行的检查清单:

  1. 接入:从@reduxjs/toolkit/query/react导入createApifetchBaseQueryconfigureStore中同时挂载api.reducerapi.middleware(rtk-query.mdx);
  2. 组织:每个 base URL 只建一个createApi根,按文件用api.injectEndpoints拆分端点(createApi 文档);
  3. 失效:优先用providesTags/invalidatesTags表达缓存依赖,别急着手动 patch(automated-refetching.mdx);
  4. 更新:乐观/悲观更新一律放进onQueryStarted,失败时patch.undo()回滚(manual-cache-updates.mdx);
  5. 持久化:默认不要持久化浏览器端 API 缓存,确有水合需求再借助extractRehydrationInfo(persistence-and-rehydration.mdx);
  6. 模型匹配:明确"文档缓存"的前提,只有真正需要规范化图缓存时才引入其他方案(endpoint-lifecycle.md)。

按照这份指南落地,你的 RTK Query 代码会保持"单一 API 根 + tags 驱动失效 + 生命周期内更新"的形态,既正确又易于长期维护。仓库中可继续深入的材料还包括 RTK Query 的完整官方文档(docs/rtk-query/目录)以及核心实现源码(packages/toolkit/src/query/目录),前者给出全部配置项与用法,后者提供逐行可读的底层机制。

  • 前端
  • 状态管理

【免费下载链接】redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

项目地址:https://gitcode.com/gh_mirrors/re/redux-toolkit
点击查看免费下载
上一篇:Drizzle ORM 0.29.5 新特性实战指南:CTE 写操作、自定义迁移表与 SQLite Proxy 批量查询
下一篇:Lean 4开发者生产力工具链:如何构建高效的形式化验证工作流?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询