# react-reduxjs-toolkit
**Repository Path**: k_2021/react-reduxjs-toolkit
## Basic Information
- **Project Name**: react-reduxjs-toolkit
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: dev1
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 4
- **Forks**: 2
- **Created**: 2021-08-17
- **Last Updated**: 2023-08-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
##
使用 rtk-query 优化你的数据请求
## 一、目前前端常见的发起`ajax`请求的方式
- 1、使用原生的`ajax`请求
- 2、使用`jquery`封装好的`ajax`请求
- 3、使用`fetch`发起请求
- 4、第三方的比如`axios`请求
- 5、`angular`中自带的`HttpClient`
就目前前端框架开发中来说我们在开发`vue`、`react`的时候一般都是使用`fetch`或`axios`自己封装一层来与后端数据交互,至于`angular`肯定是用自带的`HttpClient`请求方式,但是依然存在几个致命的弱点,
- 1、对当前请求数据不能缓存,
- 2、一个页面上由多个组件组成,但是刚好有遇到复用相同组件的时候,那么就会发起多次`ajax`请求
> 📢 针对同一个接口发起多次请求的解决方法,目前常见的解决方案
- 1、使用`axios`的取消发起请求,[参考文档](http://www.axios-js.com/zh-cn/docs/#%E5%8F%96%E6%B6%88)
- 2、`vue`中还没看到比较好的方法
- 3、在`rect`中可以借用类似[react-query](https://react-query.tanstack.com/)工具对请求包装一层
- 4、对于`angular`中直接使用`rxjs`的操作符`shareReplay`
## 二、`rtk-query`的介绍
`rtk-query`是[`redux-toolkit`](https://redux-toolkit.js.org/)里面的一个分之,专门用来优化前端接口请求,目前也只支持在`react`中使用,本文章不去介绍如何在`redux-toolkit`的使用方式,我相信在网上也能陆续的搜索到对应的资料,但是对于`rtk-query`的除了官网,几乎是没有的,有也是一些残卷,简单的`demo`使用,并不能适用于企业实际项目开发中,本人在项目中使用`redux-toolkit`,`axios`,`react-query`的基础上优化实际项目中,看到官网上有`rtk-query`的介绍,经过一段时间的研究和实际项目中使用逐渐取代了项目中的`axios`和`react-query`
> 📢 `rtk-query`的使用环境,必须是`react`版本大于 17,可以使用`hooks`的版本,因为使用`rtk-query`的查询都是`hooks`的方式,如果你项目简单`redux`都未使用到,本人不建议你用`rtk-query`,可能直接使用`axios`请求更加的简单方便。
在`rtk-query`中我们可以使用中间件和拦截器优雅的处理异常信息,使用代码拆分将不同类型的接口拆分到不同的模块下
## 三、环境的搭建
- 1、使用脚手架创建一个`typescript`的工程
```properties
npx create-react-app react-reduxjs-toolkit --template typescript
```
- 2、安装依赖包
```properties
npm install @reduxjs/toolkit react-redux
```
- 3、创建`store`文件夹来存放状态管理:`src/store`
```shell
➜ store git:(dev2) ✗ tree .
.
├── api # 接口请求的
│ ├── base.ts # 基础的
│ └── posts.ts # 帖子的接口
├── hooks.ts # 自定义hooks优化在组件中使用的时候不能联想出来
├── index.ts
└── store.ts
1 directory, 5 files
```
- 4、`base.ts`中提供拆分代码的基础服务,[参考文档](https://redux-toolkit.js.org/rtk-query/usage/code-splitting)
```typescript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const baseApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),
reducerPath: 'baseApi',
// 缓存,默认时间是秒,默认时长60秒
keepUnusedDataFor: 5 * 60,
refetchOnMountOrArgChange: 30 * 60,
endpoints: () => ({}),
});
```
- 5、在`posts.ts`文件中是关于帖子的一切请求,如果是用户的请求,我们可以同理创建一个`user.ts`的文件
```typescript
//React entry point 会自动根据endpoints生成hooks
import { baseApi } from './base';
interface IPostVo {
id: number;
name: string;
}
//使用base URL 和endpoints 定义服务
const postsApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
// 查询列表
getPostsList: builder.query, void>({
query: () => '/posts',
transformResponse: (response: { data: Promise }) => {
return response.data;
},
}),
// 根据id去查询,第一个参数是返回值的类型,第二个参是传递给后端的数据类型
getPostsById: builder.query<{ id: number; name: string }, number>({
query: (id: number) => `/posts/${id}`,
}),
// 创建帖子
createPosts: builder.mutation({
query: (data) => ({
url: '/posts',
method: 'post',
body: data,
}),
}),
// 根据id删除帖子
deletePostById: builder.mutation({
query: (id: number) => ({
url: `/posts/${id}`,
method: 'delete',
}),
}),
// 根据id修改帖子
modifyPostById: builder.mutation({
query: ({ id, data }: { id: number; data: any }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: data,
}),
}),
}),
overrideExisting: false,
});
//导出可在函数式组件使用的hooks,它是基于定义的endpoints自动生成的
export const {
useGetPostsListQuery,
useGetPostsByIdQuery,
useCreatePostsMutation,
useDeletePostByIdMutation,
useModifyPostByIdMutation,
// 惰性的查询
useLazyGetPostsListQuery,
useLazyGetPostsByIdQuery,
} = postsApi;
export default postsApi;
```
- 6、`store.ts`文件中对数据的组合
```typescript
import {
configureStore,
combineReducers,
Dispatch,
AnyAction,
} from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/dist/query/react';
import { baseApi } from './api/base';
const rootReducer = combineReducers({
[baseApi.reducerPath]: baseApi.reducer,
});
// 中间件集合
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [...getDefaultMiddleware()];
return middlewareList;
};
//API slice会包含自动生成的redux reducer和一个自定义中间件
export const rootStore = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
middlewareHandler(getDefaultMiddleware),
});
export type RootState = ReturnType;
setupListeners(rootStore.dispatch);
```
- 7、在`src/index.ts`中使用`store`仓库
```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { rootStore } from './store';
ReactDOM.render(
,
document.getElementById('root')
);
reportWebVitals();
```
- 8、在`app.tsx`在组建中使用
```typescript
import { useEffect } from 'react';
import {
useGetPostsListQuery,
useLazyGetPostsListQuery,
} from './store/api/posts.service';
import { useDispatch } from 'react-redux';
import { postsSlice } from './store/slice/post.slice';
// Test组件中依旧使用useGetPostsListQuery()方法,可以查看到两个组件中都成功获取到数据,但是发起请求只有一次
import { Test } from './Test';
function App() {
// 主动拉取数据
const { data: postList } = useGetPostsListQuery();
console.log(postList, 'app组件组件中');
// 惰性拉取数据
const [trigger, { data }] = useLazyGetPostsListQuery();
const postsListHandler = () => {
trigger();
};
useEffect(() => {
if (data) {
console.log(data, '接收到的数据');
}
// eslint-disable-next-line
}, [data]);
return (
);
}
export default App;
```
- 9、测试这样就简单实现了通过代码拆分优化请求的方式来请求后端接口,细节的问题可以继续查阅文档
## 四、中间件的使用
#### 日志中间件的使用
- 1、日志中间件的使用,我们在开发环境的时候要使用日志中间件,便于观察`redux`状态的变动
```properties
npm install redux-logger
```
- 2、在`src/store/store.ts`中配置日志中间件
```typescript
import logger from 'redux-logger';
...
// 中间件集合
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [
...getDefaultMiddleware(),
];
if (process.env.NODE_ENV === 'development') {
middlewareList.push(logger);
}
return middlewareList;
};
```
#### 错误中间件
- 1、[官网地址](https://redux-toolkit.js.org/rtk-query/usage/error-handling#handling-errors-at-a-macro-level)
- 2、我们在中间件可以处理后端抛出的错误比如 403、500 等错误信息
- 3、配置错误中间件
```typescript
import {
MiddlewareAPI,
isRejectedWithValue,
Middleware,
} from '@reduxjs/toolkit';
// 错误中间件
export const rtkQueryErrorLogger: Middleware =
(api: MiddlewareAPI) => (next: Dispatch) => (action: any) => {
console.log(action, '中间件中非错误的时候', api);
// 只能拦截不是200的时候
if (isRejectedWithValue(action)) {
console.log(action, '中间件');
// console.log(action.error.data.message, '错误信息');
console.warn(action.payload.status, '当前的状态');
console.warn(action.payload.data?.message, '错误信息');
console.warn('中间件拦截了');
// TODO 自己实现错误提示给页面上
}
return next(action);
};
// 中间件集合
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [rtkQueryErrorLogger, ...getDefaultMiddleware()];
if (process.env.NODE_ENV === 'development') {
middlewareList.push(logger);
}
return middlewareList;
};
```
## 五、拦截器的使用
上面的中间件是可以处理接口的错误请求,但是实际上常见的`httpstatus`并不能满足我们实际业务开发,后端开发也一般只要到了后端就返回`httpstatus=200`,然后自定义`code`的状态码来反馈错误信息,这时候拦截器就发挥他的作用了
- 1、[参考文档](https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#automatic-re-authorization-by-extending-fetchbasequery)
- 2、改造项目中的`src/store/base.ts`的文件,加入拦截器的方式
```typescript
import { QueryReturnValue } from '@reduxjs/toolkit/dist/query/baseQueryTypes';
import {
BaseQueryFn,
createApi,
FetchArgs,
fetchBaseQuery,
FetchBaseQueryError,
FetchBaseQueryMeta,
} from '@reduxjs/toolkit/query/react';
// 定义拦截器
const baseQuery = fetchBaseQuery({
baseUrl: 'http://localhost:5000/',
});
const baseQueryWithIntercept: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
const result: QueryReturnValue<
any,
FetchBaseQueryError,
FetchBaseQueryMeta
> = await baseQuery(args, api, extraOptions);
console.log(result, '拦截器');
const { data, error } = result;
// 如果遇到错误的时候
if (error) {
const { status } = error as FetchBaseQueryError;
const { request } = meta as FetchBaseQueryMeta;
const url: string = request.url;
// 根据状态来处理错误
printHttpError(Number(status), url);
// TODO 自己处理错误信息提示给前端
}
if (Object.is(data?.code, 0)) {
return result;
}
throw new Error(data.message);
};
export const baseApi = createApi({
baseQuery: baseQueryWithIntercept, //fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),
reducerPath: 'baseApi',
// 缓存时间,以秒为单位,默认是60秒
keepUnusedDataFor: 2 * 60,
// refetchOnMountOrArgChange: 30 * 60,
endpoints: () => ({}),
});
```
- 3、`printHttpError`方法打印错`httpStatus`的错误信息,自己继续完善
```typescript
/**
* 打印http请求错误的时候
* @param httpStatus
* @param path
*/
export const printHttpError = (httpStatus: number, path: string): void => {
switch (httpStatus) {
case 400:
console.log(`错误的请求:${path}`);
break;
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
case 401:
console.log('你没有登录,请先登录');
window.location.reload();
break;
// 跳转登录页面
case 403:
console.log('登录过期,请重新登录');
// 清除全部的缓存数据
window.localStorage.clear();
window.location.reload();
break;
// 404请求不存在
case 404:
console.log('网络请求不存在');
break;
// 其他错误,直接抛出错误提示
default:
console.log('我也不知道是什么错误');
break;
}
};
```
- 4、处理后端返回`httpStatus=200`的时候根据`code`来判断异常的情况
```typescript
export const fetchWithIntercept: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
const result: QueryReturnValue<
any,
FetchBaseQueryError,
FetchBaseQueryMeta
> = await baseQuery(args, api, extraOptions);
console.log(result, '拦截器');
const { data, error, meta } = result;
const { request } = meta as FetchBaseQueryMeta;
const url: string = request.url;
// 如果遇到httpStatus!=200-300错误的时候
if (error) {
const { status } = error as FetchBaseQueryError;
// 根据状态来处理错误
printHttpError(Number(status), url);
}
// 正确的时候,根据各自后端约定来写的
if (Object.is(data?.code, 0)) {
return result;
} else {
// TODO 打印提示信息
printPanel({ method: request.method, url: request.url });
// TODO 根据后端返回的错误提示到组件中,直接这里弹框提示也可以
return Promise.reject('错误信息');
}
};
```
* 5、注意点,使用了拦截器后中间件就失效,具体原因在文档上还没找到说明
## 六、结合数据持久化插件将请求的数据持久化到本地
* 1、安装依赖包
```properties
npm install redux-persist
```
* 2、修改`src/store/store.ts`文件
```typescript
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const persistConfig = {
key: 'root',
storage,
};
const rootReducer = combineReducers({
[baseApi.reducerPath]: baseApi.reducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
...
export const rootStore = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => middlewareHandler(getDefaultMiddleware),
});
export const persistor = persistStore(rootStore);
export type RootState = ReturnType;
```
* 3、修改根目录下的`index.tsx`文件
```typescript
import { rootStore, persistor } from './store';
ReactDOM.render(
,
document.getElementById('root')
);
```
* 4、刷新浏览器查看是否在本地存储中有数据

在这里`baseApi`其实没一点用途的,如果要持久化数据还需要手动来创建切片,这时候就使用到了`@reduxjs/toolkit`的知识点
* 5、一份完整的`store.ts`文件
```typescript
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import logger from 'redux-logger';
import { setupListeners } from '@reduxjs/toolkit/dist/query/react';
import { baseApi } from './api/base.service';
import { postsSlice } from './slice/post.slice';
const persistConfig = {
key: 'root',
storage,
};
const rootReducer = combineReducers({
[baseApi.reducerPath]: baseApi.reducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
// 中间件集合
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [
...getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST'],
},
}),
];
if (process.env.NODE_ENV === 'development') {
middlewareList.push(logger);
}
return middlewareList;
};
//API slice会包含自动生成的redux reducer和一个自定义中间件
export const rootStore = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => middlewareHandler(getDefaultMiddleware),
});
export const persistor = persistStore(rootStore);
export type RootState = ReturnType;
setupListeners(rootStore.dispatch);
```
## 六、使用切片的方式来实现将请求的数据存储到本地中
* 1、创建文件`store/slice/posts.ts`文件
```typescript
import { createSlice } from '@reduxjs/toolkit';
import { IPostVo } from '../api/posts.service';
interface PostsState {
/**后端数据返回的 */
postList: IPostVo[];
}
const initialState: PostsState = {
postList: [],
};
export const postsSlice = createSlice({
name: 'Posts',
initialState,
reducers: {
clearPosts: (state: PostsState) => {
state.postList = [];
},
setPosts: (state: PostsState, action) => {
state.postList = action.payload;
},
},
extraReducers: {},
});
```
* 2、在`store.ts`中配置切片
```typescript
import { postsSlice } from './slice/post.slice';
...
const rootReducer = combineReducers({
[baseApi.reducerPath]: baseApi.reducer,
// 自定义要存储的数据
posts: postsSlice.reducer,
});
```
* 3、在组件中将请求回来的数据存储到本地中
```typescript
import { useDispatch } from 'react-redux';
const [trigger, { error, data }] = useLazyGetPostsListQuery();
useEffect(() => {
if (data) {
console.log(data, '接收到的数据');
dispatch(postsSlice.actions.setPosts(data));
}
// eslint-disable-next-line
}, [data]);
```
* 4、获取数据后重新查看浏览器
* 5、如果是在别的组件中要使用持久化的数据直接使用
```typescript
import { RootState, useSelector } from 'src/store';
const postsList: IPostVo[] =
useSelector((state: RootState) => state.posts.postsList) ?? [];
```
* 6、📢点,这里的`useSelector`要使用我们自定义的,在`store/hooks.ts`文件中
```typescript
import {
useSelector as useReduxSelector,
TypedUseSelectorHook,
} from 'react-redux';
import { RootState } from './store';
export const useSelector: TypedUseSelectorHook = useReduxSelector;
```
## 七、给请求添加请求头
* 1、在`base.ts`中配置请求头
```typescript
const baseUrl: string = process.env.REACT_APP_BASE_API_URL as string;
const baseQuery = fetchBaseQuery({
baseUrl,
prepareHeaders: (headers) => {
headers.set('x-origin', 'admin-web');
const token: string = storage.getItem(authToken);
if (token) {
headers.set(authToken, token);
}
return headers;
},
});
```
## 八、[参考代码](https://gitee.com/k_2021/react-reduxjs-toolkit)