前言
redux-toolkit是目前redux官方推荐的编写redux逻辑的方法,针对redux的创建store繁琐、样板代码太多、依赖外部库等问题进行了优化,官方总结了四个特点是简易的/有想法的/强劲的/高效的,总结来看,就是更加的方便简单了。
从官方github的记录来看,该项目从2018年就开始了,从官方文档的更新可以看出,现在redux已经正式开始推广,其文档内容中结合了大量的redux-toolkit示例,具体可参考Redux中文官网或Redux官网,最新的redux4.2.0版本也把redux-toolkit定为最佳redux使用方式
而且原来的createStore方法已经被标识为弃用。
本文主要对redux-toolkit进行介绍,以下简称为RTK。如果对redux的概念有不清楚的请先了解后再来学习。本文示例代码见https://github.com/storepage/redux-toolkit-demo
使用介绍
安装包依赖
yarn add @reduxjs/toolkit react-redux
不再需要单独安装redux了
react配置
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import './index.css';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<Provider store={store}>
<App />
</Provider>
);
这里和之前react-redux基本没有差别,还是提供Provider的根组件,传入store树作为参数,但store的定义有些许区别,具体见下一节。(root组件渲染这里用了react 18的写法)
创建Store
/* app/store.ts */
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todo/todoSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
todo: todoReducer,
},
});
调用rtk的configureStore方法,该方法相当于集成了redux和redux-redux的createStore、combineReducers、middleware、enhancers,以及默认支持了扩展工具Redux DevTools。
参数选项为一个对象,包含以下内容
参数key值说明reducer创建reducer,传递给combineReducers使用middleware中间件,传递给applyMiddleware使用devTools扩展工具,默认为truepreloadedState初始state值,传递给createStoreenhancers增强 store,传递给createStore
数据获取和发起action
先来看下组件内如何使用的,这一步可以认为完全是react-redux的内容
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
decrement,
selectCount
} from './counterSlice';
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useAppDispatch();
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
</div>
);
}
示例为一个简单的计数器,自react支持hooks的写法后,react-redux也提供了获取state和dispatch一个action的自定义hooks,可以不再需要使用connect方法往props上绑定。
针对上面两个hooks,关于如何使用redux的数据,官方也给出了新的建议
- 任意组件都能从 Redux Store 中读取任意数据
- 任意组件都能通过 dispatch actions 引发状态更新(state updates)
目前官方文档上已经找不到之前关于pressenational和container component的概念(翻译不用,有叫做UI组件和容器组件,也有叫做智能组件和木偶组价)(这个概念是redux作者于2015年推荐的划分,喜欢考究的可以阅读Presentational and Container Components)
——以上截图出自目前找到的繁体版的文档,Read Me | Redux
redux官方不再推荐上面组件的划分,一方面原因应该是他把重心更多放在了redux本身上,至于用户怎么拆分组件,就交给用户思考好了。另一方面可能是之前connect的方法相比较会更影响性能。
定义切片Slice
rtk引入了新的定义slice,它是应用中对action和reducer逻辑的整合,通过createSlice方法进行创建
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state: CounterState) => {
state.value += 1;
},
decrement: (state: CounterState) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state: CounterState, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
这里的reducers参数既定义了reducers,又创建了关联的action。这个方法最终返回一个包含actions和reducer的对象。
可以看到这里对state进行了直接修改,表面看来是违背了redux禁止修改state原则,实际是因为引用了immer的库,总是返回一个安全的、不可变的更新值,极大的简化reducer的写法。注意该方式只能在createSlice和createReducer中编写。
从redux扩展工具可以看到,这里相当生成了{name}/{reducers.key}的type
注意这里reducer的action中如果要传入参数,只能是一个payload,如果是多个参数的情况,那就需要封装成一个payload的对象。
实际是用中可能需要对数据进行处理,reducer的定义除了支持一个方法,还可以提供一个预处理方法,因为reducer必须是纯函数,其他可变的参数就可以放到预处理函数中,预处理函数必须返回一个带有payload字段的对象
import { createSlice, PayloadAction, nanoid } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state: TodoState[], action: PayloadAction<TodoState>) {
const { id, text } = action.payload;
state.push({ id, text, completed: false });
},
prepare(text: string) {
/* nanoid 随机一个字符串 */
return { payload: { text, id: nanoid() } };
},
},
toggleTodo(state: TodoState[], action: PayloadAction<string>) {
const todo = state.find((todo: { id: any }) => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
rtk还提供了一个nanoid方法,用于生成一个固定长度的随机字符串,类似uuid功能。
可以打印dispatch(addTodo(text))的结果看到,返回了一个带有payload字段的action
副作用处理
rtk集成了redux-thunk来处理异步事件,所以可以按照之前thunk的写法来写异步请求
// 外部的 thunk creator 函数
const fetchSomething = params => {
// 内部的 thunk 函数
return async (dispatch, getState) => {
try {
// thunk 内发起异步数据请求
const res = await someApi(params)
// 但数据响应完成后 dispatch 一个 action
dispatch(someAction(res));
return res;
} catch (err) {
// 如果过程出错,在这里处理
}
}
}
/* 组件中发起action */
dispatch(fetchSomething(params))
rtk提供新的方法**
createAsyncThunk
**支持thunk,这也是redux推荐的方式
export const incrementAsync = createAsyncThunk('counter/fetchCount', async (amount: number) => {
const response = await fetchCount(amount);
return response.data;
});
export const counterSlice = createSlice({
name: 'counter',
initialState,
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = 'failed';
});
},
});
createAsyncThunk 可以写在任何一个slice的extraReducers中,它接收2个参数,
- 生成action的type值,这里type是要自己定义,不像是createSlice自动生成type,这就要注意避免命名冲突问题了(如果createSlice定义了相当的name和方法,也是会冲突的)
- 包含数据处理的promise,首先会dispatch一个action类型为'counter/fetchCount/pending',当异步请求完成后,根据结果成功或是失败,决定dispatch出action的类型为'counter/fetchCount/fulfilled'或'counter/fetchCount/rejected',这三个action可以在slice的extraReducers中进行处理。这个promise也只接收2个参数,分别是payload和包含了dispatch、getState的thunkAPI对象,所以除了在slice的extraReducers中处理之外,createAsyncThunk中也可以调用任意的action,这样就很像原本thunk的写法了,并不推荐
export const incrementAsync = createAsyncThunk('counter/fetchCount',
async (amount: number, thunkApi) => {
const response = await fetchCount(amount);
thunkApi.dispatch(counterSlice.actions.incrementByAmount(response.data));
return response.data;
}
);
另外, extraReducers除了上面链式方法外,还可以定义为也可以是一个对象,结构为{[type]: function(state, action)},这是一种遗留的语法,虽然支持,但不被官方推荐。
extraReducers: {
[incrementAsync.pending.type]: (state) => {
state.status = 'loading';
},
[incrementAsync.fulfilled.type]: (state, action) => {
state.status = 'idle';
state.value += action.payload;
},
[incrementAsync.rejected.type]: (state) => {
state.status = 'failed';
},
},
dispatch了createAsyncThunk的action会捕获所有的异常,最终返回一个action
如果要想要获取createAsyncThunk的promose里返回的数据,并且自己处理try/catch,可以调用unwrap方法
async () => {
try {
const payload = await dispatch(incrementAsync(incrementValue)).unwrap();
console.log(payload);
} catch (error) {
// deal with error
}
}
进阶使用
Selector使用缓存
当useSelector方法涉及到复杂逻辑运算时,且返回一个对象的时候,每次运行都返回了一个新的引用值,会使组件重新渲染,即使返回的数据内容并没有改变,如下带有过滤的todoList所示
const list = useSelector((state: RootState) => {
const { todo, visibilityFilter } = state;
switch (visibilityFilter) {
case VisibilityFilters.SHOW_ALL:
return todo;
case VisibilityFilters.SHOW_COMPLETED:
return todo.filter((t: TodoState) => t.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todo.filter((t: TodoState) => !t.completed);
default:
throw new Error('Unknown filter: ' + visibilityFilter);
}
});
为了解决这个问题,可以使用Reselect库,它是一个创建记忆化selector的库,只有在输入发生变化时才会重新计算结果,rtk正是集成了这个库,并把它导出为createSelector函数,上面的selector就可以做如下改写
const selectTodos = (state: RootState) => state.todo;
const selectFilter = (state: RootState) => state.visibilityFilter;
// 创建记忆化selector
const selectList = createSelector(selectTodos, selectFilter, (todo, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todo;
case VisibilityFilters.SHOW_COMPLETED:
return todo.filter((t: TodoState) => t.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todo.filter((t: TodoState) => !t.completed);
default:
throw new Error('Unknown filter: ' + filter);
}
});
// 使用记忆化selector
const list = useSelector((state: RootState) => selectList(state));
createSelector接收一个或多个selector输入函数(逐个传入或作为一个数组)和一个selector输出函数,其中每个输入函数的出参会作为最后一个输出函数的入参。
另外,上述问题还可以通过useSelector传入第二个参数比较函数来解决。
范式化state结构
“范式化 state”是指:
- 我们 state 中的每个特定数据只有一个副本,不存在重复。
- 已范式化的数据保存在查找表中,其中项目 ID 是键,项本身是值。
- 也可能有一个特定项用于保存所有 ID 的数组。
根据以上定义,决定了它的结构形式为
{
ids: ["user1", "user2", "user3"],
entities: {
"user1": {id: "user1", firstName, lastName},
"user2": {id: "user2", firstName, lastName},
"user3": {id: "user3", firstName, lastName},
}
}
这样的结构类似于字典,方便增删改查功能。rtk提供了createEntityAdapter api,对范式化结构的存储进行一系列标准化操作。
import {
createSlice,
PayloadAction,
createEntityAdapter,
nanoid,
EntityState
} from '@reduxjs/toolkit';
import { RootState } from '../../app/store';
export interface TodoPayload {
todoId: string;
text: string;
completed?: boolean;
createdTimestamp: number;
}
/* 创建EntityAdapter */
const todoAdapter = createEntityAdapter<TodoPayload>({
/* 默认值为id */
selectId: (todo) => todo.todoId,
/* 对ids进行排序,方法与Array.sort相同,如果不提供,不能保证ids顺序 */
sortComparer: (a, b) => a.createdTimestamp - b.createdTimestamp,
});
const todosSlice = createSlice({
name: 'todosEntity',
initialState: todoAdapter.getInitialState(),
reducers: {
/* 增 */
addTodo: {
reducer(state: EntityState<TodoPayload>, action: PayloadAction<TodoPayload>) {
todoAdapter.addOne(state, action.payload);
},
prepare(text: string) {
return {
payload: {
text,
todoId: nanoid(),
createdTimestamp: Date.now(),
},
};
},
},
/* 删 */
removeTodo(state: EntityState<TodoPayload>, action: PayloadAction<string>) {
todoAdapter.removeOne(state, action.payload);
},
/* 改 */
toggleTodo(state: EntityState<TodoPayload>, action: PayloadAction<string>) {
const todo = state.entities[action.payload];
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
/* 查 */
export const { selectAll: selectAllTodos } = todoAdapter.getSelectors((state: RootState) => state.todoEntity);
/* action */
export const { actions: todoActions } = todosSlice;
/* reducer */
export default todosSlice.reducer;
掌握该结构主要包含三个要点
- 创建一个特定id标识、支持排序的EntityAdapter
- 获取state的初始值getInitialState,除了默认的ids和entities外,还可以添加自定义字段
- 一系列的增删改查方法,这些方法大都需要传入当前的state和要操作的数据
使用 Redux-Persist
如果要使用Redux-Persist的话,需要特定地忽略它dispatch的所有的action types。参考Question: How to use this with redux-persist? #121
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
import App from './App'
import rootReducer from './reducers'
const persistConfig = {
key: 'root',
version: 1,
storage
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
})
})
let persistor = persistStore(store)
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root')
)
上面rootReducer写法同之前的reducer一样,需要使用combineReducers整合
import { combineReducers } from 'redux';
import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todos/todoSlice';
import todoEntityReducer from '../features/todosEntity/todoSlice';
import visibilityFilterReducer from '../features/filters/filtersSlice';
export default combineReducers({
counter: counterReducer,
todo: todoReducer,
todoEntity: todoEntityReducer,
visibilityFilter: visibilityFilterReducer,
});
版权归原作者 成江 所有, 如有侵权,请联系我们删除。