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了
- 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的写法)
- /* 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 | 扩展工具,默认为true |
preloadedState | 初始state值,传递给createStore |
enhancers | 增强 store,传递给createStore |
先来看下组件内如何使用的,这一步可以认为完全是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的数据,官方也给出了新的建议
目前官方文档上已经找不到之前关于pressenational和container component的概念(翻译不用,有叫做UI组件和容器组件,也有叫做智能组件和木偶组价)(这个概念是redux作者于2015年推荐的划分,喜欢考究的可以阅读Presentational and Container Components)
——以上截图出自目前找到的繁体版的文档,Read Me | Redux
redux官方不再推荐上面组件的划分,一方面原因应该是他把重心更多放在了redux本身上,至于用户怎么拆分组件,就交给用户思考好了。另一方面可能是之前connect的方法相比较会更影响性能。
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
) => { - 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
) { - 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
) { - 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个参数,
- 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
- }
- }
当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”是指:
根据以上定义,决定了它的结构形式为
- {
- 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
, action: PayloadAction ) { - todoAdapter.addOne(state, action.payload);
- },
- prepare(text: string) {
- return {
- payload: {
- text,
- todoId: nanoid(),
- createdTimestamp: Date.now(),
- },
- };
- },
- },
- /* 删 */
- removeTodo(state: EntityState
, action: PayloadAction ) { - todoAdapter.removeOne(state, action.payload);
- },
- /* 改 */
- toggleTodo(state: EntityState
, action: PayloadAction ) { - 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;
掌握该结构主要包含三个要点
如果要使用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')
- )