React-Redux RTK Query 使用笔记
简介
RTK Query 是 Redux Toolkit 官方提供的数据获取和缓存工具,基于 Redux 构建但极大简化了异步数据管理。它将请求、缓存、状态管理封装在一个统一的 API 中。
核心优势:
- 自动缓存和去重
- 自动生成 React Hooks
- 支持乐观更新
- 内置轮询和重新获取机制
- 无需手动编写 slice、thunk、reducer
安装
npm install @reduxjs/toolkit react-redux
基础配置
1. 创建 API Slice
// src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// 定义数据类型
export interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// 创建 API
export const api = createApi({
// 配置基础请求
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
// 可添加通用 headers
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
// 全局标签类型(用于缓存失效)
tagTypes: ['Post', 'User'],
// 定义端点
endpoints: (builder) => ({
// 查询端点(GET)
getPosts: builder.query<Post[], void>({
query: () => '/posts',
providesTags: ['Post'],
}),
getPostById: builder.query<Post, number>({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
// 修改端点(POST/PUT/DELETE)
addPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({
url: '/posts',
method: 'POST',
body,
}),
invalidatesTags: ['Post'],
}),
updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: patch,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
deletePost: builder.mutation<void, number>({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
}),
}),
});
// 自动生成 hooks
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useAddPostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = api;
2. 配置 Store
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { api } from '../services/api';
import authReducer from './authSlice';
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
auth: authReducer,
},
// 添加 API middleware 用于缓存、乐观更新、轮询等功能
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
// 启用 refetchOnFocus 和 refetchOnReconnect
setupListeners(store.dispatch);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
在组件中使用
查询数据(Query)
// 基础用法
import { useGetPostsQuery, useGetPostByIdQuery } from '../services/api';
function PostsList() {
const { data: posts, error, isLoading, isFetching, refetch } = useGetPostsQuery();
if (isLoading) return <div>加载中...</div>;
if (error) return <div>出错了</div>;
return (
<div>
<button onClick={refetch}>刷新</button>
{posts?.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
// 带参数的查询 + 配置选项
function PostDetail({ postId }: { postId: number }) {
const { data: post } = useGetPostByIdQuery(postId, {
// 组件挂载时是否跳过请求
skip: !postId,
// 轮询间隔(毫秒)
pollingInterval: 3000,
// 组件重新挂载时是否重新获取
refetchOnMountOrArgChange: true,
// 焦点回归时是否重新获取
refetchOnFocus: true,
// 网络重连时是否重新获取
refetchOnReconnect: true,
});
return <div>{post?.title}</div>;
}
修改数据(Mutation)
function AddPost() {
const [addPost, { isLoading }] = useAddPostMutation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await addPost({
title: '新文章',
body: '内容',
userId: 1
}).unwrap();
console.log('添加成功:', result);
} catch (error) {
console.error('添加失败:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<button disabled={isLoading}>
{isLoading ? '提交中...' : '添加'}
</button>
</form>
);
}
// 更新操作
function EditPost({ post }: { post: Post }) {
const [updatePost] = useUpdatePostMutation();
const handleUpdate = async () => {
await updatePost({ ...post, title: '更新后的标题' });
};
return <button onClick={handleUpdate}>更新</button>;
}
高级特性
乐观更新
updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: patch,
}),
// 发起请求前执行(乐观更新)
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// 手动更新缓存
const patchResult = dispatch(
api.util.updateQueryData('getPostById', id, (draft) => {
Object.assign(draft, patch);
})
);
try {
await queryFulfilled;
} catch {
// 请求失败时回滚
patchResult.undo();
}
},
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
条件获取与懒加载
// useLazyQuery - 手动触发查询
function SearchPosts() {
const [trigger, { data, isFetching }] = api.useLazyGetPostsQuery();
const handleSearch = (keyword: string) => {
trigger(); // 手动触发
};
return (
<div>
<button onClick={() => handleSearch('react')}>搜索</button>
{isFetching ? '搜索中...' : data?.map(...)}
</div>
);
}
查询参数序列化
getPostsByPage: builder.query<Post[], { page: number; limit: number }>({
query: ({ page, limit }) => `/posts?_page=${page}&_limit=${limit}`,
// 自定义序列化(影响缓存 key)
serializeQueryArgs: ({ endpointName, queryArgs }) => {
return `${endpointName}(${JSON.stringify(queryArgs)})`;
},
// 合并分页数据(而非替换)
merge: (currentCache, newItems) => {
currentCache.push(...newItems);
},
// 强制重新获取的条件
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg;
},
}),
轮询(Polling)
function LiveData() {
// 每 5 秒自动刷新
const { data } = useGetPostsQuery(undefined, {
pollingInterval: 5000,
// 条件轮询:只在特定条件下轮询
skipPollingIfUnfocused: true,
});
return <div>{JSON.stringify(data)}</div>;
}
// 程序化控制轮询
function ControlledPolling() {
const [pollingInterval, setPollingInterval] = useState(0);
const { data } = useGetPostsQuery(undefined, { pollingInterval });
return (
<div>
<button onClick={() => setPollingInterval(5000)}>开始轮询</button>
<button onClick={() => setPollingInterval(0)}>停止轮询</button>
</div>
);
}
缓存标签系统(Tags)
标签系统是实现自动缓存失效的核心:
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Post', 'User'],
endpoints: (build) => ({
// 提供通用标签
getPosts: build.query<Post[], void>({
query: () => '/posts',
providesTags: ['Post'],
}),
// 提供具体 ID 标签
getPost: build.query<Post, number>({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
// 修改时失效特定标签
updatePost: build.mutation<void, Post>({
query: (post) => ({
url: `/posts/${post.id}`,
method: 'PUT',
body: post,
}),
// 失效所有 Post 标签和特定 ID 的 Post
invalidatesTags: (result, error, post) => [
'Post',
{ type: 'Post', id: post.id },
],
}),
// 批量操作失效多个标签
bulkDelete: build.mutation<void, number[]>({
query: (ids) => ({
url: '/posts/bulk-delete',
method: 'POST',
body: { ids },
}),
invalidatesTags: (result, error, ids) => [
...ids.map((id) => ({ type: 'Post' as const, id })),
'User', // 可能关联用户数据也需要刷新
],
}),
}),
});
错误处理
function PostComponent() {
const { data, error, isError } = useGetPostByIdQuery(1);
// 方式 1:类型守卫
if (isError) {
if ('status' in error) {
// FetchBaseQueryError
const errMsg = 'error' in error ? error.error : JSON.stringify(error.data);
return <div>错误: {errMsg}</div>;
} else {
// SerializedError
return <div>错误: {error.message}</div>;
}
}
return <div>{data?.title}</div>;
}
// 全局错误处理(在 API 配置中)
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
prepareHeaders: (headers) => headers,
}),
endpoints: () => ({}),
// 自定义错误处理
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === REHYDRATE) {
return action.payload[reducerPath];
}
},
});
最佳实践
1. 代码分割与注入
// 主 API 文件
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
export const emptySplitApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: () => ({}),
});
// 特性模块中注入端点
// postsApi.ts
import { emptySplitApi } from './api';
const postsApi = emptySplitApi.injectEndpoints({
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
query: () => '/posts',
}),
}),
overrideExisting: false,
});
export const { useGetPostsQuery } = postsApi;
2. 自定义 BaseQuery(添加拦截器)
import { fetchBaseQuery } from '@reduxjs/toolkit/query';
import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query';
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers) => {
headers.set('X-Client-Version', '1.0.0');
return headers;
},
});
const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
if (result.error && result.error.status === 401) {
// 尝试刷新 token
const refreshResult = await baseQuery('/refresh-token', api, extraOptions);
if (refreshResult.data) {
// 重试原请求
result = await baseQuery(args, api, extraOptions);
} else {
// 登出处理
api.dispatch(logout());
}
}
return result;
};
export const api = createApi({
baseQuery: baseQueryWithReauth,
endpoints: () => ({}),
});
3. SSR 支持(Next.js)
// 在服务端预获取数据
import { api } from './api';
import { store } from './store';
export async function getServerSideProps() {
store.dispatch(api.endpoints.getPosts.initiate());
await Promise.all(store.dispatch(api.util.getRunningQueriesThunk()));
return {
props: {
initialReduxState: store.getState(),
},
};
}
与 React Query 对比
| 特性 | RTK Query | React Query |
|---|---|---|
| 状态管理 | 基于 Redux | 独立状态管理 |
| DevTools | Redux DevTools 集成 | 专用 DevTools |
| 包体积 | 适合 Redux 项目(已使用 Redux) | 独立,体积更小 |
| 缓存策略 | 标签系统 | 基于 queryKey |
| 乐观更新 | 内置 | 手动配置 |
| 最佳场景 | Redux 生态项目 | 轻量级项目 |
总结
RTK Query 是 Redux 生态中最强大的数据获取解决方案,特别适合:
- 已有 Redux 项目:无需引入额外状态管理库
- 复杂缓存需求:标签系统精确控制缓存失效
- 团队协作:强类型支持和标准化结构
掌握其核心概念(Query/Mutation/Tags)后,可大幅减少样板代码,专注于业务逻辑开发。
评论