Jansiel Notes

Redux持久化最佳实践

前言

随着 React 的不断发展,相关的集中式状态的优质解决方案逐渐增多,如: Zustand、一系列 Redux 中间件,今天本专栏将介绍当今Redux生态中最优雅的 React 统一状态解决方案,基于 Redux-ToolkitRedux_Persist 实现

Redux-Toolkit简介

Redux Toolkit 是 Redux 官方强烈推荐,开箱即用的一个高效的 Redux 开发工具集

  • 它旨在成为标准的 Redux 逻辑开发模式,我们强烈建议你使用它。
  • 它包括几个实用程序功能,这些功能可以简化最常见场景下的 Redux 开发,包括配置 store、定义 reducer,不可变的更新逻辑、甚至可以立即创建整个状态的 “切片 slice”,而无需手动编写任何 action creator 或者 action type。
  • 它还自带了一些最常用的 Redux 插件,例如用于异步逻辑 Redux Thunk,用于编写选择器 selector 的函数 Reselect ,你都可以立刻使用。

实践基础准备

其实就是下载对应的依赖

1 pnpm add antd --save
2 pnpm add redux --save
3 pnpm add react-redux --save
4 pnpm add redux-toolkit --save
5 pnpm add redux-persist --save
6

Reducer的开发

代码里面都有详细的注释,结合注释对于了解React与Redux的完全可以看懂

1.Book 书籍reducer

在modules文件夹中的features文件夹中编写一个文件名是: bookSlice.tsx

 1 import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
 2 ​
 3 export interface CounterState {
 4     value: number;
 5     bookList: Array<object>;
 6 }
 7 const initialState: CounterState = {
 8     value: 100,
 9     bookList: [
10         {
11             title: "西游记",
12         },
13         {
14             title: "水浒传",
15         },
16         {
17             title: "红楼梦",
18         },
19 ​
20         {
21             title: "三国演义",
22         },
23     ],
24 };
25 ​
26 const getBookListApi = () =>
27     fetch(
28         "https://mock.presstime.cn/mock/653695baffb279f23e01cefb/remoteBook"
29     ).then((res) => {
30         console.log(res);
31 ​
32         return res.json();
33     });
34 ​
35 // thunk函数允许执行异步逻辑, 通常用于发出异步请求。
36 // createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态:
37 // pending(进行中)、fulfilled(成功)、rejected(失败)
38 export const getRemoteBookData = createAsyncThunk("book/getBook", async () => {
39     const res = await getBookListApi();
40     console.log(res);
41 ​
42     return res;
43 });
44 ​
45 // 创建一个 Slice
46 export const BookSlice = createSlice({
47     name: "bookData",
48     initialState,
49     // 定义 reducers 并生成关联的操作
50     reducers: {
51         // 定义一个加的方法
52         addingBook: (state, { payload, type }) => {
53             console.log(type); //bookData/addingBook
54             console.log(payload.value); //{title:"传来的值"}
55             const allList = JSON.parse(JSON.stringify(state)).bookList; //必须要重新深拷贝一次!!!
56 ​
57             allList.push(payload.value);
58 ​
59             state.bookList = allList;
60         },
61     },
62     // extraReducers 字段让 slice 处理在别处定义的 actions,
63     // 包括由 createAsyncThunk 或其他slice生成的actions。
64     //说白了就是判断action函数在不同状态下做什么不同的逻辑
65     extraReducers(builder) {
66         builder
67             .addCase(getRemoteBookData.pending, () => {
68                 console.log("⚡ ~ 正在获取用户列表信息!");
69             })
70             .addCase(getRemoteBookData.fulfilled, (state, { payload }) => {
71                 console.log("⚡ ~ 获取远程用户列表成功", payload);
72                 const allList = JSON.parse(JSON.stringify(state)).bookList; //必须要重新深拷贝一次!!!
73                 state.bookList = allList.concat(payload.bookList);
74             })
75             .addCase(getRemoteBookData.rejected, (_, err) => {
76                 console.log("⚡ ~ 获取远程用户列表失败", err);
77             });
78     },
79 });
80 // 导出加减的方法
81 export const { addingBook } = BookSlice.actions;
82 ​
83 // 默认导出
84 export default BookSlice.reducer;
85 ​
86

2.User 用户reducer

在modules文件夹中的features文件夹中编写一个文件名是: userSlice.tsx

 1 import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
 2 ​
 3 export interface MovieState {
 4     list: object;
 5     userList: Array<object>;
 6 }
 7 const initialState: MovieState = {
 8     value: 100,
 9     userList: [
10         {
11             title: "张明明",
12         },
13         {
14             title: "李来",
15         },
16         {
17             title: "魏韩雪",
18         },
19     ],
20 };
21 ​
22 const getUserListApi = () =>
23     fetch(
24         "https://mock.presstime.cn/mock/653695baffb279f23e01cefb/remoteUser"
25     ).then((res) => {
26         console.log(res);
27 ​
28         return res.json();
29     });
30 ​
31 // thunk函数允许执行异步逻辑, 通常用于发出异步请求。
32 // createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态:
33 // pending(进行中)、fulfilled(成功)、rejected(失败)
34 export const getRemoteUserData = createAsyncThunk("user/getUser", async () => {
35     const res = await getUserListApi();
36     console.log(res);
37 ​
38     return res;
39 });
40 ​
41 // 创建一个 Slice
42 export const UserSlice = createSlice({
43     name: "userData",
44     initialState,
45     reducers: {
46         // 数据请求完触发 loaddataend是自己写得函数名,不是内置的,叫其他名字也行
47         // addingRemoteUser: (state, { payload }) => {
48         //  state.list = payload;
49         // },
50         // 定义一个加的方法
51         addingUser: (state, { payload, type }) => {
52             console.log(type); //bookData/addingBook
53             console.log(payload.value); //{title:"传来的值"}
54             const allList = JSON.parse(JSON.stringify(state)).userList; //必须要重新深拷贝一次!!!
55 ​
56             allList.push(payload.value);
57 ​
58             state.userList = allList;
59         },
60     },
61     // extraReducers 字段让 slice 处理在别处定义的 actions,
62     // 包括由 createAsyncThunk 或其他slice生成的actions。
63     //说白了就是判断action函数在不同状态下做什么不同的逻辑
64     extraReducers(builder) {
65         builder
66             .addCase(getRemoteUserData.pending, () => {
67                 console.log("⚡ ~ 正在获取用户列表信息!");
68             })
69             .addCase(getRemoteUserData.fulfilled, (state, { payload }) => {
70                 console.log("⚡ ~ 获取远程用户列表成功", payload);
71                 const allList = JSON.parse(JSON.stringify(state)).userList; //必须要重新深拷贝一次!!!
72                 state.userList = allList.concat(payload.userList);
73             })
74             .addCase(getRemoteUserData.rejected, (_, err) => {
75                 console.log("⚡ ~ 获取远程用户列表失败", err);
76             });
77     },
78 });
79 ​
80 // 导出方法,导出的是reducer,让外面组件使用
81 export const { addingUser } = UserSlice.actions;
82 ​
83 // 默认导出
84 export default UserSlice.reducer;
85 ​
86

Store仓库的开发

代码里面都有详细的注释,结合注释对于了解React与Redux的完全可以看懂

在modules文件夹中编写一个文件名是: index.tsx 用于创建整个Redux仓库的配置

 1 // index.ts 文件
 2 ​
 3 import { combineReducers, configureStore } from "@reduxjs/toolkit";
 4 import userSlice from "./features/userSlice";
 5 import bookSlice from "./features/bookSlice";
 6 import { persistStore, persistReducer } from "redux-persist";
 7 import storage from "redux-persist/es/storage";
 8 ​
 9 const persistConfig = {
10     key: "root",
11     storage,
12     blacklist: ["userSlice"], //一般的黑名单,只能达到一级的禁止,要想实现更深层级的禁止或允许持久化,可使用嵌套持久化实现,下面就是
13 };
14 ​
15 const userPersistConfig = {
16     key: "user",
17     storage,
18     blacklist: ["value"],
19 }; //实现嵌套持久化,重写user的持久化配置
20 ​
21 const reducers = combineReducers({
22     userSlice: persistReducer(userPersistConfig, userSlice), //实现嵌套持久化,原理是在localstorage中再开辟一个空间专门存储user相关的数据,在user里面在限制黑名单即可,这样子就实现了仅仅黑名单(user里面的value数据)
23     bookSlice: bookSlice,
24 });
25 ​
26 const persistedReducer = persistReducer(persistConfig, reducers);
27 ​
28 // configureStore创建一个redux数据仓库
29 const store = configureStore({
30     // 合并多个Slice
31     reducer: persistedReducer,
32 });
33 ​
34 //创建一个redux持久化仓库
35 export const persistor = persistStore(store);
36 ​
37 export default store;
38 ​
39

Redux配置

代码里面都有详细的注释,结合注释对于了解React与Redux的完全可以看懂

在整个项目的入口文件 main.tsx 中进行如下代码编写

 1 import React from "react";
 2 import ReactDOM from "react-dom/client";
 3 import { Provider } from "react-redux";
 4 import { PersistGate } from "redux-persist/integration/react";
 5 import App from "./App.tsx";
 6 import store, { persistor } from "./modules/index.tsx";
 7 ​
 8 ReactDOM.createRoot(document.getElementById("root")!).render(
 9     <React.StrictMode>
10         <Provider store={store}>
11             <PersistGate loading={null} persistor={persistor}>
12                 <App />
13             </PersistGate>
14         </Provider>
15     </React.StrictMode>
16 );
17 ​
18

组件调用状态与操作

  1. 代码里面都有详细的注释,结合注释对于了解React与Redux的完全可以看懂
  2. 由于本次实践是一个小案例的形式,所以引入了antd进行简要的UI开发

在整个项目的src文件夹中的文件 app.tsx 中进行如下代码编写

  1 import {
  2     Input,
  3     Avatar,
  4     List,
  5     Button,
  6     Popconfirm,
  7     Divider,
  8     Select,
  9     message,
 10 } from "antd";
 11 import { useState } from "react";
 12 import { useDispatch, useSelector } from "react-redux";
 13 import { addingBook, getRemoteBookData } from "./modules/features/bookSlice";
 14 import { addingUser, getRemoteUserData } from "./modules/features/userSlice";
 15 ​
 16 const { Option } = Select;
 17 ​
 18 function App() {
 19     const dispatch = useDispatch();
 20 ​
 21     const [iptValue, setIptValue] = useState<string>("");
 22     const [typeValue, setTypeValue] = useState<string>("book");
 23     const { userList } = useSelector((store: any) => store.userSlice);
 24     const { bookList } = useSelector((store: any) => store.bookSlice);
 25 ​
 26     const handleAddingData = () => {
 27         if (iptValue !== "") {
 28             const preObject: any = { title: iptValue };
 29             if (typeValue === "user") {
 30                 dispatch(addingUser({ value: preObject })); //这里要求,必须专递对象数据,reducer的payload来接
 31                 message.success("用户添加成功"); //简便起见,就这么加了,实际应该放在操作完成之后
 32             } else if (typeValue === "book") {
 33                 dispatch(addingBook({ value: preObject })); //这里要求,必须专递对象数据,reducer的payload来接
 34                 message.success("书籍添加成功"); //简便起见,就这么加了,实际应该放在操作完成之后
 35             }
 36         }
 37     };
 38 ​
 39     const handleAddingRemoteUser = () => {
 40         dispatch(getRemoteUserData());
 41         message.success("获取远程用户列表成功"); //简便起见,就这么加了,实际应该放在操作完成之后
 42     };
 43 ​
 44     const handleAddingRemoteBook = () => {
 45         dispatch(getRemoteBookData());
 46         message.success("获取远程用户列表成功"); //简便起见,就这么加了,实际应该放在操作完成之后
 47     };
 48 ​
 49     return (
 50         <>
 51             <div style={{ display: "flex" }}>
 52                 <div
 53                     style={{
 54                         width: "670px",
 55                         display: "flex",
 56                         flexDirection: "column",
 57                         margin: "0 auto",
 58                         marginTop: "10px",
 59                     }}
 60                 >
 61                     <div style={{ marginBottom: "40px", display: "flex" }}>
 62                         <Input
 63                             addonAfter={
 64                                 <Select
 65                                     defaultValue="book"
 66                                     onChange={(value) => {
 67                                         setTypeValue(value);
 68                                     }}
 69                                 >
 70                                     <Option value="book">书籍信息</Option>
 71                                     <Option value="user">用户信息</Option>
 72                                 </Select>
 73                             }
 74                             placeholder="输入一些相关的数据吧"
 75                             size="large"
 76                             onChange={(value) => {
 77                                 setIptValue(value.target.value);
 78                             }}
 79                         />
 80                         <Button
 81                             size="large"
 82                             style={{ marginLeft: "10px" }}
 83                             type="primary"
 84                             onClick={() => {
 85                                 handleAddingData();
 86                             }}
 87                         >
 88                             提交
 89                         </Button>
 90                     </div>
 91 ​
 92                     <div
 93                         style={{
 94                             display: "flex",
 95                             alignItems: "center",
 96                             justifyContent: "flex-end",
 97                         }}
 98                     >
 99                         <Button
100                             type="primary"
101                             style={{ marginRight: "30px" }}
102                             onClick={() => {
103                                 handleAddingRemoteUser();
104                             }}
105                         >
106                             远程用户列表
107                         </Button>
108                         <Button
109                             type="primary"
110                             onClick={() => {
111                                 handleAddingRemoteBook();
112                             }}
113                         >
114                             远程书籍列表
115                         </Button>
116                     </div>
117 ​
118                     <Divider>用户列表</Divider>
119 ​
120                     <div>
121                         <List
122                             itemLayout="horizontal"
123                             dataSource={userList}
124                             renderItem={(item, index) => (
125                                 <List.Item
126                                     actions={[
127                                         <Popconfirm
128                                             title="删除日程确认"
129                                             description="你确定要删除这个日程?"
130                                             okText="确认"
131                                             cancelText="取消"
132                                         >
133                                             <Button danger type="primary" size="small">
134                                                 删除用户
135                                             </Button>
136                                         </Popconfirm>,
137                                     ]}
138                                 >
139                                     <List.Item.Meta
140                                         avatar={
141                                             <Avatar
142                                                 src={`https://xsgames.co/randomusers/avatar.php?g=pixel&key=${index}`}
143                                             />
144                                         }
145                                         title={<a href="https://ant.design">{item.title}</a>}
146                                         description="Ant Design, a design language for background applications, is refined by Ant UED Team"
147                                     />
148                                 </List.Item>
149                             )}
150                         />
151                     </div>
152 ​
153                     <Divider>书籍列表</Divider>
154 ​
155                     <div>
156                         <List
157                             itemLayout="horizontal"
158                             dataSource={bookList}
159                             renderItem={(item, index) => (
160                                 <List.Item
161                                     actions={[
162                                         <Popconfirm
163                                             title="删除日程确认"
164                                             description="你确定要删除这个日程?"
165                                             okText="确认"
166                                             cancelText="取消"
167                                         >
168                                             <Button danger type="primary" size="small">
169                                                 删除书籍
170                                             </Button>
171                                         </Popconfirm>,
172                                     ]}
173                                 >
174                                     <List.Item.Meta
175                                         avatar={
176                                             <Avatar
177                                                 src={`https://xsgames.co/randomusers/avatar.php?g=pixel&key=${index}`}
178                                             />
179                                         }
180                                         title={<a href="https://ant.design">{item.title}</a>}
181                                         description="Ant Design, a design language for background applications, is refined by Ant UED Team"
182                                     />
183                                 </List.Item>
184                             )}
185                         />
186                     </div>
187                 </div>
188             </div>
189         </>
190     );
191 }
192 ​
193 export default App;
194 ​
195

总结

本实践没有过多的文本描述,多在代码中的注释。但通过此个实践了解学习之后,应该可以较好的掌握Redux-Toolkit和Redux-Persist这套优雅的React全局状态管理方案