React全局状态管理实践:应用React Redux,Redux Toolkit,React Context 和 Global State
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-thunk,immer,reselect等包,目的为了解决:
- "配置一个 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,但是store
和dispatch
都放在了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无异,只是store
和dispatch
需要调用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👈
转载自:https://juejin.cn/post/7051599623835615269