likes
comments
collection
share

React全局状态管理实践:应用React Redux,Redux Toolkit,React Context 和 Global State

作者站长头像
站长
· 阅读数 12

React框架遵循原则:状态驱动UI变化,UI = f(states)。React 的开发其实就是复杂应用程序状态的管理和开发。React Hooks的到来使得逻辑复用更加便捷,也为React全局状态管理带来了新的思路。

这篇实践以经典的计数器程序为例,应用了React Redux,Redux Toolkit,React Context 和 Global State四种全局状态管理工具/方法,记录一下学习心得。

React Redux

React Redux 是非常经典的全局状态管理包,引入了Hooks之后也让令人诟病的大段模板代码有所简化,但是个人感觉仍然较为繁琐,但是在大型项目中、有各种中间件加持的情况下,开发应该还是比较愉快的🐟(但是搭建项目的时候应该是很不愉快🙃)。

下面是具体实践:

先将type文件放在这里,后面的例子如果有类型定义可以参看

export enum ActionType {
  'increment',
  'decrement',
  'toggle',
}

export type CounterState = { counter: number; showCounter: boolean };

export type CounterAction = { type: ActionType; payload?: any };

在store文件里进行定义:initialState,reducer,配置store

//========store.ts

// 从redux中引入createStore
import { createStore } from 'redux';
// 引入action的枚举类型和counter的state和action的定义类型
import { ActionType, CounterAction, CounterState } from '../../types';

// 定义初始化state
const initialState: CounterState = { counter: 0, showCounter: true };

// 定义reducer用于处理Action返回state
const counterReducer = (state = initialState, action: CounterAction) => {
  switch (action.type) {
    case ActionType.increment:
      // 原生的react-redux没有引入类似于immer或immutable库,所以需要在新的state里加入解构的旧的state
      return { ...state, counter: state.counter + 1 };

    case ActionType.decrement:
      return { ...state, counter: state.counter - 1 };

    case ActionType.toggle:
      return { ...state, showCounter: !state.showCounter };

    default:
      return state;
  }
};

// 使用createStore创建一个store
// createStore接受三个参数:reducer函数,preloadedState用于初始化state,enhancer用于向store添加第三方中间件
const store = createStore(counterReducer);

export default store;

在组件里我们可以:

  • dispatch actions改变某一个全局的state
  • 调用一个state

在这里调用state的时候使用了useSelector这个hook,这里简单介绍一下:

useSelector接收一个selector函数,用于从state对象里导出想要使用的state;可选传入第二个参数比较函数equalityFn,用于自定义比较state。

当一个action被dispatch函数发送出去后,useSelector会比较selector前后两次state的差异,如果不同的话就会强制re-render。

默认采用 strict === reference equality 做比较。如果只需要做浅比较可以从react-redux引入shallowEqual,然后传入第二个参数。

//===========Counter.tsx

import React, { FC } from 'react';
// 从react-redux引入两个hooks
import { useSelector, useDispatch } from 'react-redux';

import { ActionType, CounterState } from '../types';

const Counter: FC = () => {
  // useDispatch用于导出dispatch方法
  const dispatch = useDispatch();

  // useSelector用于从state对象里导出想要使用的state
  const counter = useSelector<CounterState, number>((state) => state.counter);
  const isCounterShown = useSelector<CounterState, boolean>((state) => state.showCounter);

  const incrementHandler = () => {
    // 一般dispatch出去的action包含两个部分:
    // type:为reducer里定义的可以接受的action type;可以将action type单独提取为enum类
		// payload: 可以理解为这个action所携带的data部分(这里没有data所以传null,也可以不传)
    dispatch({ type: ActionType.increment, payload: null });
  };

  const decrementHandler = () => {
    dispatch({ type: ActionType.decrement });
  };

  const toggleCounterHandler = () => {
    dispatch({ type: ActionType.toggle });
  };

  return (
    <div style={{ display: 'flex', flexFlow: 'column', alignItems: 'center' }}>
      <h1>React Redux Counter</h1>
      {isCounterShown && <p style={{ fontSize: '32px' }}>{counter}</p>}
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={decrementHandler}>Decrement</button>
        <button onClick={toggleCounterHandler}>Toggle Counter</button>
      </div>
    </div>
  );
};

export default Counter;

最后在根组件App里需要用Provider 组件包裹调用store的组件,并指定使用的是哪个store

// 使用React-Redux和Redux-Toolkit都需要用到Provider组件
import { Provider } from 'react-redux';

const App: FC = () => {
	<Provider store={counterReduxStore}>
		<Counter />
	</Provider>
};

export default App;

Redux Toolkit

React Toolkit和React Redux同根同源,可以看做是React Redux的一个打包升级版。它包含了redux-thunkimmerreselect等包,目的为了解决:

  • "配置一个 Redux store 过于复杂"
  • "做任何 Redux 的事情我都需要添加很多包"
  • "Redux 需要太多的样板代码"

使用后发现React Toolkit功能更强大,简单使用时需要配置的东西更少,但是各种API变得更多更复杂了😧想要深入玩转它可能得花点时间

下面是具体实践:

store与React Redux相比有几点不同:

  • 引入了Slice的概念,将一个state相关的初始值、reducer等放在一起定义
  • 使用了immer,操作state更直观
  • 调用configureStore创建一个store,这个函数可配置参数更多

💡 createSlice还可以定义extraReducer,配合createAsyncThunk异步actions,具体可以参看这里

// 从toolkit里引入createSlice, configureStore两个关键函数
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';

import { CounterState } from '../../types';

const initialState: CounterState = { counter: 0, showCounter: true };

// createSlice为某一个类型的action创建了一个切片,将这个action相关的信息都归集到了一起,更便于维护
const counterSlice = createSlice({
  // slice的名称
  name: 'counter',
  // state的初始值
  initialState,
  // 一个包含了action的对象,每一个key都会生成一个actions(相当于原生redux的Switch Case写法)
  reducers: {
    // toolkit内部调用了immer包,我们可以直接对state对象做修改,不用解构旧的state
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increaseByAmount(state, action: PayloadAction<number>) {
      state.counter += action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

// 调用切片对象的actions属性可以获得所有在reducers里定义的actions
export const counterActions = counterSlice.actions;

// 调用configureStore创建一个store,这个函数是对Redux的createStore函数的一个封装,它接受一个对象,对象里包括:
// reducer:接收单个reducer函数或者包含若干reducer函数的对象
// middleware?:接收Redux中间件函数数组,默认使用了react-thunk
// devTools?:决定是否开启对Redux DevTools浏览器插件的支持
// preloadedState?:传给Redux的createStore函数中同名
// enhancers?:接收Redux store enhancer数组
const store = configureStore({ reducer: counterSlice.reducer });
export default store;

组件内调用部分与React Redux相差无几,唯一不同的是我们可以直接调用由slice.actions导出的action函数,不用再去dispatch了

const decrementHandler = () => {
    // 可以直接调用从store导出的actions
    dispatch(counterActions.decrement());
  };

在根组件里的使用方法与React Redux一模一样,这里可以参看前面

useContext + useReducer

在React Hooks时代,运用React的原生Hooks useContext + useReducer 配合Context完全可以手动搭建起一套简单的状态管理机制。这套机制在小型项目上很快速,很好用。

下面是具体实践:

store文件依旧需要定义初始值和reducer,但是storedispatch都放在了Context中,以便于全局共享。

import React, { createContext, Dispatch, FC, useReducer } from 'react';
// 引入immutable对reducer进行改造
import { setIn } from 'immutable';
import { CounterState, CounterAction, ActionType } from '../../types';

type Context = {
  store: CounterState;
  dispatch: Dispatch<CounterAction>;
};

// 调用createContext创建一个context,用泛型定义context里包含的内容:一个store和一个dispatch
export const CounterContext = createContext<Context>({} as Context);

// 这里可以使用与react-redux里完全一样的reducer
// 模仿redux-toolkit的设计,引入immutable对actions返回的state处理一下
const counterReducer = (state: CounterState, action: CounterAction) => {
  switch (action.type) {
    case ActionType.increment:
      return setIn(state, ['counter'], state.counter + 1);

    case ActionType.decrement:
      return setIn(state, ['counter'], state.counter - 1);

    case ActionType.toggle:
      return setIn(state, ['showCounter'], !state.showCounter);

    default:
      return state;
  }
};

// 定义初始化state
const initialState: CounterState = { counter: 0, showCounter: true };

// 引入useReducer将context封装一下
const Provider: FC = (props) => {
  // 调用useReducer hook导出store和dispatch
  const [store, dispatch] = useReducer(counterReducer, initialState);
  // 将store和dispatch传入context.provider,这样被该provider包裹的组件都可以用到store和dispatch
  return <CounterContext.Provider value={{ store, dispatch }}>{props.children}</CounterContext.Provider>;
};

export default Provider;

组件内调用与React Redux无异,只是storedispatch需要调用useContext

const { store, dispatch } = useContext(CounterContext);
const { counter, showCounter } = store;

这里对Context.Provider 进行了封装

import CounterContext from './CounterContext';
// context的provider其实已经封装在了store里
import CounterContextStore from './CounterContext/store';

//...
<CounterContextStore>
  <CounterContext />
</CounterContextStore>
//...

Global State

这套方法是对立超代码最佳实践使用Global State Hook取代Redux一个实践。Global State其实是利用了自定义Hooks的优势,定义了一个可以读写state的hook。

  • 将一个state的value和setValue分别以同一个key存在两个类Map的对象里,再将value和setValue返回出去以便于组件的调用
  • 在组件销毁时,相应的setValue也会被销毁
import { useState, useEffect } from 'react';
import { CounterState } from '../../types';

const GLOBAL_STATES: Record<string, any> = {};
const GLOBAL_STATES_DISPATCHERS: Record<string, any[]> = {};

// GlobalState实际上是一个自定义的hook
// 将一个state的value和setValue分别以同一个key存在两个类Map的对象里,再将value和setValue返回出去以便于组件的调用
// 在组件销毁时,相应的setValue也会被销毁
const useGlobalState = <T>(key: string, initState: T): [T, (value: T) => void] => {
  const [state, setState] = useState(initState);

  useEffect(() => {
    // 判断是否存在这个state
    if (!GLOBAL_STATES[key]) {
      // 如果不存在则初始化这个state和对应的dispatchers数组
      GLOBAL_STATES[key] = initState;
      GLOBAL_STATES_DISPATCHERS[key] = [];
    } else {
      // 如果存在就存入hook的state,最后return出去
      setState(GLOBAL_STATES[key]);
    }
    GLOBAL_STATES_DISPATCHERS[key].push(setState);
    return () => {
      // 组件销毁时从dispatchers数组删除这个组件创建的setState函数
      GLOBAL_STATES_DISPATCHERS[key] = GLOBAL_STATES_DISPATCHERS[key].filter((item) => item !== setState);
    };
  }, []);

  const setStates = (newState: T) => {
    // 若一个global state要更新,遍历所有dispatchers(setState函数),更新来自不同组件的state
    GLOBAL_STATES_DISPATCHERS[key].forEach((dispatch) => {
      dispatch(newState);
    });
    GLOBAL_STATES[key] = newState;
  };

  return [state, setStates];
};

// 对于某一类state,需要先在一处定义一个key和初始值
export const useCounterStore = () => useGlobalState<CounterState>('counter', { counter: 0, showCounter: true });

在组件内调用方法和useState一样

//...
const [state, setState] = useCounterStore();

const incrementHandler = () => {
  setState({ ...state, counter: state.counter + 1 });
};
//...

总结

  • React Redux使用较为繁琐,适用于大型项目的全局状态管理
  • Redux Toolkit是原生React Redux的一个增强版,降低了开包即用的上手难度,但是新增更多API,需要额外的学习成本
  • 使用Hooks管理全局状态是目前比较快速简便的一种方法
  • Demo链接👉ClickHere👈