• Redux最新实践指南之Redux-Toolkit


    前言

    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配置

    1. import React from 'react';
    2. import { createRoot } from 'react-dom/client';
    3. import { Provider } from 'react-redux';
    4. import { store } from './app/store';
    5. import App from './App';
    6. import './index.css';
    7. const container = document.getElementById('root')!;
    8. const root = createRoot(container);
    9. root.render(
    10. <Provider store={store}>
    11. <App />
    12. Provider>
    13. );

    这里和之前react-redux基本没有差别,还是提供Provider的根组件,传入store树作为参数,但store的定义有些许区别,具体见下一节。(root组件渲染这里用了react 18的写法)

    创建Store

    1. /* app/store.ts */
    2. import { configureStore } from '@reduxjs/toolkit';
    3. import counterReducer from '../features/counter/counterSlice';
    4. import todoReducer from '../features/todo/todoSlice';
    5. export const store = configureStore({
    6. reducer: {
    7. counter: counterReducer,
    8. todo: todoReducer,
    9. },
    10. });

    调用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

    数据获取和发起action

    先来看下组件内如何使用的,这一步可以认为完全是react-redux的内容

    1. import React, { useState } from 'react';
    2. import { useDispatch, useSelector } from 'react-redux';
    3. import {
    4. decrement,
    5. selectCount
    6. } from './counterSlice';
    7. export function Counter() {
    8. const count = useSelector(selectCount);
    9. const dispatch = useAppDispatch();
    10. return (
    11. <div>
    12. <div className={styles.row}>
    13. <button
    14. className={styles.button}
    15. aria-label="Decrement value"
    16. onClick={() => dispatch(decrement())}
    17. >
    18. -
    19. button>
    20. <span className={styles.value}>{count}span>
    21. <button
    22. className={styles.button}
    23. aria-label="Increment value"
    24. onClick={() => dispatch(increment())}
    25. >
    26. +
    27. button>
    28. div>
    29. div>
    30. );
    31. }

    示例为一个简单的计数器,自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方法进行创建

    1. import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    2. export interface CounterState {
    3. value: number;
    4. status: 'idle' | 'loading' | 'failed';
    5. }
    6. const initialState: CounterState = {
    7. value: 0,
    8. status: 'idle',
    9. };
    10. export const counterSlice = createSlice({
    11. name: 'counter',
    12. initialState,
    13. reducers: {
    14. increment: (state: CounterState) => {
    15. state.value += 1;
    16. },
    17. decrement: (state: CounterState) => {
    18. state.value -= 1;
    19. },
    20. // Use the PayloadAction type to declare the contents of `action.payload`
    21. incrementByAmount: (state: CounterState, action: PayloadAction) => {
    22. state.value += action.payload;
    23. },
    24. },
    25. });
    26. export const { increment, decrement, incrementByAmount } = counterSlice.actions;
    27. 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字段的对象

    1. import { createSlice, PayloadAction, nanoid } from '@reduxjs/toolkit';
    2. const todosSlice = createSlice({
    3. name: 'todos',
    4. initialState: [],
    5. reducers: {
    6. addTodo: {
    7. reducer(state: TodoState[], action: PayloadAction) {
    8. const { id, text } = action.payload;
    9. state.push({ id, text, completed: false });
    10. },
    11. prepare(text: string) {
    12. /* nanoid 随机一个字符串 */
    13. return { payload: { text, id: nanoid() } };
    14. },
    15. },
    16. toggleTodo(state: TodoState[], action: PayloadAction) {
    17. const todo = state.find((todo: { id: any }) => todo.id === action.payload);
    18. if (todo) {
    19. todo.completed = !todo.completed;
    20. }
    21. },
    22. },
    23. });

     rtk还提供了一个nanoid方法,用于生成一个固定长度的随机字符串,类似uuid功能。

    可以打印dispatch(addTodo(text))的结果看到,返回了一个带有payload字段的action

    副作用处理

    rtk集成了redux-thunk来处理异步事件,所以可以按照之前thunk的写法来写异步请求

    1. // 外部的 thunk creator 函数
    2. const fetchSomething = params => {
    3. // 内部的 thunk 函数
    4. return async (dispatch, getState) => {
    5. try {
    6. // thunk 内发起异步数据请求
    7. const res = await someApi(params)
    8. // 但数据响应完成后 dispatch 一个 action
    9. dispatch(someAction(res));
    10. return res;
    11. } catch (err) {
    12. // 如果过程出错,在这里处理
    13. }
    14. }
    15. }
    1. /* 组件中发起action */
    2. dispatch(fetchSomething(params))

     rtk提供新的方法createAsyncThunk支持thunk,这也是redux推荐的方式

    1. export const incrementAsync = createAsyncThunk('counter/fetchCount', async (amount: number) => {
    2. const response = await fetchCount(amount);
    3. return response.data;
    4. });
    5. export const counterSlice = createSlice({
    6. name: 'counter',
    7. initialState,
    8. extraReducers: (builder) => {
    9. builder
    10. .addCase(incrementAsync.pending, (state) => {
    11. state.status = 'loading';
    12. })
    13. .addCase(incrementAsync.fulfilled, (state, action) => {
    14. state.status = 'idle';
    15. state.value += action.payload;
    16. })
    17. .addCase(incrementAsync.rejected, (state) => {
    18. state.status = 'failed';
    19. });
    20. },
    21. });

    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的写法了,并不推荐
    1. export const incrementAsync = createAsyncThunk('counter/fetchCount',
    2. async (amount: number, thunkApi) => {
    3. const response = await fetchCount(amount);
    4. thunkApi.dispatch(counterSlice.actions.incrementByAmount(response.data));
    5. return response.data;
    6. }
    7. );

      另外, extraReducers除了上面链式方法外,还可以定义为也可以是一个对象,结构为{[type]: function(state, action)},这是一种遗留的语法,虽然支持,但不被官方推荐。

    1. extraReducers: {
    2. [incrementAsync.pending.type]: (state) => {
    3. state.status = 'loading';
    4. },
    5. [incrementAsync.fulfilled.type]: (state, action) => {
    6. state.status = 'idle';
    7. state.value += action.payload;
    8. },
    9. [incrementAsync.rejected.type]: (state) => {
    10. state.status = 'failed';
    11. },
    12. },

     dispatch了createAsyncThunk的action会捕获所有的异常,最终返回一个action

    如果要想要获取createAsyncThunk的promose里返回的数据,并且自己处理try/catch,可以调用unwrap方法

    1. async () => {
    2. try {
    3. const payload = await dispatch(incrementAsync(incrementValue)).unwrap();
    4. console.log(payload);
    5. } catch (error) {
    6. // deal with error
    7. }
    8. }

    进阶使用

    Selector使用缓存

    当useSelector方法涉及到复杂逻辑运算时,且返回一个对象的时候,每次运行都返回了一个新的引用值,会使组件重新渲染,即使返回的数据内容并没有改变,如下带有过滤的todoList所示

    1. const list = useSelector((state: RootState) => {
    2. const { todo, visibilityFilter } = state;
    3. switch (visibilityFilter) {
    4. case VisibilityFilters.SHOW_ALL:
    5. return todo;
    6. case VisibilityFilters.SHOW_COMPLETED:
    7. return todo.filter((t: TodoState) => t.completed);
    8. case VisibilityFilters.SHOW_ACTIVE:
    9. return todo.filter((t: TodoState) => !t.completed);
    10. default:
    11. throw new Error('Unknown filter: ' + visibilityFilter);
    12. }
    13. });

    为了解决这个问题,可以使用Reselect库,它是一个创建记忆化selector的库,只有在输入发生变化时才会重新计算结果,rtk正是集成了这个库,并把它导出为createSelector函数,上面的selector就可以做如下改写

    1. const selectTodos = (state: RootState) => state.todo;
    2. const selectFilter = (state: RootState) => state.visibilityFilter;
    3. // 创建记忆化selector
    4. const selectList = createSelector(selectTodos, selectFilter, (todo, filter) => {
    5. switch (filter) {
    6. case VisibilityFilters.SHOW_ALL:
    7. return todo;
    8. case VisibilityFilters.SHOW_COMPLETED:
    9. return todo.filter((t: TodoState) => t.completed);
    10. case VisibilityFilters.SHOW_ACTIVE:
    11. return todo.filter((t: TodoState) => !t.completed);
    12. default:
    13. throw new Error('Unknown filter: ' + filter);
    14. }
    15. });
    16. // 使用记忆化selector
    17. const list = useSelector((state: RootState) => selectList(state));

    createSelector接收一个或多个selector输入函数(逐个传入或作为一个数组)和一个selector输出函数,其中每个输入函数的出参会作为最后一个输出函数的入参。

    另外,上述问题还可以通过useSelector传入第二个参数比较函数来解决。

    范式化state结构

    “范式化 state”是指:

    • 我们 state 中的每个特定数据只有一个副本,不存在重复。
    • 已范式化的数据保存在查找表中,其中项目 ID 是键,项本身是值。
    • 也可能有一个特定项用于保存所有 ID 的数组。

    根据以上定义,决定了它的结构形式为

    1. {
    2. ids: ["user1", "user2", "user3"],
    3. entities: {
    4. "user1": {id: "user1", firstName, lastName},
    5. "user2": {id: "user2", firstName, lastName},
    6. "user3": {id: "user3", firstName, lastName},
    7. }
    8. }

    这样的结构类似于字典,方便增删改查功能。rtk提供了createEntityAdapter api,对范式化结构的存储进行一系列标准化操作。

    1. import {
    2. createSlice,
    3. PayloadAction,
    4. createEntityAdapter,
    5. nanoid,
    6. EntityState
    7. } from '@reduxjs/toolkit';
    8. import { RootState } from '../../app/store';
    9. export interface TodoPayload {
    10. todoId: string;
    11. text: string;
    12. completed?: boolean;
    13. createdTimestamp: number;
    14. }
    15. /* 创建EntityAdapter */
    16. const todoAdapter = createEntityAdapter<TodoPayload>({
    17. /* 默认值为id */
    18. selectId: (todo) => todo.todoId,
    19. /* 对ids进行排序,方法与Array.sort相同,如果不提供,不能保证ids顺序 */
    20. sortComparer: (a, b) => a.createdTimestamp - b.createdTimestamp,
    21. });
    22. const todosSlice = createSlice({
    23. name: 'todosEntity',
    24. initialState: todoAdapter.getInitialState(),
    25. reducers: {
    26. /* 增 */
    27. addTodo: {
    28. reducer(state: EntityState, action: PayloadAction) {
    29. todoAdapter.addOne(state, action.payload);
    30. },
    31. prepare(text: string) {
    32. return {
    33. payload: {
    34. text,
    35. todoId: nanoid(),
    36. createdTimestamp: Date.now(),
    37. },
    38. };
    39. },
    40. },
    41. /* 删 */
    42. removeTodo(state: EntityState, action: PayloadAction) {
    43. todoAdapter.removeOne(state, action.payload);
    44. },
    45. /* 改 */
    46. toggleTodo(state: EntityState, action: PayloadAction) {
    47. const todo = state.entities[action.payload];
    48. if (todo) {
    49. todo.completed = !todo.completed;
    50. }
    51. },
    52. },
    53. });
    54. /* 查 */
    55. export const { selectAll: selectAllTodos } = todoAdapter.getSelectors((state: RootState) => state.todoEntity);
    56. /* action */
    57. export const { actions: todoActions } = todosSlice;
    58. /* reducer */
    59. export default todosSlice.reducer;

    掌握该结构主要包含三个要点

    1. 创建一个特定id标识、支持排序的EntityAdapter
    2. 获取state的初始值getInitialState,除了默认的ids和entities外,还可以添加自定义字段
    3. 一系列的增删改查方法,这些方法大都需要传入当前的state和要操作的数据

    使用 Redux-Persist

    如果要使用Redux-Persist的话,需要特定地忽略它dispatch的所有的action types。参考Question: How to use this with redux-persist? #121

    1. import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
    2. import {
    3. persistStore,
    4. persistReducer,
    5. FLUSH,
    6. REHYDRATE,
    7. PAUSE,
    8. PERSIST,
    9. PURGE,
    10. REGISTER
    11. } from 'redux-persist'
    12. import storage from 'redux-persist/lib/storage'
    13. import { PersistGate } from 'redux-persist/integration/react'
    14. import App from './App'
    15. import rootReducer from './reducers'
    16. const persistConfig = {
    17. key: 'root',
    18. version: 1,
    19. storage
    20. }
    21. const persistedReducer = persistReducer(persistConfig, rootReducer)
    22. const store = configureStore({
    23. reducer: persistedReducer,
    24. middleware: getDefaultMiddleware({
    25. serializableCheck: {
    26. ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
    27. }
    28. })
    29. })
    30. let persistor = persistStore(store)
    31. ReactDOM.render(
    32. <Provider store={store}>
    33. <PersistGate loading={null} persistor={persistor}>
    34. <App />
    35. PersistGate>
    36. Provider>,
    37. document.getElementById('root')
    38. )

  • 相关阅读:
    智元机器人岗位内推
    Spark实现TopN
    应用层协议的实现
    【Linux】一些工具的简单使用,vim/gcc/gdb/make
    leetcode:714. 买卖股票的最佳时机含手续费
    css基本介绍
    Matlab信号处理1:模拟去除信号噪声
    vue3 响应式 API 之 reactive
    【学懂数据结构】算法及其复杂度分析
    ROS基础-ROS msg发布订阅:嵌套自定义类型 数组
  • 原文地址:https://blog.csdn.net/cscj2010/article/details/125705530