代码量立减 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 支持一下。

相关推荐
大模型铲屎官16 分钟前
HTML5 技术深度解读:本地存储与地理定位的最佳实践
前端·html·html5·本地存储·localstorage·地理定位·geolocation api
一 乐1 小时前
基于vue船运物流管理系统设计与实现(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·后端·船运系统
m0_528723811 小时前
在React中使用redux
前端·javascript·react.js
傻小胖1 小时前
vue3中customRef的用法以及使用场景
前端·javascript·vue.js
谦谦橘子1 小时前
手把手教你实现一个富文本
前端·javascript
Future_yzx1 小时前
Java Web的发展史与SpringMVC入门学习(SpringMVC框架入门案例)
java·前端·学习
star010-2 小时前
【视频+图文详解】HTML基础4-html标签的基本使用
前端·windows·经验分享·网络安全·html·html5
engchina2 小时前
CSS Display属性完全指南
前端·css
engchina2 小时前
详解CSS `clear` 属性及其各个选项
前端·css·css3
yashunan3 小时前
Web_php_unserialize
android·前端·php