为了少加班,我设计了一款 React 状态库让代码写得快又读得懂

1,263 阅读5分钟

如果大环境难以左右,通过优化工具,我们至少可以减少技术因素导致的加班。

大家好,我是 OQ(Open Quoll),一个前端人,今天想从加班的角度聊一聊 React 状态库,看一看现有状态库在加班上的利弊,然后试着设计一款帮助少加班的状态库。

欢迎朋友们多多交流,不足之处还请多指教。

(友情提示:本文中部分 API 仅适用于 react-mug@0.7.x 版本。)

加班的直接原因,写不快的代码

聊起 React 状态库,自然先要聊一下老牌明星库 Redux,它凭借纯函数带来的 “高可预测性” 曾让我趋之若鹜。

所谓 高可预测性 实际上就是 高可读性 和 高可测试性,一方面阅读代码时可以直观地理解逻辑,另一方面单元测试时可以便捷地验证逻辑。

不过 Redux 的用法太过繁琐,这是它的状态变化过程,从 “写参数” 到 “读参数” 的每一步都需要占用精力编码:

Redux Data Flow.jpg

尽管后来 Redux Tookit 的出现可以省去一些模板代码,但是概念上的高复杂度却没有变过,依然算不上简洁。

而用法繁琐,代码就写不快,所以开发项目就只能晚上线、砍功能 或者 加班。

而业务往往只关心进度,对比其他不用 Redux 的项目进度飞起,晚上线 或 砍功能 的请求只会显得苍白无力:

Without Redux vs With Redux in Iteration.jpg

所以加班成为了用 Redux 做开发时不可回避的选择。

加班的间接原因,读不懂的代码

相对地,像 MobX 这种状态库用法简洁,状态变化过程没有多余的步骤,可以避免代码写不快的问题:

MobX Data Flow.jpg

不过中途加入用了 MobX 的老项目会遇到另一个问题,代码读不懂。

原因是 MobX 中执行状态变化的逻辑主体是封装了状态更新语句的 Action 方法,它们除了读取本地参数和生成返回值之外,还会读取和写入状态对象中的属性、以及其他可能用到的变量,导致理解状态变化时需要推演和记忆存在于 Action 方法之外的大量上下文信息的变化。

而老项目中一个完整的状态变化常常由多个 Action 方法组合完成,导致理解所需的上下文信息激增,非常容易超出项目新人的短期认知极限。

而一旦上下文信息多到超出认知极限,代码就读不懂了:

Context Info in MobX.jpg

读不懂代码就会写不快代码,写不快代码就又回到了加班上去。

而且项目体量越大,这个问题越严重。

所以加班也成了用 MobX 做开发时常常出现的选择。

写得快又读得懂的代码

于是 Redux 和 MobX 在技术层面上都助长了加班,而这背后的原因是:写不快代码 和 读不懂代码。

其中,写不快代码是由于用法繁琐,所以可以通过简洁的用法解决。

然后,读不懂代码是由于上下文信息过多。

而纯函数是目前已知上下文信息最少的逻辑主体,没有副作用,只会读取本地参数和生成返回值,所以可以通过以纯函数主导状态逻辑的方式解决。

因此,对于现有状态库导致的加班问题,可以通过设计一款 用法简洁又以纯函数主导 的状态库来解决。

设计一款用法简洁又以纯函数主导的状态库

设计这样的一款状态库,首先要在概念上有所保障,即这款状态库的状态变化过程本身就要做到简洁并且适合以纯函数主导,所以我梳理了以下过程:

React Mug Data Flow.jpg

其中 “写 Action” 和 “读 Action” 以纯函数主导创建:

const { w, r } = upon(stateReference);

const writeAction = w((state, writeArg0, writeArg1, ...restWriteArgs) => {
  // ...
  return { ...state, /* ... */ };
});

const readAction = r((state, readArg0, readArg1, ...restReadArgs) => {
  return /* ... */;
});

并且调用它们只需要传入 “写参数” 和 “读参数”:

writeAction(writeArg0, writeArg1, ...restWriteArgs);

const readResult = readAction(readArg0, readArg1, ...restReadArgs);

于是,骨架就这样设计好了。

最后,补充上状态引用、类型、和 React 集成,这款旨在少加班的状态库就设计完成了。

我把它命名为 React Mug 放在了这个 Repo 中,可以通过包管理器安装:

npm i react-mug

下面是对 “计数器” 案例的快速实现:

import { construction, upon, useIt } from 'react-mug';

const countMug = { [construction]: 0 };

const [r, w] = upon(countMug);

const getCount = r();
const setCount = w();

const increment = w((n) => n + 1);
const decrement = w((n) => n - 1);

function Counter() {
  const count = useIt(getCount);
  return <div>{count}</div>;
}

function Buttons() {
  return (
    <>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={() => setCount(0)}>To 0</button>
    </>
  );
}

实现效果:

Screen Recording 2024-09-29 at 15.33.22.gif

其中,

  • countMug 是对计数器状态的引用,
  • getCount 是读 Action,
  • r() 等价于 r((state) => state)
  • setCountincrementdecrement 是写 Action,
  • w() 等价于 w((state, patch) => /* 以合并逻辑生成新状态 */)
  • useIt 是集成 “读结果” 到 React 的 Hook。

此外,还可以像这样创建 能够接收额外参数的 Action:

const getMagnifiedCount = r((n, factor: number) => n * factor);

const magnifiedCount = getMagnifiedCount(5);

复用其他 Action 内部的纯函数的 Action:

const step = w((n, direction: boolean) => (direction ? pure(increment)(n) : pure(decrement)(n)));

跨多个状态引用的 Action:

const someOtherCountMug = { [construction]: 999 };

const exchangeCounts = upon([countMug, someOtherCountMug]).w(([count, someOtherCount]) => {
  return [someOtherCount, count];
});

exchangeCounts();

以及独立于状态引用的通用 Action:

import { w } from 'react-mug';

const incrementIt = w((n: number) => n + 1);

incrementIt(countMug); // 等价于 increment();

incrementIt(someOtherCountMug);

这样,就以简洁的方式尽可能地把状态逻辑编织进了纯函数当中。

致谢

以上便是我从加班的角度对 React 状态库的所见所想,希望朋友们喜欢。如果觉得按这个思路管理状态不错的话,还请麻烦在 Repo 上点个 ⭐️ 支持一下,或者留言交流,我会尽快完善不足、补全文档,谢谢。