代码量立减 61.2%!从 useReducer 出发,探求以纯函数管理状态的极简之道

大家好,我是 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 应该可以极简地:

  1. 以初始状态创建全局状态容器。
  2. 从状态容器读取当前状态。
  3. 用 Reducer 为状态容器创建 dispatch 函数。

因此,可以进行如下设计:

  1. 直接通过引用初始状态的 普通对象 创建全局状态容器,而且不依赖 Provider。
  2. 基于状态容器创建既可以 直接调用 也可以 结合 hook 调用 的 get 函数。
  3. 基于状态容器和 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 的写状态过程,可以发现每一种写操作都被拆分成了两段:

  1. 将 WriteParams 转化为 Payload 并附上 ActionType 组合成 Action 对象的过程。
  2. ActionType 对应 case 中根据 CurrentState 和 Payload 计算 NextState 的过程。

但是这两段过程本质上只做了一件事情:

  • 根据 CurrentState 和 WriteParams 计算 NextState。

所以不如按照写操作的种类,将原来的逻辑实体横向切割和重新定义新的逻辑实体,使其:

  1. 从外部看,可以接收 WriteParams 进行调用,
  2. 从内部看,可以根据 CurrentState 和 WriteParams 计算 NextState,

从而将两个过程合并成一个。

按照这个思路就设计出来了写过程简化后的数据流:

进一步地,再看下从 NextState 到 ReadResult 的读状态过程,留意 Selector 的参数可以发现两个小问题:

  1. 传入的状态是应用状态,导致逻辑冗长而脆弱。
  2. 不接收额外的参数,导致逻辑不灵活。

因此数据流进一步改进为了:

于是,对应设计和应用 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 支持一下。

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax