🤠 Redux Toolkit is our official, opinionated, batteries-included toolset for efficient Redux development. It is intended to be the standard way to write Redux logic, and we strongly recommend that you use it.
初步配置 1. 创建项目;
2. 配置别名;
vite.config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 import { defineConfig } from "vite" ;import { join } from "node:path" ;import react from "@vitejs/plugin-react" ;export default defineConfig ({ plugins : [react ()], resolve : { alias : { "@" : join (__dirname, "./src/" ), }, }, });
tsconfig.app.json
1 2 3 4 5 6 7 8 9 10 { "compilerOptions" : { "baseUrl" : "." , "paths" : { "@/*" : [ "./src/*" ] } } , "include" : [ "src" ] }
3. 安装 Redux Toolkit;
1 npm i @reduxjs/toolkit@2.0.1 react-redux@9.1.0
4. 在 src/store/ 目录下新建 index.ts 文件,初始化 Store 对象;
1 2 3 4 5 6 7 8 9 10 11 import { configureStore } from "@reduxjs/toolkit" ;const store = configureStore ({ reducer : {}, }); export default store;
5. 改造 src/main.tsx 模块,在头部区域导入 store 和 Provider;
1 2 3 4 5 6 7 8 9 import ReactDOM from "react-dom/client" ;import store from "@/store" ;import { Provider as StoreProvider } from "react-redux" ;import App from "@/App.tsx" ;import "@/index.css" ;
之后,使用 组件把 包裹起来,并通过 store 属性注入数据;
1 2 3 4 5 6 7 ReactDOM .createRoot (document .getElementById ("root" )!).render ( <StoreProvider store ={store} > <App /> </StoreProvider > );
6. 整理 App.tsx 中的代码。
1 2 3 4 5 function App ( ) { return <div > Hello World</div > ; } export default App ;
创建切片 1. 在 src/store 目录下新建 counterSlice.ts 模块,用来定义 Store 的数据切片;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { createSlice } from "@reduxjs/toolkit" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : {}, }); export default counterSlice.reducer ;
2. 改造 src/store/index.ts 模块,在头部区域导入 counterSlice.ts 的 reducer 函数,并把 counterReducer 挂载到 configureStore 的 reducer 选项中;
1 2 3 4 5 6 7 8 9 10 11 12 import { configureStore } from "@reduxjs/toolkit" ;import counterReducer from "@/store/counterSlice" ;const store = configureStore ({ reducer : { counter : counterReducer, }, }); export default store;
此时,刷新览器之后我们发现:Console 终端不再报错了。Redux 的 Store 中有了第一份数据,数据模块的名字叫做 counter,数据项是 value,初始值是 0,此时可以使用 Redux 调试工具,进行观测。
访问数据 访问数据的误区 下面代码是错误的示范,实际开发中请勿模仿,src/components/Left.tsx。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import type { FC } from "react" ;import store from "@/store" ;const Left : FC = () => { return ( <div > {/* #2 */} {/* 在组件中,直接调用 store.getState() 函数,得到 store 中的所有数据 */} {/* 然后根据数据模块的名称,访问数据模块下的具体数据 */} {/* 其中 counter 是数据模块的名称,value 是模块下具体的数据名 */} <h3 > count: 的值是: {store.getState().counter.value}</h3 > </div > ); }; export default Left ;
注意:在函数式组件中,不要直接调用 store.getState() 函数获取并使用 store 中的数据,缺点:无法使用数据的缓存;无法实现数据的复用。
参考:https://cn.redux.js.org/usage/deriving-data-selectors/
使用 Selector 派生数据 1. 准备 Left 组件,src/components/left.tsx;
1 2 3 4 5 6 7 8 9 import { useSelector } from "react-redux" ;export default function Left ( ) { const count = useSelector ((state ) => state.counter .value ); return ( <div > <h3 > count: 的值是: {count}</h3 > </div > ); }
2. App.tsx 中导入 Left 组件。
1 2 3 4 5 6 7 8 9 10 import Left from "./components/left" ;function App ( ) { return ( <div > <Left /> </div > ); } export default App ;
为 Selector 添加 TS 类型 方式 1 在调用 useSelector<Store 数据的类型, 派生数据的类型> 函数时,可以提供两个泛型参数,分别是:
a. Store 数据的类型:可以在派生数据时提供智能提示;
b. 派生数据的类型:返回的派生数据的类型,在组件中使用派生数据时提供智能提示。
1. 改造 src/components/left.tsx 模块,在头部区域导入 store 对象:
1 import store from "@/store" ;
2. 在 Left 组件外,定义 Store 中所有数据的 TS 类型,命名为 RootState,其中:
a. store.getState 是一个函数,它的返回值是 Store 中存储的数据;
b. typeof store.getState 是 TS 中的类型操作,typeof 用来获取 store.getState 函数的 TS 类型;
c. ReturnType<函数的 TS 类型> 是 TS 中的类型操作,ReturnType 用来获取函数的返回值类型。
1 type RootState = ReturnType <typeof store.getState >;
3. 在 Left 组件中调用 useSelector 时,提供 Store 数据的类型和派生数据的类型;
1 const count = useSelector<RootState, number>(state => state.count.value)
4. 完整代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import type { FC } from "react" ;import { useSelector } from "react-redux" ;import store from "@/store" ;type RootState = ReturnType <typeof store.getState >;const Left : FC = () => { const count = useSelector<RootState , number >((state ) => state.counter .value ); return ( <div > <h3 > count: 的值是: {count}</h3 > </div > ); }; export default Left ;
方式 2 使用 useSelector<Store 数据的类型,派生数据的类型> 的方式添加 TS 类型比较麻烦,程序员必须同时指定 Store 数据的类型和派生数据的类型,为了简化类型操作,我们可以直接为 state 指定数据类型即可,示例代码如下:
1 const count = useSelector ((state: RootState ) => state.count .value );
在 Right 组件中访问 count 的值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import type { FC } from "react" ;import { useSelector } from "react-redux" ;import store from "@/store" ;type RootState = ReturnType <typeof store.getState >;const Right : FC = () => { const count = useSelector ((state: RootState ) => state.counter .value ); return <div > Right: {count}</div > ; }; export default Right ;
App.tsx 中使用 Right 组件。
1 2 3 4 5 6 7 8 9 10 11 12 import Left from "./components/left" ;import Right from "./components/right" ;function App ( ) { return ( <div > <Left /> <Right /> </div > ); } export default App ;
封装 RootState 的 TS 类型 1. 抽离 RootState 到 store/index.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 import { configureStore } from "@reduxjs/toolkit" ;import counterReducer from "@/store/counterSlice" ;const store = configureStore ({ reducer : { counter : counterReducer, }, }); export default store;export type RootState = ReturnType <typeof store.getState >;
2. 引入 RootState,src/components/right.tsx。
1 2 3 4 5 6 7 8 9 10 11 import type { FC } from "react" ;import type { RootState } from "@/store" ;import { useSelector } from "react-redux" ;const Right : FC = () => { const count = useSelector ((state: RootState ) => state.counter .value ); return <div > Right: {count}</div > ; }; export default Right ;
把 Selector 封装到 slice 中 1. 抽离 Selector,src/store/counterSlice.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { createSlice } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : {}, }); export default counterSlice.reducer ;export const selectCount = (state: RootState ) => state.counter .value ;
2. 引用 Selector,src/components/right.tsx。
1 2 3 4 5 6 7 8 9 10 11 12 import type { FC } from "react" ;import { selectCount } from "@/store/counterSlice" ;import { useSelector } from "react-redux" ;const Right : FC = () => { const count = useSelector (selectCount); return <div > Right: {count}</div > ; }; export default Right ;
修改数据 在 Left 组件中渲染自增 +1 的按钮 1. 改造 src/components/left.tsx 组件,从 antd 中按需导入 Button 组件和 Space 组件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Button , Space } from 'antd' 渲染按钮区域的布局结构:import type { FC } from "react" ;import { useSelector } from "react-redux" ;import { selectCount } from "@/store/counterSlice" ;import { Button , Space } from "antd" ;const Left : FC = () => { const count = useSelector (selectCount); return ( <div > <h3 > count: 的值是: {count}</h3 > <Space direction ="horizontal" > <Button type ="primary" onClick ={increment} > +1</Button > </Space > </div > ); }; export default Left ;
2. 在 Left 组件中定义 increment 处理函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import type { FC } from "react" ;import { useSelector, useDispatch } from "react-redux" ;import { selectCount } from "@/store/counterSlice" ;import { Button , Space } from "antd" ;const Left : FC = () => { const count = useSelector (selectCount); const dispatch = useDispatch (); const increment = ( ) => { dispatch ({ type : "counter/increment" , }); }; return ( <div > <h3 > count: 的值是: {count}</h3 > <Space direction ="horizontal" > <Button type ="primary" onClick ={increment} > +1 </Button > </Space > </div > ); }; export default Left ;
点击按钮实现 count 值自增的功能 src/store/counterSlice.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { createSlice } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : { increment (state ) { state.value += 1 ; }, }, }); export default counterSlice.reducer ;export const selectCount = (state: RootState ) => state.counter .value ;
使用 counterSlice 自动生成 action 1. 使用 counterSlice 自动生成 action,src/store/counterSlice.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { createSlice } from "@reduxjs/toolkit" ;import { RootState } from "@/store" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : { increment (state ) { state.value += 1 ; }, }, }); export default counterSlice.reducer ;export const selectCount = (state: RootState ) => state.counter .value ;export const { increment } = counterSlice.actions ;
2. 组件中使用 actionCreator,src/components/left.tsx。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { useDispatch, useSelector } from "react-redux" ;import { Button , Space } from "antd" ;import { selectCount, increment as incrementActionCreator, } from "@/store/counterSlice" ; export default function Left ( ) { const count = useSelector (selectCount); const dispatch = useDispatch (); const increment = ( ) => { dispatch (incrementActionCreator ()); }; return ( <div > <h3 > count: 的值是: {count}</h3 > <Space direction ="horizontal" > <Button type ="primary" onClick ={increment} > +1 </Button > </Space > </div > ); }
action 中的 payload 1. 传递参数,src/components/left.tsx;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { useDispatch, useSelector } from "react-redux" ;import { Button , Space } from "antd" ;import { selectCount, increment as incrementActionCreator, } from "@/store/counterSlice" ; export default function Left ( ) { const count = useSelector (selectCount); const dispatch = useDispatch (); const increment = (payload?: number ) => { dispatch (incrementActionCreator (payload)); }; return ( <div > <h3 > count: 的值是: {count}</h3 > <Space direction ="horizontal" > {/* #2 */} <Button type ="primary" onClick ={() => increment()}> +1 </Button > {/* #1 */} <Button type ="primary" onClick ={() => increment(3)}> +3 </Button > </Space > </div > ); }
2. 接受参数,src/store/counterSlice.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { createSlice } from "@reduxjs/toolkit" ;import { RootState } from "@/store" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : { increment (state, action ) { state.value += action.payload || 1 ; }, }, }); export default counterSlice.reducer ;export const selectCount = (state: RootState ) => state.counter .value ;export const { increment } = counterSlice.actions ;
为 action 对象指定 TS 类型 为 action 对象指定 TS 类型,src/store/counterSlice.ts。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { createSlice, PayloadAction } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : { increment (state, action: PayloadAction<number | undefined > ) { state.value += action.payload || 1 ; }, }, }); export default counterSlice.reducer ;export const selectCount = (state: RootState ) => state.counter .value ;export const { increment } = counterSlice.actions ;
在 Right 组件中实现自减功能 1. 改造 src/store/counterSlice.ts 模块,在 reducers 下封装 decrement 函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { createSlice, PayloadAction } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;const counterSlice = createSlice ({ name : "counter" , initialState : { value : 0 , }, reducers : { increment (state, action: PayloadAction<number | undefined > ) { state.value += action.payload || 1 ; }, decrement (state, action: PayloadAction<number | undefined > ) { state.value -= action.payload ?? 1 ; }, }, }); export default counterSlice.reducer ;export const selectCount = (state: RootState ) => state.counter .value ;export const { increment, decrement } = counterSlice.actions ;
2. 改造 src/components/right.tsx 模块,从 react-redux 中按需导入 useDispatch,从 counterSlice.ts 中按需导入 decrement 这个 action creator:
1 import { selectCount, decrement } from "@/store/counterSlice" ;
继续改造 src/components/right.tsx 模块,在头部区域按需导入 antd 的组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import type { FC } from "react" ;import { useDispatch, useSelector } from "react-redux" ;import { decrement as decrementActionCreator, selectCount, } from "@/store/counterSlice" ; import { Space , Button } from "antd" ;const Right : FC = () => { const count = useSelector (selectCount); const dispatch = useDispatch (); const decrement = (payload?: number ) => { dispatch (decrementActionCreator (payload)); }; return ( <div > <h3 > Right: {count}</h3 > <Space > {/* #3 */} <Button type ="primary" onClick ={() => decrement(1)}> -1 </Button > <Button type ="primary" onClick ={() => decrement(3)}> -3 </Button > </Space > </div > ); }; export default Right ;
根据用户输入的 step 值自减 从 antd 中按需导入 Input 组件,只有组件之间需要共享的数据,才有必要存储到 redux 中,组件内部的私有状态没必要往 redux 中存。因此,我们在 Right 组件中借助于 useState() 来声明组件内部的私有状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { useState, type FC } from "react" ;import { useDispatch, useSelector } from "react-redux" ;import { decrement as decrementActionCreator, selectCount, } from "@/store/counterSlice" ; import { Space , Button , Input } from "antd" ;const Right : FC = () => { const count = useSelector (selectCount); const dispatch = useDispatch (); const [num, setNum] = useState (1 ); const decrement = (payload?: number ) => { dispatch (decrementActionCreator (payload)); }; return ( <div > <h3 > Right: {count}</h3 > {/* #2 */} <Input value ={num} onChange ={(e) => setNum(Number(e.currentTarget.value))} /> <Space > <Button type ="primary" onClick ={() => decrement(1)}> -1 </Button > <Button type ="primary" onClick ={() => decrement(3)}> -3 </Button > {/* #3 */} <Button type ="primary" onClick ={() => decrement(num)}> -n </Button > </Space > </div > ); }; export default Right ;
异步处理 🤠 注意:在 reducer 函数中只能编写同步代码去修改 store 中的数据,不能编写异步代码,否则 redux 将无法正常工作。通过前面的学习我们已经知道:在修改 Store 数据时,必须调用 dispatch0 函数,并提供一个 action 对象。例如:
1 dispatch ({ type : "counter/increment" });
在调用 dispatch 函数期间,如果我们 dispatch 的 action 是一个普通的对象,则表示要以同步的方式修改 Store 中的数据,引入的 reducers 中只能包含同步代码。如果要编写异步代码修改 Store 中的数据,可以使用 thunk 函数作为 action,传递给 dispatch,语法格式如下:
1 2 3 // 使用 thunk 函数作为 action // thunk 是一个编程术语,表示:一段执行某些延迟工作的代码,可以包含异步逻辑,例如 setTimeout、Promise、async/await // dispatch(thunk函数)
thunk 函数有两个固定的形参,分别是 dispatch 和 getState 函数,其中:
a. dispatch 负责在异步代码执行完毕后,把异步结果以同步的方式提交个 reducer
b. getState 用来获取 store 中的数据,详细的语法格式如下:
1 2 3 4 5 6 7 dispatch ((dispatch, getState ) => { setTimeout (() => { dispatch ({ type : "counter/increment" }); }, 1000 ); });
再例如,在 thunk 中调用接口异步获取数据:
1 2 3 4 5 dispatch (async (dispatch, getState) => { const { data : res } = await axios.get ("/users" ); dispatch ({ type : "user/initUserList" , payload : res.list }); });
以 thunk 的方式实现 1s 后自增的功能 1. 改造 src/components/left.tsx 模块,为异步自增 +1 按钮绑定 onClick 函数;
1 2 3 <Button type ="primary" onClick={incrementAsync}> 异步自增+1 </Button >
2. 在 Left 组件中定义 incrementAsync 函数,在 incrementAsync 函数内部以 thunk 的方式实现异步操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { useDispatch, useSelector } from "react-redux" ;import { Button , Space } from "antd" ;import { selectCount, increment as incrementActionCreator, } from "@/store/counterSlice" ; export default function Left ( ) { const count = useSelector (selectCount); const dispatch = useDispatch (); const increment = (payload?: number ) => { dispatch (incrementActionCreator (payload)); }; const incrementAsync = ( ) => { dispatch ((dispatch ) => { setTimeout (() => { dispatch (incrementActionCreator ()); }, 1000 ); }); }; return ( <div > <h3 > count: 的值是: {count}</h3 > <Space direction ="horizontal" > <Button type ="primary" onClick ={() => increment()}> +1 </Button > <Button type ="primary" onClick ={() => increment(3)}> +3 </Button > {/* #1 */} <Button type ="primary" onClick ={incrementAsync} > 异步自增+1 </Button > </Space > </div > ); }
注意:在上述代码中,调用 dispatch(thunk 函数) 时,会提示 TS 的类型警告:
1 类型"(dispatch: any) => void" 的参数不能赋值给类型"UnknownAction" 的参数
这是因为 const dispatch = useDispatch() 获得的 dispatch 函数无法正确识别 thunk 类型的 action(即:函数类型的 action),需要我们使用 store.dispatch 来代替普通的 dispatch 即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { useDispatch, useSelector } from "react-redux" ;import { Button , Space } from "antd" ;import { selectCount, increment as incrementActionCreator, } from "@/store/counterSlice" ; import store from "@/store" ;export default function Left ( ) { const count = useSelector (selectCount); const dispatch = useDispatch (); const increment = (payload?: number ) => { dispatch (incrementActionCreator (payload)); }; const incrementAsync = ( ) => { store.dispatch ((dispatch ) => { setTimeout (() => { dispatch (incrementActionCreator ()); }, 1000 ); }); }; return ( <div > <h3 > count: 的值是: {count}</h3 > <Space direction ="horizontal" > <Button type ="primary" onClick ={() => increment()}> +1 </Button > <Button type ="primary" onClick ={() => increment(3)}> +3 </Button > <Button type ="primary" onClick ={incrementAsync} > 异步自增+1 </Button > </Space > </div > ); }
封装自定义的 hook 函数 通过刚才的学习,我们总结出以下的经验:
1. 通过 useDispatch 得到的 dispatch 函数,仅适合与普通的 action 对象一起使用,不适合与 thunk action 配合使用(缺少 TS 美型支持);
2. 通过 store.dispatch 得到的 dispatch 函数,既支持普通的 action,又支持 thunk action,但是每次使用它都需要通过 store 进行访问略显麻烦,所以,我们可以在 src/store/目录下封装一个 hooks.ts 的模块,专门用来封装自定义的 redux 相关的 hook:
1 2 3 4 5 6 7 8 9 import { useDispatch } from "react-redux" ;import store from "@/store" ;export const useAppDispatch = useDispatch.withTypes <typeof store.dispatch >();
今后,如果想在 React 组件中获取并调用 dispatch 函数,我们推荐大家导入自定义的 useAppDispatch hook,而非 react-redux 官方提供的 useDispatch,例如:
1 2 3 4 5 6 7 8 const Left : FC = () => { const count = useSelector (selectCount); const dispatch = useAppDispatch (); const increment = (payload?: number ) => { dispatch (incrementActionCreator (payload)); }; };
同样的,为了给 useSelector 提供完善的 TS 类型提示,我们也可以在 src/store/hooks.ts 中封装一个自定义的 useAppSelector hook:
src/store/hooks.ts
1 export const useAppSelector = useSelector.withTypes <RootState >();
src/components/left.tsx
1 2 3 4 5 6 7 8 export default function Left ( ) { const count = useSelector (selectCount); const dispatch = useAppDispatch (); const doubleCount = useAppSelector ((state ) => state.counter .value * 2 ); }
封装 thunk creator 函数 当 dispatch 普通 action 对象时,我们有两种方式提供普通的 action 对象。
原始写法
1 dispatch ({ type : "counter/increment" , payload : 1 });
进阶写法
1 2 dispatch (incrementActionCreator (1 ));
所以,当 dispatch thunk action 时,我们也有两种方式提供 action 函数。
原始写法
1 2 3 4 5 dispatch ((dispatch, getState ) => { setTimeout (() => { dispatch (increment (1 )); }, 1000 ); });
进阶写法
1 2 dispatch (incrementAsyncActionCreator (1 ));
现在问题是:该如何封装 thunk action creator 函数呢?其实不难,thunk action creator 函数就是两个 function 的嵌套:
1. 外层 function 负责向外 return 一个 thunk 函数;
2. 内层的 thunk 函数负责执行异步操作,并把异步操作的结果以同步的方式进行 dispatch。
例如,我们可以改造 src/store/counterSlice.ts 模块,定义并向外导出一个名为 incrementAsyncActionCreator 的 thunk action creator 函数:
1 2 3 4 5 6 7 8 9 10 11 export const incrementAsyncActionCreator = (step?: number ) => { return (dispatch ) => { setTimeout (() => { dispatch (increment (step)); }, 1000 ); }; };
3. 接下来,改造 src/components/left.tsx 模块,先从 counterSlice.ts 中按需导入 incrementAsync 和这个 thunk action creator 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default function Left ( ) { const count = useSelector (selectCount); const dispatch = useAppDispatch (); const doubleCount = useAppSelector ((state ) => state.counter .value * 2 ); const increment = (payload?: number ) => { dispatch (incrementActionCreator (payload)); }; const incrementAsync = ( ) => { dispatch (incrementAsyncActionCreator (1 )); }; }
为 thunk creator 添加 TS 类型 1. 改造 src/store/index.ts 模块,从 @reduxjs/toolkit 中按需导入下面的 TS 类型
1 2 3 import type { ThunkAction , Action } from "@reduxjs/toolkit" ;
2. 之后,定义并向外导出自定义的 AppThunk 类型,它用来描述 thunk creator 函数的返回值的类型(即 thunk 函数的 TS 类型):
1 2 3 4 5 6 7 8 9 10 11 export type AppThunk <RT = void > = ThunkAction < RT , RootState , unknown , Action <string > >;
3. 改造 src/store/counterSlice.ts 模块,先在头部区域按需导入自定义的 AppThunk 类型:
1 import type { RootState , AppThunk } from "@/store" ;
再为 IncrementAsync 函数指定返回值的类型为 AppThunk。实例代码如下:
1 2 3 4 5 6 7 8 9 10 11 export const incrementAsync = (step?: number ): AppThunk => { return (dispatch ) => { setTimeout (() => { dispatch (increment (step)); }, 1000 ); }; };
基于 thunk 实现 1s 后自减的功能 1. 改造 src/store/counterSlice.ts 模块,定义并向外导出 decrementAsync 函数,用来实现异步数值自减的功能:
1 2 3 4 5 6 7 export const decrementAsyncActionCreator = (step?: number ): AppThunk => { return (dispatch ) => { setTimeout (() => { dispatch (decrement (step)); }, 1000 ); }; };
2. 改造 src/components/right.tsx 模块,在头部区域导入自定义的 Hooks,从而增强 TS 的类型提示;
1 import { useAppDispatch, useAppSelector } from "@/store/hooks" ;
3. 把 Right 组件中的 useSelector 替换为 useAppSelector,把 useDispatch 替换为 useAppDispath。
1 2 const count = useAppSelector (selectCount);const dispatch = useAppDispatch ();
3. 改造 src/components/right.tsx 模块,从 @/store/counterSlice.ts 中按需导入 decrementAsync 函数;
1 2 3 4 5 import { selectCount, decrement, decrementAsyncActonCreator, } from "@/store/counterSlice" ;
在 jsx 中添加 1s 后减 1 的按钮 ,绑定 onClick 处理函数如下:
1 2 3 <Button type ="primary" onClick={() => decrementAsyncActionCreator (1 )}> async -1 </Button >
4. src/components/right.tsx 完整代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { useState, type FC } from "react" ;import { decrement as decrementActionCreator, decrementAsyncActionCreator, selectCount, } from "@/store/counterSlice" ; import { Space , Button , Input } from "antd" ;import { useAppDispatch, useAppSelector } from "@/store/hook" ;const Right : FC = () => { const count = useAppSelector (selectCount); const dispatch = useAppDispatch (); const [num, setNum] = useState (1 ); const decrement = (payload?: number ) => { dispatch (decrementActionCreator (payload)); }; return ( <div > <h3 > Right: {count}</h3 > <Input value ={num} onChange ={(e) => setNum(Number(e.currentTarget.value))} /> <Space > <Button type ="primary" onClick ={() => decrement(1)}> -1 </Button > <Button type ="primary" onClick ={() => decrement(3)}> -3 </Button > <Button type ="primary" onClick ={() => decrement(num)}> -n </Button > <Button type ="primary" onClick ={() => dispatch(decrementAsyncActionCreator(1))}> async -1 </Button > </Space > </div > ); }; export default Right ;
加载状态 以原始的方式添加 loading 状态 1. 添加 loading 逻辑,src/store/counterSlice.ts;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import { createSlice, PayloadAction } from "@reduxjs/toolkit" ;import type { AppThunk , RootState } from "@/store" ;type CounterStateType = { value : number ; status : "idle" | "pending" | "fulfilled" | "rejected" ; }; const initialState : CounterStateType = { value : 0 , status : "idle" , }; type LoadingStatus = CounterStateType ["status" ];const counterSlice = createSlice ({ name : "counter" , initialState, reducers : { increment (state, action: PayloadAction<number | undefined > ) { state.value += action.payload || 1 ; }, decrement (state, action: PayloadAction<number | undefined > ) { state.value -= action.payload ?? 1 ; }, setPendingStatus (state, action: PayloadAction<LoadingStatus> ) { state.status = action.payload ; }, }, }); export default counterSlice.reducer ;export const selectPending = (state: RootState ) => state.counter .status === "pending" ; export const selectCount = (state: RootState ) => state.counter .value ;export const { increment, decrement, setPendingStatus } = counterSlice.actions ;export const incrementAsyncActionCreator = (step?: number ): AppThunk => { return (dispatch ) => { setTimeout (() => { dispatch (increment (step)); }, 1000 ); }; }; export const decrementAsyncActionCreator = (step?: number ): AppThunk => { return (dispatch ) => { dispatch (setPendingStatus ("pending" )); setTimeout (() => { dispatch (decrement (step)); dispatch (setPendingStatus ("fulfilled" )); }, 1000 ); }; };
2. 使用 loading 状态,src/components/right.tsx。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import { useState, type FC } from "react" ;import { decrement as decrementActionCreator, decrementAsyncActionCreator, selectCount, selectPending, } from "@/store/counterSlice" ; import { Space , Button , Input } from "antd" ;import { useAppDispatch, useAppSelector } from "@/store/hook" ;const Right : FC = () => { const count = useAppSelector (selectCount); const dispatch = useAppDispatch (); const pending = useAppSelector (selectPending); const [num, setNum] = useState (1 ); const decrement = (payload?: number ) => { dispatch (decrementActionCreator (payload)); }; return ( <div > <h3 > Right: {count}</h3 > <Input value ={num} onChange ={(e) => setNum(Number(e.currentTarget.value))} /> <Space > <Button type ="primary" onClick ={() => decrement(1)}> -1 </Button > <Button type ="primary" onClick ={() => decrement(3)}> -3 </Button > <Button type ="primary" onClick ={() => decrement(num)}> -n </Button > {/* #6 */} <Button loading ={pending} type ="primary" onClick ={() => dispatch(decrementAsyncActionCreator(1))} > async -1 </Button > </Space > </div > ); }; export default Right ;
createAsyncThunk + 异步数据获取 createAsync 的语法格式 在 Redux 中,如果想要异步的调用 AJAX 接口获取网络数据,可以使用 @reduxjs/toolkit 提供的 createAsyncThunk API 生成异步 thunk,语法格式如下:
1 2 3 4 5 6 7 8 import { createAsyncThunk } from '@reduxjs/toolkit' export const fetchPosts = createAsyncThunk ('action-type 的前缀' , async () => { })
封装 colorSlice 切片 1. 在 src/store 目录下新建 colorSlice.ts 模块,并初始化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { createSlice } from '@reduxjs/toolkit' import type { RootState } from '@/store' const initialState = { value : '#000' } const colorSlice = createSlice ({ name : 'color' , initialState, reducers : {} }) export default colorSlice.reducer export const selectColor = (state: RootState ) => state.color .value
src/store/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { configureStore } from '@reduxjs/toolkit' import type { ThunkAction , Action } from '@reduxjs/toolkit' import counterReducer from '@/store/counterSlice' import colorSlice from './colorSlice' const store = configureStore ({ reducer : { counter : counterReducer, color : colorSlice } }) export default storeexport type RootState = ReturnType <typeof store.getState >;export type AppThunk <RT = void > = ThunkAction <RT , RootState , unknown , Action <string >>;
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import Left from "./components/left" ;import Right from "./components/right" ;import { selectColor } from "./store/colorSlice" ;import { useAppSelector } from "./store/hook" ;function App ( ) { const color = useAppSelector (selectColor); return ( <div style ={{ backgroundColor: color }}> <Left /> <Right /> </div > ); } export default App ;
初步使用 createAsyncThunk src/store/colorSlice.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;const initialState = { value : "#fff" , }; const colorSlice = createSlice ({ name : "color" , initialState, reducers : {}, }); export default colorSlice.reducer ;export const selectColor = (state: RootState ) => state.color .value ;export const getColorThunkCreator = createAsyncThunk ( "color/getcolor" , (num: number ) => { console .log ("ok" , num); } );
src/components/right.tsx;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import { useState, type FC } from "react" ;import { decrement as decrementActionCreator, decrementAsyncActionCreator, selectCount, selectPending, } from "@/store/counterSlice" ; import { Space , Button , Input } from "antd" ;import { useAppDispatch, useAppSelector } from "@/store/hook" ;import { getColorThunkCreator } from "@/store/colorSlice" ;const Right : FC = () => { const count = useAppSelector (selectCount); const dispatch = useAppDispatch (); const pending = useAppSelector (selectPending); const [num, setNum] = useState (1 ); const decrement = (payload?: number ) => { dispatch (decrementActionCreator (payload)); }; return ( <div > <h3 > Right: {count}</h3 > <Input value ={num} onChange ={(e) => setNum(Number(e.currentTarget.value))} /> <Space > <Button type ="primary" onClick ={() => decrement(1)}> -1 </Button > <Button type ="primary" onClick ={() => decrement(3)}> -3 </Button > <Button type ="primary" onClick ={() => decrement(num)}> -n </Button > {/* #6 */} <Button loading ={pending} type ="primary" onClick ={() => dispatch(decrementAsyncActionCreator(1))} > async -1 </Button > {/* #2 */} <Button type ="primary" onClick ={() => dispatch(getColorThunkCreator(2))} >随机颜色</Button > </Space > </div > ); }; export default Right ;
封装 AJAX 请求并匹配请求的状态 src/store/colorSlice.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;const initialState = { value : "#fff" , }; const colorSlice = createSlice ({ name : "color" , initialState, reducers : {}, extraReducers (builder ) { builder .addCase (getColorThunkCreator.pending , (state ) => { console .log ("pending" ); }) .addCase (getColorThunkCreator.fulfilled , (state, action ) => { state.value = action.payload ; }); }, }); export default colorSlice.reducer ;export const selectColor = (state: RootState ) => state.color .value ;const randomColor = (): Promise <string > => { return new Promise ((resolve ) => { setTimeout (() => { resolve ("#" + Math .random ().toString (16 ).slice (2 , 8 )); }, 1000 ); }); }; export const getColorThunkCreator = createAsyncThunk ( "color/getcolor" , async (num : number ) => { const r = await randomColor (); return r; } );
为 AJAX 添加 loading 状态 src/store/colorSlice.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;type ColorStateType = { value : string ; status : "idle" | "pending" | "fulfilled" | "rejected" ; }; const initialState : ColorStateType = { value : "#fff" , status : "idle" , }; const colorSlice = createSlice ({ name : "color" , initialState, reducers : {}, extraReducers (builder ) { builder .addCase (getColorThunkCreator.pending , (state ) => { state.status = 'pending' }) .addCase (getColorThunkCreator.fulfilled , (state, action: PayloadAction<string > ) => { state.value = action.payload ; state.status = 'fulfilled' }); }, }); export default colorSlice.reducer ;export const selectColor = (state: RootState ) => state.color .value ;const randomColor = (): Promise <string > => { return new Promise ((resolve ) => { setTimeout (() => { resolve ("#" + Math .random ().toString (16 ).slice (2 , 8 )); }, 1000 ); }); }; export const getColorThunkCreator = createAsyncThunk ( "color/getcolor" , async (num : number ) => { const r = await randomColor (); return r; } ); export const selectColorPending = (state: RootState ) => state.color .status === 'pending'
src/components/right.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import { useState, type FC } from "react" ;import { decrement as decrementActionCreator, decrementAsyncActionCreator, selectCount, selectPending, } from "@/store/counterSlice" ; import { Space , Button , Input } from "antd" ;import { useAppDispatch, useAppSelector } from "@/store/hook" ;import { getColorThunkCreator, selectColorPending } from "@/store/colorSlice" ;const Right : FC = () => { const count = useAppSelector (selectCount); const dispatch = useAppDispatch (); const pending = useAppSelector (selectPending); const colorPending = useAppSelector (selectColorPending); const [num, setNum] = useState (1 ); const decrement = (payload?: number ) => { dispatch (decrementActionCreator (payload)); }; return ( <div > <h3 > Right: {count}</h3 > <Input value ={num} onChange ={(e) => setNum(Number(e.currentTarget.value))} /> <Space > <Button type ="primary" onClick ={() => decrement(1)}> -1 </Button > <Button type ="primary" onClick ={() => decrement(3)}> -3 </Button > <Button type ="primary" onClick ={() => decrement(num)}> -n </Button > <Button loading ={pending} type ="primary" onClick ={() => dispatch(decrementAsyncActionCreator(1))} > async -1 </Button > {/* #2 */} <Button loading ={colorPending} type ="primary" onClick ={() => dispatch(getColorThunkCreator(2))} > 随机颜色 </Button > </Space > </div > ); }; export default Right ;
模拟请求失败后的操作 src/store/colorSlice.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit" ;import type { RootState } from "@/store" ;import { message } from "antd" ;type ColorStateType = { value : string ; status : "idle" | "pending" | "fulfilled" | "rejected" ; }; const initialState : ColorStateType = { value : "#fff" , status : "idle" , }; const colorSlice = createSlice ({ name : "color" , initialState, reducers : {}, extraReducers (builder ) { builder .addCase (getColorThunkCreator.pending , (state ) => { state.status = "pending" ; }) .addCase ( getColorThunkCreator.fulfilled , (state, action: PayloadAction<string > ) => { state.value = action.payload ; state.status = "fulfilled" ; } ).addCase (getColorThunkCreator.rejected , (state, action ) => { state.status = 'rejected' state.value = 'red' message.error (action.error .message ) }) }, }); export default colorSlice.reducer ;export const selectColor = (state: RootState ) => state.color .value ;const randomColor = (): Promise <string > => { return new Promise ((resolve ) => { setTimeout (() => { resolve ("#" + Math .random ().toString (16 ).slice (2 , 8 )); }, 1000 ); }); }; export const getColorThunkCreator = createAsyncThunk ( "color/getcolor" , async () => { try { const r = await randomColor (); throw 'Error ~~~' return r; } catch (err) { return Promise .reject (err); } } ); export const selectColorPending = (state: RootState ) => state.color .status === "pending" ;
如果发请求的时间超过 500ms,会调用 controller.abort(),这个时候 fetch() promise 将 reject 一个名为 AbortError 的 DOMException。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export const getColorThunkCreator = createAsyncThunk ( "color/getcolor" , async (num : number ) => { try { const controller = new AbortController (); setTimeout (() => controller.abort (), 500 ); const response = await fetch ("https://api.test.top/v1/color" , { signal : controller.signal , }); const res = await response.json (); return res.data ; } catch (err) { return Promise .reject (err); } } );
数据缓存 初步配置 Redux 持久化 1. 运行如下的命令,安装持久化 redux 数据的依赖包;
1 2 npm i redux-persist@6.0.0 npm i --save-dev @types/redux-persist
2. 改造 src/store/index.ts 模块,从 redux-persist 中导入 storage 存储器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import { configureStore, combineReducers } from "@reduxjs/toolkit" ;import type { ThunkAction , Action } from "@reduxjs/toolkit" ;import counterReducer from "@/store/counterSlice" ;import colorReducer from '@/store/colorSlice' import storage from "redux-persist/lib/storage" ;import { persistReducer, persistStore } from "redux-persist" ;const rootReducer = combineReducers ({ counter : counterReducer, color : colorReducer, }); const persistConfig = { key : "root" , storage, }; const store = configureStore ({ reducer : persistReducer (persistConfig, rootReducer), middleware (getDefaultMiddleware ) { return getDefaultMiddleware ({ serializableCheck : false , }); } }); export const persistor = persistStore (store);export default store;export type RootState = ReturnType <typeof store.getState >;export type AppThunk <RT = void > = ThunkAction < RT , RootState , unknown , Action <string > >;
实现 Redux 数据的持久化 src/main.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import ReactDOM from "react-dom/client" ;import { Provider as StoreProvider } from "react-redux" ;import { PersistGate } from "redux-persist/integration/react" ;import store, { persistor } from "./store" ;import App from "./App.tsx" ;ReactDOM .createRoot (document .getElementById ("root" )!).render ( <StoreProvider store ={store} > <PersistGate persistor ={persistor} > <App /> </PersistGate > </StoreProvider > );