React 状态管理:从现在开始拥抱redux-toolkit

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

在之前的文章:从零开始学习React-5:状态与状态管理,我们简单的介绍目前最流行的状态管理库 redux。今天我们介绍由 redux 团队推出的,可以更高效、更方便使用 redux 的另一个工具:Redux-Toolkit(简称:RTK)

为什么是 redux-toolkit

redux 被设计的非常灵活,这也带来了一些问题,例如如果我们直接使用 redux 会有如下几点不便之处:

  1. 模板代码太多

    在上一篇文章示例中可能感觉还不明显,其实正常项目中,每一个 Action 一般都会有这样几个模板代码:

    • 声明一个静态的 type 字符串,例如 const ADD_TODO = 'addTodo';
    • 声明一个 action creator 函数,例如 export const addTodo = (data) => ({ type: ADD_TODO, data });
    • reducer 函数内需要根据 action 的 type 写 switch - case

    action 对象的 type 一般都是唯一的一个字符串,因为 dispatch 一个 action 时,所有的 reducer 函数都会收到。

    这带来了一个隐藏的问题,如果我们在多个 reducer 函数中 case 了 一个相同的 type 字符串,这两个 reducer 都会执行动作,改变状态。

    这也就是为什么 Redux 推荐我们使用 action creator 函数,这可以有效的避免写错导致bug。

  2. 仅安装 redux 可能无法胜任。我们一般还需要安装 比如 redux-thunk(异步)、immer(状态不可变) 等库来配合 redux 工作。

    这就导致了 redux 的配置比较复杂,很难做到开箱即用,即使有的配置已经成为了某种意义上的 “ 最佳实践 ”。

RTK 通过几个函数很好的帮我们解决了这些细节问题,几乎做到了开箱即用。

如何使用 RTK

这里我们还是使用之前文章的 TodoList 来作为例子,我们将它修改成使用 RTK 实现。

  1. 安装 RTK : yarn add @reduxjs/toolkit react-redux

    查看其依赖我们可以看到 RTK 已经依赖了 redux 与 redux-thunk了

    1
    2
    3
    4
    5
    6
    "dependencies": {
    "immer": "^9.0.7",
    "redux": "^4.1.2",
    "redux-thunk": "^2.4.1",
    "reselect": "^4.1.5"
    },
  2. 创建 todo.slice.js Slice 切片

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
import { createSlice } from "@reduxjs/toolkit";

const initialState = [];

// 固定格式,创建 slice 对象
const todoSlice = createSlice({
name: 'todo',
initialState,
// 相当于原来reducer中的case
reducers: {
// 后面文中我们将这类函数称之为 case
addTodo: (state, action) => {
state.push(action.payload);
},
delTodo: (state, action) => {
// 这里要注意不能使用之前的filter方式,因为immer的原因,我们必须直接操作state
const index = state.findIndex(item => item.id === action.payload);
state.splice(index, 1);
}
}
});

// 官方推荐使用 ES6 解构和导出语法
// 提取action creator 对象与reducer函数
const { reducer, actions } = todoSlice;
// 提取并导出根据reducers命名的 action creator 函数
export const { addTodo, delTodo } = actions;
// 导出 reducer 函数
export default reducer;

这里我们按照官方推荐使用 ES6 解构和导出语法,来获取 action creator 与 reducer。

从代码可以看出,通过 createSlice 函数,我们减少了绝大多数的模板代码,我们无需再写 type、action creator,这一切 RTK 都帮我们在背后默默实现了。

而且我们无需再关注状态不可变,我们只需像操作普通对象一样,在 reducers 中的 case 函数中随意操作对象,这一切都被 immer 帮我们搞定了。

你可能会好奇,最终通过 action creator 函数返回的是一个这样的 action 对象:

1
{type: 'todo/addTodo', payload: ...}

我们在 reducers 中写的 case 中拿到的 action 就是这样的固定格式,这一定需要注意,因为在我们过去使用 redux 时,并没有规范 action 对象的类型。

ps:可以看到 RTK 自动的帮我们将 type 命名为了 name + case函数名 这样的格式,这对于我们调试也是非常方便的,起名困难症表示 RTK 赛高!

注意:其实代码修改到这里,我们的程序就可以正常运行了,这也是 RTK 的一大优势,我们可以不必全部重新改造我们的程序,它没有增加我们的心智负担,并不需要我们重新去掌握一个库,而是通过这些工具性质的函数,帮助我们更好的使用 Redux。

  1. 使用 configureStore 函数创建 store 对象
1
2
3
4
5
6
7
8
9
import { configureStore } from "@reduxjs/toolkit"
import todoReducer from "./todo.slice";

export default configureStore({
reducer: {
todos: todoReducer
// ... 其他的 reducer
}
});

configureStore 函数接收一个配置redux的对象作为参数,具有如下选项:

  • reducer 必选参数,可以是一个 reducer 函数也可以是一个由 slice reducer 构成的对象

    如果是 reducer 函数,就直接将他作为根 reducer,如果是一个对象,就自动帮我们调用 combineReducers 函数来合并多个 reducer 创建根 reducer 函数

  • middleware 中间件

  • devTools 是否启用 Redux DevTools

更多配置,请查看文档:https://redux-toolkit.js.org/api/configureStore

剩余的使用是与原来一模一样的,我们无需改动其他代码!

其他 api 介绍

除了我们上面介绍的这种比较完整的改造,其实 RTK 还提供了很多粒度更小的 api,来方便我们改造原有的模板代码,这里我们简单介绍几个:

1. createAction

这个函数可以帮我们减少创建 action 时的模板代码,还有我们的例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// before
const ADD_TODO = 'ADD_TODO';
const DEL_TODO = 'DEL_TODO';

export const addTodo = (payload) => ({ type: ADD_TODO, payload });
export const delTodo = (payload) => ({ type: DEL_TODO, payload });

const action = addTodo({...});

// after
const addTodo = createAction('ADD_TODO')
const delTodo = createAction('DEL_TODO')

const action = addTodo({...});

我们完全不需要再写 action creator 函数了,直接使用 createAction 就可以帮我们创建,它的源码其实也很简单:

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
export function createAction(type: string, prepareAction?: Function): any {
// action creator 函数
function actionCreator(...args: any[]) {
if (prepareAction) {
let prepared = prepareAction(...args)
if (!prepared) {
throw new Error('prepareAction did not return an object')
}

return {
type,
payload: prepared.payload,
...('meta' in prepared && { meta: prepared.meta }),
...('error' in prepared && { error: prepared.error }),
}
}
//action creator 函数返回的固定样式的 action 对象
return { type, payload: args[0] }
}

// 自定义 action creator 函数的 toString
actionCreator.toString = () => `${type}`

actionCreator.type = type

actionCreator.match = (action: Action<unknown>): action is PayloadAction =>
action.type === type

return actionCreator
}

2.createReducer()

这个函数可以帮我们简化 reducer 函数的创建,并在其内部使用了 immer ,让我们可以直接操作 state,而不用再考虑不可变的问题,极大的简化了更新状态的逻辑。我们一般用 createAction 配合使用

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
// before
const initState = [];
//模板代码 action type 常量
const ADD_TODO = 'ADD_TODO';
const DEL_TODO = 'DEL_TODO';
//模板代码用于创建 action 的 action creator 函数
export const addTodo = (payload) => ({ type: ADD_TODO, payload });
export const delTodo = (payload) => ({ type: DEL_TODO, payload });
// reducer 函数
export default function todoReducer(preState = initState, action) {
const { type, payload } = action;
switch (type) {
case ADD_TODO:
return [...preState, payload];
case DEL_TODO:
return preState.filter(item => item.id !== payload);
default:
return preState;
}
}

//after
import { createReducer, createAction } from "@reduxjs/toolkit";
const initialState = [];
//使用 createAction 函数创建 action creator
export const addTodo = createAction('ADD_TODO')
export const delTodo = createAction('DEL_TODO')
//使用createReducer生成reducer函数
export default createReducer(initialState, {
[addTodo]: (state, action) => {
state.push(action.payload)
},
[delTodo]: (state, action) => {
const index = state.findIndex(item => item.id === action.payload);
state.splice(index, 1);
}
})

现在我们的代码变得更加简洁,几乎没有了模板代码,非常的优雅!

除了上面我演示的这种方式以外,RTK 还支持使用 Builder Callback 这种方式,大家要是感兴趣可以自行查看文档:https://redux-toolkit.js.org/api/createReducer#usage-with-the-builder-callback-notation

好了,关于 RTK 的话题我们就介绍到这里了,更多有趣的内容请关注我的专栏!