危险

为之则易,不为则难

0%

RTK

🤠 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. 创建项目;

1
npm create vite@latest

2. 配置别名;

1
npm i -D @types/node

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";

// https://vitejs.dev/config/
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
// 1. 导入 Redux 核心函数
import { configureStore } from "@reduxjs/toolkit";

// 2. 创建 store
const store = configureStore({
// reducer 用于定义修改状态的变更函数
reducer: {},
});

// 3. 导出 store
export default store;

5. 改造 src/main.tsx 模块,在头部区域导入 store 和 Provider;

1
2
3
4
5
6
7
8
9
import ReactDOM from "react-dom/client";
// #1
import store from "@/store";
// #2
// Provider is a component that makes the Redux store available to any nested components that need access to the 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(
// #3
<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
// #1 导入 createSlice 函数,用来创建切片
import { createSlice } from "@reduxjs/toolkit";

// #2 调用 createSlice 函数,必须提供 name、initialState 和 reducers 三个参数
const counterSlice = createSlice({
// 唯一标识
name: "counter",
// 初始数据
initialState: {
value: 0,
},
// 用来描述如何更新上面的 state
reducers: {},
});

// #3
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";
// #1 导入切换的 reducer 函数
import counterReducer from "@/store/counterSlice";

const store = configureStore({
// #2
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";
// #1
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";
// #1
import { useSelector } from "react-redux";
// #2
import store from "@/store";

// #3
type RootState = ReturnType<typeof store.getState>;
const Right: FC = () => {
// #4
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;

// #mark
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";
// #mark
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;

// mark
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";
// #1
import { selectCount } from "@/store/counterSlice";
import { useSelector } from "react-redux";

const Right: FC = () => {
// #2
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";
// #1
import { useSelector, useDispatch } from "react-redux";
import { selectCount } from "@/store/counterSlice";
import { Button, Space } from "antd";

const Left: FC = () => {
const count = useSelector(selectCount);
// #2
const dispatch = useDispatch();

const increment = () => {
// #3
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: {
// #mark
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;

// #mark => actionCreator
// 类似于 actionCreator
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";
// #1
import {
selectCount,
increment as incrementActionCreator,
} from "@/store/counterSlice";

export default function Left() {
const count = useSelector(selectCount);
// #2
const dispatch = useDispatch();
const increment = () => {
// #3
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();
// #3
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: {
// 根据 step 实现数值自增
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
// #1
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { RootState } from "@/store";

const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
// #2
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;
},
// #1
decrement(state, action: PayloadAction<number | undefined>) {
// null、undefined
// ''、0、false
state.value -= action.payload ?? 1;
},
},
});

export default counterSlice.reducer;
export const selectCount = (state: RootState) => state.counter.value;

// #2
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";
  1. 继续改造 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);
// #1
const dispatch = useDispatch();

// #2
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();
// #1
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 action 到 reducer
dispatch({ type: "counter/increment" });
}, 1000);
});

再例如,在 thunk 中调用接口异步获取数据:

1
2
3
4
5
dispatch(async (dispatch, getState) => {
// AJAX 请求
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));
};
// #2
const incrementAsync = () => {
dispatch((dispatch) => {
// 可以写异步
setTimeout(() => {
// 把异步的结果以同步的方式 dispatch 出去
// dispatch({ type: "counter/increment" });
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";
// #1
import store from "@/store";
export default function Left() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const increment = (payload?: number) => {
dispatch(incrementActionCreator(payload));
};
const incrementAsync = () => {
// #2
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";

// Creates a 'pre-typed' version of useDispatch where the type of the dispatch function is predefined.
// 创建 useDispatch 的预定义类型版本,其中 dispatch 函数的类型是预定义的
// This allows you to set the dispatch type once,eliminating the need to specify it with every useDispatch call
// 这允许您设置一次 dispatch 类型,无需在每次 useDispatch 时为 dispatch 指定 type 类型
// export const useAppDispatch = () => useDispatch<typeof store.dispatch>()
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 = useSelector((state: RootState) => state.counter.value * 2)
const doubleCount = useAppSelector((state) => state.counter.value * 2);
// ...
}

封装 thunk creator 函数

当 dispatch 普通 action 对象时,我们有两种方式提供普通的 action 对象。

原始写法

1
dispatch({ type: "counter/increment", payload: 1 });

进阶写法

1
2
// 其中,increment 函数是一个 action creator 函数,返回值是一个 action 对象
dispatch(incrementActionCreator(1));

所以,当 dispatch thunk action 时,我们也有两种方式提供 action 函数。

原始写法

1
2
3
4
5
dispatch((dispatch, getState) => {
setTimeout(() => {
dispatch(increment(1));
}, 1000);
});

进阶写法

1
2
// 其中,incrementAsync 函数是一个 thunk action creator,返回值是一个 thunk action 函数
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
// 外层这个函数,是 thunk action creator
export const incrementAsyncActionCreator = (step?: number) => {
// 内部 return 的这个函数,是真正的 thunk action
// 它有两个固定的形参,dispatch 和 getState,可按需要接受并使用
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 = useSelector((state: RootState) => state.counter.value * 2)
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
// ThunkAction 是 thunk action(Thunk 函数)的 TS 类型
// Action 是普通 action 的 TS 类型
import type { ThunkAction, Action } from "@reduxjs/toolkit";

2. 之后,定义并向外导出自定义的 AppThunk 类型,它用来描述 thunk creator 函数的返回值的类型(即 thunk 函数的 TS 类型):

1
2
3
4
5
6
7
8
9
10
11
// Thunk 的 TS 类型
// 泛型参数1:ReturnType 表示 Thunk 函数的返回值,一般是 void
// 泛型参数2:RootState 表示 redux state 的 TS 类型
// 泛型参数3:可选的、要传递给 thunk 函数的额外参数的 TS 类型
// 泛型参数4:普通 action 的 TS 类型(普通 action 指的是对象类型的 action)
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
// 外层这个函数,是 thunk action creator
export const incrementAsync = (step?: number): AppThunk => {
// 内部 return 的这个函数,是真正的 thunk action
// 它有两个固定的形参,dispatch 和 getState,可按需要接受并使用
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";

// #1
type CounterStateType = {
value: number;
status: "idle" | "pending" | "fulfilled" | "rejected";
};
// #2
const initialState: CounterStateType = {
value: 0,
status: "idle",
};

// #7
type LoadingStatus = CounterStateType["status"];

const counterSlice = createSlice({
name: "counter",
// #3
initialState,
reducers: {
increment(state, action: PayloadAction<number | undefined>) {
state.value += action.payload || 1;
},
decrement(state, action: PayloadAction<number | undefined>) {
state.value -= action.payload ?? 1;
},
// #8
setPendingStatus(state, action: PayloadAction<LoadingStatus>) {
state.status = action.payload;
},
},
});

export default counterSlice.reducer;

// #4
export const selectPending = (state: RootState) =>
state.counter.status === "pending";
export const selectCount = (state: RootState) => state.counter.value;

// #9
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) => {
// #10
dispatch(setPendingStatus("pending"));
setTimeout(() => {
dispatch(decrement(step));
// #11
// dispatch({ type: 'counter/setPendingStatus', payload: 'fulfilled' })
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();
// #5
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
// slice 切片中的代码
// 导入 createAsyncThunk 函数
import { createAsyncThunk } from '@reduxjs/toolkit'
// 调用 createAsyncThunk 函数,创建 thunk
export const fetchPosts = createAsyncThunk('action-type 的前缀', async () => {
// 执行异步操作
// return 异步操作的结果
})

封装 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,
// #mark
color: colorSlice
}
})

export default store

export 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;

// #1
// 调用 createAsyncThunk 函数之后,它的返回值,是一个 Thunk creator 函数
// 如果想得到真正的 Thunk 函数,需要再次调用 getColorThunkCreator 函数
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();
// #5
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: {},
// #2
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) => {
// 可以执行任何异步操作
// console.log("ok", num);
// #1
// const response = await fetch("https://api.test.top/v1/color");
// const res = await response.json();
// return res.data
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";

// #1
type ColorStateType = {
value: string;
status: "idle" | "pending" | "fulfilled" | "rejected";
};

// #2
const initialState: ColorStateType = {
value: "#fff",
status: "idle",
};

const colorSlice = createSlice({
name: "color",
initialState,
reducers: {},
// #2
extraReducers(builder) {
builder
.addCase(getColorThunkCreator.pending, (state) => {
// #3
state.status = 'pending'
})
// #5 PayloadAction<string>
.addCase(getColorThunkCreator.fulfilled, (state, action: PayloadAction<string>) => {
state.value = action.payload;
// #4
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) => {
// 可以执行任何异步操作
// console.log("ok", num);
// #1
// const response = await fetch("https://api.test.top/v1/color");
// const res = await response.json();
// return res.data
const r = await randomColor();
return r;
}
);

// #6
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);
// #1
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) => {
// #2
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();
// #1
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 {
// #1
const controller = new AbortController();
// #3
setTimeout(() => controller.abort(), 500);
const response = await fetch("https://api.test.top/v1/color", {
// #2
signal: controller.signal,
});
const res = await response.json();
return res.data;
} catch (err) {
// #4
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
// #1
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import type { ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "@/store/counterSlice";
import colorReducer from '@/store/colorSlice'
// #3
import storage from "redux-persist/lib/storage";
// #5
import { persistReducer, persistStore } from "redux-persist";
// #2
const rootReducer = combineReducers({
counter: counterReducer,
color: colorReducer,
});

// #4
const persistConfig = {
key: "root",
storage,
};

const store = configureStore({
// #6
reducer: persistReducer(persistConfig, rootReducer),
// #7
middleware(getDefaultMiddleware) {
return getDefaultMiddleware({
serializableCheck: false,
});
}
});

// #8
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";
// #1
import { PersistGate } from "redux-persist/integration/react";
// #2
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>
);