大家好,我是 OQ(Open Quoll)。
本文将以 "计数器" 为案例,先活用 useReducer 实现全局状态管理,随后在保持函数式的前提下,逐步分析、确定可简化的部分,有针对性地设计、应用新的 API 完成简化,最终达到省去六成以上代码量的效果。
之后,这些 API 会汇编成库,方便大家尝试。
希望喜欢,欢迎交流。
活用 useReducer 实现全局状态管理
"计数器" 的效果预期:
那么,按照 Reducer 理念,如下编码 计数器 的 状态类型、初始状态、Action Type、Action Creator、Reducer 和 Selector:
tsx
interface CounterState {
value: number;
}
enum CounterActionType {
Increment = 'Counter/Increment',
Reset = 'Counter/Reset',
}
type CounterAction =
| { type: CounterActionType.Increment; step: number }
| { type: CounterActionType.Reset };
function increment(step: number): CounterAction {
return { type: CounterActionType.Increment, step };
}
function reset(): CounterAction {
return { type: CounterActionType.Reset };
}
const initialCounterState: CounterState = {
value: 0,
};
function counterReducer(state: CounterState, action: AppAction): CounterState {
switch (action.type) {
case CounterActionType.Increment:
return { value: state.value + action.step };
case CounterActionType.Reset:
return initialCounterState;
default:
return state;
}
}
function selectCounterValue(appState: AppState) {
return appState.counter.value;
}
然后,编码 应用 的 状态类型、初始状态、Action Type、Reducer,将 计数器状态 集成进 应用状态:
tsx
interface AppState {
counter: CounterState;
}
type AppAction = CounterAction;
const initialAppState: AppState = {
counter: initialCounterState,
};
function appReducer(state: AppState, action: AppAction): AppState {
return {
counter: counterReducer(state.counter, action),
};
}
之后,将 应用状态 通过 useReducer 和 Context 建立为 全局状态:
tsx
import { createContext, Dispatch, PropsWithChildren, useContext, useMemo, useReducer } from 'react';
const AppReducerContext = createContext<[AppState, Dispatch<AppAction>] | null>(null);
function AppReducerProvider({ children }: PropsWithChildren) {
const value = useReducer(appReducer, initialAppState);
return <AppReducerContext.Provider value={value}>{children}</AppReducerContext.Provider>;
}
function useAppReducer() {
const value = useContext(AppReducerContext);
if (!value) {
throw new Error('App reducer not initialized');
}
return value;
}
function useAppDispatch() {
const [, dispatch] = useAppReducer();
return dispatch;
}
function useAppSelector<TSelector extends (appState: AppState) => any>(
select: TSelector
): ReturnType<TSelector> {
const [appState] = useAppReducer();
return useMemo(() => select(appState), [appState]);
}
最后,编码 React 组件进行渲染:
tsx
function Display() {
const counterValue = useAppSelector(selectCounterValue);
return <div>Counter value: {counterValue}</div>;
}
function Control() {
const dispatch = useAppDispatch();
return (
<>
<button onClick={() => dispatch(increment(1))}>Increment 1</button>
<button onClick={() => dispatch(increment(5))}>Increment 5</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</>
);
}
function App() {
return (
<AppReducerProvider>
<Display />
<Control />
</AppReducerProvider>
);
}
至此,代码 111 行,基于 useReducer 实现了完整的全局状态管理。
简化 1:建立全局状态的过程
下面开始分析和简化。
首先,不难发现的是,将应用状态建立为全局状态的过程,即使放在不同应用中也是不变的,所以可以引入 API 简化。
与 useReducer 相对应地,这个 API 应该可以极简地:
- 以初始状态创建全局状态容器。
- 从状态容器读取当前状态。
- 用 Reducer 为状态容器创建 dispatch 函数。
因此,可以进行如下设计:
- 直接通过引用初始状态的 普通对象 创建全局状态容器,而且不依赖 Provider。
- 基于状态容器创建既可以 直接调用 也可以 结合 hook 调用 的 get 函数。
- 基于状态容器和 Reducer 创建可以 直接调用 的 dispatch 函数。
于是,之前的实现就简化为了:
diff
-import { createContext, Dispatch, PropsWithChildren, useContext, useMemo, useReducer } +from 'react';
+import { useMemo } from 'react';
+import { construction, upon, useIt } from 'my-lib';
...
-const AppReducerContext = createContext<[AppState, Dispatch<AppAction>] | null>(null);
-
-function AppReducerProvider({ children }: PropsWithChildren) {
- const value = useReducer(appReducer, initialAppState);
- return <AppReducerContext.Provider value={value}>{children}</AppReducerContext.Provider>;
-}
-
-function useAppReducer() {
- const value = useContext(AppReducerContext);
-
- if (!value) {
- throw new Error('App reducer not initialized');
- }
-
- return value;
-}
-
-function useAppDispatch() {
- const [, dispatch] = useAppReducer();
- return dispatch;
-}
+const appMug = {
+ [construction]: initialAppState,
+};
+
+const [r, w] = upon(appMug);
+
+const getAppState = r();
+
+const dispatch = w(appReducer);
function useAppSelector<TSelector extends (appState: AppState) => any>(
select: TSelector
): ReturnType<TSelector> {
- const [appState] = useAppReducer();
+ const appState = useIt(getAppState);
return useMemo(() => select(appState), [appState]);
}
function Display() {
const counterValue = useAppSelector(selectCounterValue);
return <div>Counter value: {counterValue}</div>;
}
function Control() {
- const dispatch = useAppDispatch();
return (
<>
<button onClick={() => dispatch(increment(1))}>Increment 1</button>
<button onClick={() => dispatch(increment(5))}>Increment 5</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</>
);
}
function App() {
return (
- <AppReducerProvider>
+ <>
<Display />
<Control />
- </AppReducerProvider>
+ </>
);
}
至此,代码 99 行,减少了 12 行,减少比例为 10.8%。
简化 2:数据流
不过,单纯靠去重复,能简化的幅度终究是有限的。想要有突破,还得从理念本身入手。
而 Reducer 理念的核心是它的数据流:
仔细观察从 WriteParams 到 NextState 的写状态过程,可以发现每一种写操作都被拆分成了两段:
- 将 WriteParams 转化为 Payload 并附上 ActionType 组合成 Action 对象的过程。
- ActionType 对应 case 中根据 CurrentState 和 Payload 计算 NextState 的过程。
但是这两段过程本质上只做了一件事情:
- 根据 CurrentState 和 WriteParams 计算 NextState。
所以不如按照写操作的种类,将原来的逻辑实体横向切割和重新定义新的逻辑实体,使其:
- 从外部看,可以接收 WriteParams 进行调用,
- 从内部看,可以根据 CurrentState 和 WriteParams 计算 NextState,
从而将两个过程合并成一个。
按照这个思路就设计出来了写过程简化后的数据流:
进一步地,再看下从 NextState 到 ReadResult 的读状态过程,留意 Selector 的参数可以发现两个小问题:
- 传入的状态是应用状态,导致逻辑冗长而脆弱。
- 不接收额外的参数,导致逻辑不灵活。
因此数据流进一步改进为了:
于是,对应设计和应用 API 后,之前的实现就深度简化为了:
diff
-import { useMemo } from 'react';
import { construction, upon, useIt } from 'my-lib';
interface CounterState {
value: number;
}
const initialCounterState: CounterState = {
value: 0,
};
-enum CounterActionType {
- Increment = 'Counter/Increment',
- Reset = 'Counter/Reset',
-}
-
-type CounterAction =
- | { type: CounterActionType.Increment; step: number }
- | { type: CounterActionType.Reset };
-
-function increment(step: number): CounterAction {
- return { type: CounterActionType.Increment, step };
-}
-
-function reset(): CounterAction {
- return { type: CounterActionType.Reset };
-}
-
-function counterReducer(state: CounterState, action: AppAction): CounterState {
- switch (action.type) {
- case CounterActionType.Increment:
- return { value: state.value + action.step };
- case CounterActionType.Reset:
- return initialCounterState;
- default:
- return state;
- }
-}
-
-function selectCounterValue(appState: AppState) {
- return appState.counter.value;
-}
-
-interface AppState {
- counter: CounterState;
-}
-
-const initialAppState: AppState = {
- counter: initialCounterState,
-};
-
-type AppAction = CounterAction;
-
-function appReducer(state: AppState, action: AppAction): AppState {
- return {
- counter: counterReducer(state.counter, action),
- };
-}
-
-const appMug = {
- [construction]: initialAppState,
-};
-
-const [r, w] = upon(appMug);
-
-const getAppState = r();
-
-const dispatch = w(appReducer);
-
-function useAppSelector<TSelector extends (appState: AppState) => any>(
- select: TSelector
-): ReturnType<TSelector> {
- const appState = useIt(getAppState);
- return useMemo(() => select(appState), [appState]);
-}
+const counterMug = {
+ [construction]: initialCounterState,
+};
+
+const [r, w] = upon(counterMug);
+
+const getCounterValue = r((state) => state.value);
+
+const increment = w((state, step: number) => ({ value: state.value + step }));
+
+const reset = w(() => initialCounterState);
function Display() {
- const counterValue = useAppSelector(selectCounterValue);
+ const counterValue = useIt(getCounterValue);
return <div>Counter value: {counterValue}</div>;
}
function Control() {
return (
<>
- <button onClick={() => dispatch(increment(1))}>Increment 1</button>
- <button onClick={() => dispatch(increment(5))}>Increment 5</button>
- <button onClick={() => dispatch(reset())}>Reset</button>
+ <button onClick={() => increment(1)}>Increment 1</button>
+ <button onClick={() => increment(5)}>Increment 5</button>
+ <button onClick={reset}>Reset</button>
</>
);
}
function App() {
return (
<>
<Display />
<Control />
</>
);
}
接着,把初始状态也内联进状态容器里,就得到了最终代码:
tsx
import { construction, Mug, upon, useIt } from 'my-lib';
interface CounterState {
value: number;
}
const counterMug: Mug<CounterState> = {
[construction]: {
value: 0,
},
};
const [r, w] = upon(counterMug);
const getCounterValue = r((state) => state.value);
const increment = w((state, step: number) => ({ value: state.value + step }));
const reset = w(() => counterMug[construction]);
function Display() {
const counterValue = useIt(getCounterValue);
return <div>Counter value: {counterValue}</div>;
}
function Control() {
return (
<>
<button onClick={() => increment(1)}>Increment 1</button>
<button onClick={() => increment(5)}>Increment 5</button>
<button onClick={reset}>Reset</button>
</>
);
}
function App() {
return (
<>
<Display />
<Control />
</>
);
}
至此,代码 43 行,减少了 68 行,减少比例为 61.2%。
汇编成库
由于状态有时给人的感觉就像热咖啡一样烫手,所以在上面的代码中,盛装状态的容器取用了最常用于盛装咖啡的容器,Mug(马克杯),作为了命名后缀。
而状态容器也是状态管理的核心组件,因此上面的这些 API 就以 npm 包 react-mug
进行了发布,可以通过命令行安装:
sh
npm i react-mug
此外,为了方便单元测试,库中补充了 pure
函数用来访问 Action 内部的纯函数,以便通过测试纯函数的方式轻松测试 Action:
tsx
import { pure } from 'react-mug';
describe('increment', () => {
test('shifts value by step', () => {
expect(pure(increment)({ value: 1 }, 2)).toStrictEqual({ value: 3 });
});
});
以上就是以纯函数管理状态的极简之道了,感谢阅读,欢迎交流。
库的实现放在了 GitHub 上,如果喜欢,还望点个 Star 支持一下。