从 Redux 到 Redux Toolkit

177 阅读8分钟

什么是Redux?

数据流更新动画

Redux 是一个使用叫做 “action” 的事件来管理和更新应用状态的模式和工具库 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

在以下情况下使用 Redux:

  • 应用中有很多 state 在多个组件中需要使用
  • 应用 state 会随着时间的推移而频繁更新
  • 更新 state 的逻辑很复杂
  • 中型和大型代码量的应用,很多人协同开发

并非所有应用程序都需要 Redux。 花一些时间思考你正在构建的应用程序类型,并决定哪些工具最能帮助解决你正在处理的问题。

react + redux

核心概念

  • state

    全局共享的状态

// ToDo 的初始State
const initState: IToDo[] = [
    {
        id: '1',
        todo: "睡觉",
        isDone: false
    },
    {
        id: '2',
        todo: "吃饭",
        isDone: false
    },
    {
        id: '3',
        todo: "打豆豆",
        isDone: false
    },
];
  • reducer

    修改 state 的纯函数

    接收 初始state 和 action 为参数

export const todoReducer = (state = initState, action: IAction) => {
    const { type, payload } = action;
    switch (type) {
        case "ADD_TODO":
            return [...state, payload];
        case "DEL_TODO":
            return state.filter(item => item.id !== payload);
        default:
            return state;
    }
}
  • action

    修改 state 的动作

    为对象时(同步),包含两个属性:type 表示做什么事;payload 额外的数据参数

    为函数时(异步)

// 添加todo
export const addTodo = (payload: IToDo) => ({ type: 'ADD_TODO', payload })
// 删除todo
export const deleteTodo = (payload: string) => ({ type: 'DEL_TODO', payload })
  • store

    redux 的核心模块,用于联接 action 和 reducer

    方法:

    1. getState 获取 state
    2. dispatch 接收 action ,触发 reducer
    3. subscribe 当 state 状态改变时,重新执行回调,重新渲染
// We recommend using the configureStore method of the @reduxjs/toolkit package, which replaces createStore.
import { createStore, combineReducers } from "redux"
import { todoReducer } from "./reducers/todo"
import { countReducer } from "./reducers/count"/*
combineReducers 内部,根据 key, 为每个 reducer 传入各自的 state,起到环境隔离作用
action.type 没有隔离, 如果 2 个 reducer 都有同名的 action.type, 则会同时触发。
当 dispatch 时,会触发所有的 reducer, 但是每个 reducer 只会处理自己的 state
*/// 合并多个 reducer
const rootReducer = combineReducers({
    todos: todoReducer,
    count: countReducer
});
​
const store = createStore(rootReducer);
​
export default store;

在组件中使用redux

// src/index.tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import store from './store';
​
const container = document.getElementById('root')!;
const root = createRoot(container);
​
root.render(<App />)
​
// 订阅 当 store 中的全局状态发生改变时,重新执行回调函数
store.subscribe(() => { root.render(<App />) })
// ToDo.tsx
const ToDo = () => {
  const todos = store.getState().todos
  const addTodos = () => {
    const todo: IToDo = {
      id: crypto.randomUUID(),
      todo: 'test',
      isDone: false
    }
    store.dispatch(addTodo(todo))
  }
  const deleteTodos = (id: string) => {
    store.dispatch(deleteTodo(id))
  }
  return (<>
    <>
      <div className="container">
        {
          todos.map(item => (<div className="item" key={item.id}>
            <span>{item.todo}</span> 
           <span onClick={() => { deleteTodos(item.id) }}>x</span>
          </div>))
        }
      </div>
      <button onClick={addTodos}>添加事项</button>
      <button onClick={() => { store.dispatch(todoSameName()) }}>触发相同的Action</button>
    </>
  </>)
}
// Count.tsx
const Counter = () => {
  const count = store.getState().count;
  const todos = store.getState().todos
  return (<>
    <div>{count}</div>
    <div>todos的数量:{todos.length}</div>
    <button onClick={() => { store.dispatch(addCount(3)) }}>+3</button>
    <button onClick={() => { store.dispatch(subCount(2)) }}>-2</button>
    <button onClick={() => { store.dispatch(mulCount(4)) }}>*4</button>
    <button onClick={() => { store.dispatch(divCount(2)) }}>/2</button>
    <button onClick={() => { store.dispatch(countSameName()) }}>触发相同的Action</button>
  </>)
}

redux-thunk 中间件处理异步

redux 的 dispatch 只能接收一个对象,但是有时候需要在 action creator 时进行异步操作,如果直接返回一个对象,Redux会无法处理异步操作,因为Redux只能处理纯对象的action。

redux-thunk中间件的作用是拦截action,如果action是一个函数而不是一个纯对象,redux-thunk会将dispatch和getState作为参数传递给该函数,并执行该函数。函数可以进行异步操作,并在适当的时候再次dispatch一个action,这个action可以是一个纯对象。

// store/index.ts
import { applyMiddleware } from "redux"
import thunk from "redux-thunk"
// ...
const store = createStore(rootReducer,applyMiddleware(thunk));
export default store
// actions/count.ts
// 异步加 3
import { Dispatch } from "redux";
export const addCountAsync = (payload: number) => {
    return (dispatch: Dispatch<IAction>) => {
        setTimeout(() => {
            dispatch(addCount(payload))
        }, 1000)
    }
}
// Count.tsx
store.dispatch(asyncAddCount(3));

react + react-redux

React-Redux 使用流程图

img

核心 API

  • mapStateToProps

    1. 它接收整个 store state 作为第一个参数(必传)的函数
    2. 每次 store state 变更时都会调用它
    3. 返回一个包含组件所需数据的普通对象,对象中的每个字段都将成为实际组件中的 prop,字段中的值将用于确定你的组件是否需要重新渲染
    const mapStateToProps = (state: IState, ownProps?) => {
      return {
        count: state.count,
        todos: state.todos
      };
    };
    

    默认情况下,React Redux 针对 mapStateToProps 返回的对象中的每个字段,使用 === 进行比较(“浅对比”检查)以判断其内容是否不同。如果任何字段发生更改,那么你的组件将被重新渲染,以便它可以接收更新的值作为 props。

  • mapDispatchToProps

    • 函数形式

      1. 一个接收 dispatch并返回一个普通对象的函数
      2. 在内部调用 dispatch(),并接收 action 对象或传入 action creator
      3. 返回的对象中的字段会作为 UI 组件的 props
     const mapDispatchToProps = (dispatch: Dispatch<IAction>, ownProps?) => {
       return {
         addCount: (count: number) => dispatch(addCount(count)),
         addCountAsync: (count: number) => dispatch(addCountAsync(count)),
         subCount: (count: number) => dispatch(subCount(count)),
         mulCount: (count: number) => dispatch(mulCount(count)),
         divCount: (count: number) => dispatch(divCount(count))
       };
     };
    
    • 使用 bindActionCreators 定义 mapDispatchToProps 函数

      1. bindActionCreators 接收 function (an action creator) 或者 object (每个字段都是一个 action creator)和 dispatch 两个参数
      2. 返回值和上面函数形式返回的对象相同
      const mapDispatchToProps = (dispatch: Dispatch<IAction>) => {
        return bindActionCreators({
          addCount,
          addCountAsync,
          subCount,
          mulCount,
          divCount
        }, dispatch)
      }
      
    • 对象形式

      1. 把mapDispatchToProps定义为一个充满 action creators 的对象而不是一个函数,connect 将在内部自动为你调用 bindActionCreators
      const mapDispatchToProps = { addCount, addCountAsync, subCount, mulCount, divCount };
      
  • connect

    1. connect 接收四种不同的、皆可选的参数。按照惯例,它们被称为:

      • mapStateToProps?: Function
      • mapDispatchToProps?: Function
      • mergeProps?:Function
      • options?:Object
    2. 返回值是一个 wrapper(封套)函数,它接收你的组件并返回一个 wrapper 组件,其中包含注入到组件的 props。

在类组件中的使用

// CounterUI 组件
class CounterUI extends React.Component<any, ICountProps> {
  render() {
    const { count, todos, addCount, addCountAsync, subCount, mulCount, divCount } = this.props
    return (<> ... </>)
  }
}

// Counter容器组件
const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterUI)

在 react hooks 中使用

const Counter = () => {
  // 返回一个 Redux store 引用,该 store 与传递给 <Provider> 组件的 store 相同。
  const store = useStore();
  // 调用useDispatch函数会返回一个dispatch函数
  const dispatch = useDispatch();
  // useSelector声明了一个泛型,第一个是state的类型,第二表示返回值的类型
  const count = useSelector<IState, IState['count']>((state) => state.count);
  
  // 仅仅是示例!不要在实际的应用中这么做。
  // 当 store state 变更时,组件不会自动更新
  return <div>{store.getState()}</div>
}

react + redux toolkit

Redux Toolkit 简化了编写 Redux 逻辑和设置 store 的过程,目前 Redux 的最佳实践方式。

Redux-Toolkit 核心API

  • configureStore

    1. 封装 createStore 生成 store 实例
    2. 封装 combineReducers 合并 reducers
    3. 可以添加任何 Redux 中间件,默认情况下包含 redux-thunk
    4. 返回值:getState,dispatch,subscribe三个核心方法
  • createAction

    1. 生成 action 创建函数
    2. 接收字符串参数作为 action type
    3. 返回 action 对象,默认包含 type 和 payload 属性
    import { createAction } from "@reduxjs/toolkit"
    
    const modifyName = createAction('MODIFY_NAME');
    console.log(modifyName());
    

    ![]( "com.atlassian.confluence.content.render.xhtml.XhtmlException: Missing required attribute: {atlassian.com/resource/id…")

    上面的方式创建 action 函数并不能传递额外参数。可以传递额外参数的写法

    import { createAction } from "@reduxjs/toolkit"
    
    const modifyName = createAction('MODIFY_NAME', (data) => {
        return {
            payload: data,
            meta: '', // optional
            error: '' // optional
        }
    });
    console.log(modifyName('王五'));
    
  • createReducer

    1. 接收两个参数:第一个参数初始值 initState,第二个参数回调函数或对象(RTK 2.0将移除对象形式,已过时)

    2. 使用Immer库,可以直接修改对象的属性值,而无需再返回一个新的对象。

    3. immer是一个简化不可变数据更新的JavaScript库。它允许您以可变的方式编写代码,同时在背后自动处理不可变性。通过使用immer,您可以编写更简洁和易于理解的代码,同时避免手动处理不可变数据的复杂性。

      const initState = { name:"李四",age:18 }
      
      // 使用 immer 之前
      return { ...state, name:action.payload }
      // 使用 immer
      state.name = action.payload
      
    4. 第二个参数的传递

    // 回调函数中接收一个构建器builder,builder构建器中有三个属性addCase,addMatcher,addDefaultCase
    // addCase 创建 case ,类似于自定义 reducer 函数中的 switch case
    // addMatcher 根据自定义匹配函数来处理逻辑,接收两个回调函数作为参数:第一个回调函数为匹配函数;第二个回调函数为匹配成功的处理函数
    // addDefaultCase 没有匹配到任何已定义的 action 类型时进行的逻辑处理
    import { createAction, createReducer } from "@reduxjs/toolkit"
    
    export const modifyName = createAction("MODIFY_NAME", (data) => {
        return {
            payload: data
        }
    });
    export const modifyAge = createAction("MODIFY_AGE", (data) => ({ payload: data }));
    
    
    const initialState = {
        name: "张三",
        age: 18
    };
    
    export const userReducer = createReducer(initialState, (builder) => {
        builder.addCase(modifyName, (state, action: IAction) => {
            state.name = action.payload;
            console.log('addCase--modifyName');
        });
        builder.addCase(modifyAge, (state, action: IAction) => {
            state.age = action.payload;
        });
        builder.addMatcher((action: IAction) => {
            return action.payload === '李四';
        }, (state, action: IAction) => {
            state.name = action.payload;
            console.log('addMatcher--modifyName');
        })
        builder.addDefaultCase((state, action: IAction) => {
            // 什么也不做
            // console.log(state, action);
        });
    });
    
  • createSlice

    1. 接收一个包含三个主要选项字段的对象
    2. name:一个字符串,将用作生成的 action types 的前缀
    3. initialState:reducer 的初始 state
    4. reducers:一个对象,其中键是字符串,值是处理特定 actions 的 “case reducer” 函数
    5. 返回值:![]( "com.atlassian.confluence.content.render.xhtml.XhtmlException: Missing required attribute: {atlassian.com/resource/id…")
    import { createSlice } from "@reduxjs/toolkit"
    
    const initState: IToDo[] = [
        {
            id: '1',
            todo: "睡觉",
            isDone: false
        },
        {
            id: '2',
            todo: "吃饭",
            isDone: false
        },
        {
            id: '3',
            todo: "打豆豆",
            isDone: false
        },
    ];
    
    export const todosSlice = createSlice({
        name: 'todos',
        initialState: initState,
        reducers: {
            // 添加todo
            addTodo(state: IToDo[], action) {
                // console.log(state, action);
                /**
                 * action
                 * 
                 * payload: {id: '3f11e030-c15b-4910-8233-a91be220f55b', todo: 'test', isDone: false}
                 * type: "todos/addTodo"
                 */
                state.push(action.payload);
            },
            // 删除todo
            deleteTodo(state: IToDo[], action) {
                state.forEach((item, index) => {
                    if (item.id === action.payload) {
                        state.splice(index, 1);
                    }
                });
            },
        }
    })
    
    export const { addTodo, deleteTodo } = todosSlice.actions;
    
    console.log(todosSlice);
    
    • createSelector

      1. Reselect 提供了一个名为 createSelector 的函数来生成记忆化 Selector 。createSelector 接收一个或多个 input selector 函数,外加一个 output selector 作为参数,并返回一个新的 Selector 函数作为结果。

      2. 所有 input selector 的结果作为单独的参数提供给 output selector。

      3. 当你调用 Selector 时,Reselect 将使用你提供的所有参数运行你的 input selector,并查看返回的值。如果任何结果与之前的 === 不同,它将重新运行 output selector,并将这些结果作为参数传递。如果所有结果都与上次相同,它将跳过重新运行 output selector,并返回之前缓存的最终结果。

      4. 计算属性

        const count = (state: IInitialState, payload?: any) => {
            if (payload) {
                return state.value - payload
            } else {
                return state.value
            }
        }
        const other = (state: IInitialState, payload?: any) => {
            return state.other
        }
        
        // 计算属性
        export const selectCount = createSelector(
            [count, other],
            (value, other) => {
                return value + other
            }
        );
        
        /**
         *  Reselect 内部会做的事情:
         *  const firstArg = count(state, 2); 
         *  const secondArg = other(state); 
         * 
         *  const result = outputSelector(firstArg, secondArg); 
         *  return result
         */
        
      5. 在组件中使用

        import { selectCount } from './store/features/countSlice'
        // 可以将多个参数传递给 Selector,Reselect 将使用这些参数调用所有 input selector
        const count2 = useSelector((state: IState) => selectCount(state.count, 2));
        const count3 = useSelector((state: IState) => selectCount(state.count));