上一篇文章react-redux 使用与实现原理介绍了在react中使用react-redux将redux中的store注入到组件中从而自动地根据store变化render组件, 但是仍然没有解决注册action type与action creator比较麻烦,reducer 函数庞大复杂等问题。
本篇文章将介绍redux官方推荐的redux toolkit工具集,使用redux toolkit可以配置 store、定义 reducer,更方便的构建不可变数据state、方便地创建整个状态的切片 slice,无需手动编写任何 action creator 或者 action type
什么是 redux-toolkit
redux-toolkit 是官方的, 开箱即用的且强大的用于高效开发应用的redux工具集,它有如下几个特点:
- 使用
configureStore创建redux store简单高效, 减少很多模板代码 - 使用
createReducer将action type映射到reducer函数,而不是编写switch...case语句 - 可以添加任意中间件,内置
redux-thunk中间件, 默认开启 redux-dev-tools - 集成
immer, 在reducer中创建不可变数据将变得非常简单 - 使用
createSelector创建可记忆的selector函数, 优化性能
使用
以下面的例子为例我们将实现一个简单的计数器以及一个点击add todo按钮在列表中增加一项的功能,效果如下:
store.ts:
import { configureStore, createSlice } from '@reduxjs/toolkit';
interface CounterState {
value: number
}
const counterInitState: CounterState = {
value: 0,
}
const counterSlice = createSlice({
name: 'counter',
initialState: counterInitState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
}
}
});
interface TodoState {
todos: string[]
}
const todoInitState: TodoState = {
todos: []
}
const todoSlice = createSlice({
name: 'todo',
initialState: todoInitState,
reducers: {
addTodo: (state) => {
state.todos = ['Use Redux']
}
}
})
export const counterActions = counterSlice.actions
export const todoActions = todoSlice.actions
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
todo: todoSlice.reducer,
},
})
App.tsx:
import React from 'react';
import logo from './logo.svg';
import styles from './App.module.css';
import { useDispatch, useSelector } from 'react-redux';
import { todoActions, counterActions } from './store';
function App() {
const dispatch = useDispatch();
const state: any = useSelector((state: any) => state)
const handleDecrement = () => {
dispatch(counterActions.decrement())
}
const handleIncrement = () => {
dispatch(counterActions.increment())
}
const handleAddTodo = () => {
dispatch(todoActions.addTodo())
}
return (
<div className={styles.App}>
<header className={styles['App-header']}>
<img src={logo} className={styles['App-logo']} alt="logo" />
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={handleDecrement}
>
-
</button>
<span className={styles.value}>{state.counter.value}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={handleIncrement}
>
+
</button>
</div>
<div className={styles.row}>
<ul>
todos:
{
state.todo.todos.map((item: any) => (<li key={item}>{item}</li>))
}
</ul>
<button
className={styles.button}
aria-label="Add todo"
onClick={handleAddTodo}
>
add todo
</button>
</div>
</div>
</header>
</div>
);
}
export default App;
可以看到在上面的代码中通过 createSlice 函数创建了两个状态切片 counterSlice 和 todoSlice, 并通过 configureStore 函数创建了 store。这样便可以通过 counterSlice 和 todoSlice 获取对应的 action,并且在reducer中可以直接操作state,内置的 immer 库,会帮我们构建一个不可变数据state并返回。
实现
configureStore
首先来看 configureStore, 实际上是在 configureStore 中调用了 redux 的 combineReducers 来组合各个 reducer 然后再返回 store
import { combineReducers, createStore } from 'redux';
function isPlainObject(value: any) {
if (typeof value !== "object" || value === null) return false;
return Object.getPrototypeOf(value) === Object.prototype;
}
function configureStore(options: any) {
let { reducer } = options;
let rootReducer;
if (typeof reducer === "function") {
rootReducer = reducer;
} else if (isPlainObject(reducer)) {
rootReducer = combineReducers(reducer);
}
return createStore(rootReducer);
}
export default configureStore;
createSlice
接下来实现 redux toolkit 中十分重要的 createSlice 函数的实现,createSlice中用到了 createAction 函数,createAction 函数用于创建 action creator 给 dispatch 函数派发,先来实现 createAction
createAction 接收一个 type, 返回一个 actionCreator, 该 actionCreator 返回一个 action, action.type 即为传入的 type 参数
function createAction(type: string) {
function actionCreator(...args: any) {
return {
type: type,
payload: args[0]
};
}
actionCreator.toString = function () {
return "" + type;
}
actionCreator.type = type;
return actionCreator;
}
export default createAction;
此外 createSlice 函数也用到了 createReducer 函数, 它接收一个初始 state, 和一个 reducers 对象, reducers 对象的 key 即为 action.type, value 即为真正的 reducer; createReducer 使得用户可以用对象方式来配置 reducer, 而不是用一个内部使用 switch...case 的函数来定义 reducer
function createReducer(initialState: any, reducers = {} as any) {
return function (state = initialState, action: { type: string }) {
let reducer = reducers[action.type];
if (reducer) return reducer(state, action);
return state;
}
}
export default createReducer;
createSlice 实际上就是集成了 createReducer 和 createAction, 返回一个 reducer 函数和 actions 对象
import { createReducer, createAction } from './'
function createSlice(options: {
name: string;
initialState: any;
reducers: Record<string, Function>;
}) {
let { name, initialState = {}, reducers = {} } = options;
let actions: any = {};
const prefixReducers: any = {};
Object.keys(reducers).forEach((key) => {
const type = getType(name, key);
actions[key] = createAction(type);
prefixReducers[type] = reducers[key];
})
let reducer = createReducer(initialState, prefixReducers);
return {
name,
reducer,
actions
};
}
function getType(slice: string, actionKey: string) {
return slice + "/" + actionKey;
}
export default createSlice;
使用 redux toolkit 能极大地方便用户使用,做到开箱即用,内置的很多诸如 immer, reselect, redux-thunk 等库能帮助用户快速开发应用,可以说是现在使用 redux 的最佳实践了。
曾经
dva作为redux的工具集也流行过一段时间,但是redux toolkit在使用上比dva方便很多, 而且redux toolkit内部也内置了本文没有介绍的RTK Query,RTK Query是一个强大的数据获取和缓存工具。不过dva内置的副作用处理中间件是redux saga而redux toolkit内置的是redux thunk, 下一篇文章redux middleware 的使用与实现原理我将介绍redux的中间件的使用及其实现原理。