React Mug:持续快地灵活管理 React 状态

285 阅读7分钟

如果觉得状态烫手,不要担心,把它装进马克杯里,用带把的杯子就能轻松地拿起来烫手的咖啡。

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

大家好,我是 OQ(Open Quoll),一个专注于状态管理的前端人。

今天想聊的是一款全新的状态库 React Mug,它的核心理念是 持续快灵活 管理状态

持续快是指:

  1. 用法简洁
  2. 纯函数主导
  3. 强支持类型

灵活则是:

  1. 独立于 Hook
  2. 组合机制

Talk is cheap. Show me the code..jpg

不过展开之前,我们先实现一个 “计数器” 看一下它的基本用法:

import { create, initial } from 'react-mug';

const countMug = create(0).attach(({ r, w, mug }) => ({
  get: r(),
  increment: w((count) => count + 1),
  reset: w(() => initial(mug)),
}));
import { useIt } from 'react-mug';

function Display() {
  const count = useIt(countMug.get);
  return <div>Count: {count}</div>;
}

function Control() {
  return (
    <>
      <button onClick={countMug.increment}>Increment</button>
      <button onClick={countMug.reset}>Reset</button>
    </>
  );
}

实现效果:

Screen Recording 2024-11-08 at 11.49.10.gif

持续快

代码写得快才可能少加班。但是快又不能只在一时。业务时常发展,代码时常变更,快还要在持续。

这便是 React Mug 放在第一位的理念:持续快。为此,它同时做到了:

  1. 用法简洁
  2. 纯函数主导
  3. 强支持类型

用法简洁

库的用法简洁能够减少写代码的量。实现一个功能,写的代码量少了,自然就写得快了。

而用法简洁的基础是概念简洁。

因此 React Mug 从数据流上就保证了简洁:

React Mug Data Flow.jpg

随后,用法的简洁便水到渠成了。

通过 API create,可以快速地创建带有 Action 的状态容器:

import { create, initial } from 'react-mug';

const countMug = create(0).attach(({ r, w, mug }) => ({
  get: r((count) => count),
  increment: w((count) => count + 1),
  reset: w(() => initial(mug)),
}));

其中,状态容器也称为 Mug,r 创建读 Action,w 创建写 Action,initial 返回初始状态。

r 创建的是读取全量状态的读 Action 时,还可以进一步简化:

const countMug = create(0).attach(({ r, w, mug }) => ({
-  get: r((count) => count),
+  get: r(),
  ...
}));

然后 Action 使用起来也非常方便,并且不需要初始化设置 Context:

import { useIt } from 'react-mug';

function Display() {
  const count = useIt(countMug.get);
  return <div>Count: {count}</div>;
}

function Control() {
  return (
    <>
      <button onClick={countMug.increment}>Increment</button>
      <button onClick={countMug.reset}>Reset</button>
    </>
  );
}

此外,换用 API creator,也可以创建 Mug 的创建器:

const createCountMug = creator((count: number) => count).attach(({ r, w, mug }) => ({
  get: r(),
  increment: w((count) => count + 1),
  reset: w(() => initial(mug)),
}));

// 稍后:
const countMug = createCountMug(0);

纯函数主导

写好的代码后续还会变更,越接近业务核心,变更越频繁。

而想要变更就要先读代码,读懂了才能凭借用法的简洁继续写得快。

所以,让快持续的关键在于让代码读得懂。

而读懂代码,最难的是弄清楚变量的变化。

好在纯函数可以通过没有副作用的特点严格限制外部变量的访问,大幅降低阅读难度。

因此,React Mug 提供了以纯函数主导创建 Action 的方式:

  • 通过 API r,可以传入 首个参数为状态、返回值任意的 读纯函数 创建 读 Action;
  • 通过 API w,可以传入 首参为状态、返回值也为状态的 写纯函数 创建 写 Action。

而额外的参数也可以声明在纯函数中,并反映在 Action 的调用上:

const countMug = create(0).attach(({ r, w, mug }) => ({
-  get: r(),
+  getMagnified: r((count, factor: number) => count * factor),
-  increment: w((count) => count + 1),
+  incrementBy: w((count, step: number) => count + step),
  ...
}));
function Display() {
-  const count = useIt(countMug.get);
-  return <div>Count: {count}</div>;
+  const magnifiedCount = useIt(countMug.getMagnified, 5);
+  return <div>Magnified Count: {magnifiedCount}</div>;
}

function Control() {
  return (
    <>
-      <button onClick={countMug.increment}>Increment</button>
+      <button onClick={() => countMug.incrementBy(3)}>Increment by 3</button>
      ...
    </>
  );
}

并且,在单元测试时,可以通过 pure 字段访问 Action 内部的纯函数轻松验证逻辑:

describe('getMagnified', () => {
  test('gets the factor-magnified count', () => {
    expect(countMug.getMagnified.pure(2, 2)).toBe(4);
  });
});

describe('incrementBy', () => {
  test('increments the count by the step', () => {
    expect(countMug.incrementBy.pure(1, 1)).toBe(2);
  });
});

强支持类型

支持类型可以进一步地提升可写性和可读性,所以 React Mug 选择了强支持。

所谓强支持,是指将类型作为库整体的一部分进行设计,而不是只当作补充。

这能够以最自然的方式引入类型带来的增益。

同时,从根源上保证避免了费额外力气处理类型的情况。

比如,本文全文都在自然而然地使用 TypeScript,而零负担。

灵活

在 “快” 之外,还有 “能不能” 的问题。业务需求多种多样,这是 “快” 无力解决的。

想要自如应对,就要用到 React Mug 的第二个理念:灵活

这包括了:

  1. 独立于 Hook
  2. 组合机制

独立于 Hook

Hook 是 React 组件挂接各类功能的入口,但是它只能在组件的顶层调用,略微笨拙。

这让有些功能实现起来比较别扭。

因此,React Mug 保持了独立于 Hook 的设计。

任何 Action,不论读写,都可以直接调用:

const magnifiedCount = countMug.getMagnified(5);
countMug.incrementBy(3);

而需要在 React 组件中渲染状态时,结合 useIt 调用读 Action 即可:

function Display() {
  const magnifiedCount = useIt(countMug.getMagnified, 5);
  return <div>Magnified Count: {magnifiedCount}</div>;
}

组合机制

功能之间的组合是很常见的,对应到状态上也是同理。

为此,React Mug 提供了丰富的组合机制,其中包括:

  • Mug 与 Mug 组合
  • Mug 与 Action 组合
  • Action 与 Action 组合

Mug 与 Mug 组合

首先,Mug 可以引用其他 Mug 进行组合。

比如,弹窗 Mug 可以这样引用 countMug

const countDraftDialogMug = create({
  shown: false,
  draft: 0,
  count: countMug,
});

随着引用建立:

  • 出入引用者 Action 内部的状态会变为对应结构的状态组合,
  • 写 Action 内部还可以通过返回值把状态对应写入被引用者。

比如,弹窗 Mug 可以这样实现起草和应用 countMug 的替换值:

const countDraftDialogMug = create({
  shown: false,
  draft: 0,
  count: countMug,
}).attach(({ r, w }) => ({
  open: w((state) => ({
    ...state,
    shown: true,
    draft: state.count, // 把 `countMug` 的值写入 `draft` 字段
  })),
  ok: w((state) => ({
    ...state,
    shown: false,
    count: state.draft, // 把 `draft` 字段的值写入 `countMug`
  })),

  get: r(),
  setDraft: w((state, draft: number) => ({ ...state, draft })),
}));

预期效果:

Screen Recording 2024-11-15 at 16.31.02.gif

Mug 与 Action 组合

其次,Mug 可以在位置、效果上与 Action 关联进行组合。

在位置上,Action 可以 附属 或 游离 于 Mug。

在效果上,Action 可以 绑定 或 不拘 于 Mug。

之前,通过 create 回调中 rw 创建的,便是在位置上附属、效果上绑定于 Mug 的 附属(Attached)特殊(Special)Action。

此外,通过 API upon,可以创建在位置上游离、效果上绑定于 Mug 的 游离(Detached)特殊 Action。

比如,交换 countMug 与另一个计数 Mug 的值的这种写 Action 可以实现为:

import { create, upon } from 'react-mug';

const anotherCountMug = create(0);

const swapCounts = upon([countMug, anotherCountMug]).w(([count, anotherCount]) => {
  return [anotherCount, count];
});

// 调用:
swapCounts();

另外,通过从库中直接引用的 rw,可以创建在位置上游离、效果上不拘于 Mug 的 游离 通用(General)Action。

比如,获取放大后的值的这种读 Action 可以换种方式实现为:

import { r } from 'react-mug';

const getMagnifiedIt = r((count: number, factor: number) => count * factor);

// 调用:
const magnifiedCount = getMagnifiedIt(countMug, 3);
const anotherMagnifiedCount = getMagnifiedIt(anotherCountMug, 5);

进一步地,游离通用 Action 可以通过 API upon 转化为游离特殊 Action:

const getMagnifiedCount = upon(countMug).r(getMagnifiedIt);

// 调用:
const magnifiedCount = getMagnifiedCount(5);

或者通过 create 回调中的 rw 转化为附属特殊 Action:

const countMug = create(0).attach(({ r, w, mug }) => ({
-  getMagnified: r((count, factor: number) => count * factor),
+  getMagnified: r(getMagnifiedIt),
  ...
}));

// 调用:
const magnifiedCount = countMug.getMagnified(5);

Action 与 Action 组合

最后,Action 可以调用其他 Action 进行组合。

create 回调中,如果需要调用直接作为返回值字段创建的 Action,可以把这个 Action 从返回值中挪上来,然后通过它的 pure 字段在有需要的 Action 内部纯函数中调用。

比如,在 countMug 中,可以这样把 incrementBy 挪上来,然后在新 Action shift 中调用:

const countMug = create(0).attach(({ r, w, mug }) => {
  const incrementBy = w((count, step: number) => count + step);

  return {
    ...
    incrementBy,
    ...
    shift: w((count, direction: boolean = true) => incrementBy.pure(count, direction ? 1 : -1)),
  };
});

如果需要调用的是被引用 Mug 的 Action,可以沿着 create 回调中的 mug 字段和对应的引用字段找到这个 Action,然后用同样的方式调用。

比如,在 countDraftDialogMug 中,可以这样找到 incrementBy 然后调用,从而把逻辑变更为起草和应用 countMug 的增加值:

const countDraftDialogMug = create({
  shown: false,
  draft: 0,
  count: countMug,
-}).attach(({ r, w }) => ({
+}).attach(({ r, w, mug }) => ({
  open: w((state) => ({
    ...state,
    shown: true,
-    draft: state.count,
+    draft: 0,
  })),
  ok: w((state) => ({
    ...state,
    shown: false,
-    count: state.draft,
+    count: mug.count.incrementBy.pure(state.count, state.draft),
  })),
  ...
}));

预期效果:

Screen Recording 2024-11-15 at 16.27.39.gif

如果需要调用的是游离通用 Action,直接调用就可以了。因为,通用 Action 在传入的首个参数为状态时,会自动消去副作用变得与纯函数等价。

比如,在 countMug 中,可以这样换个方式复用 getMagnifiedIt

const countMug = create(0).attach(({ r, w, mug }) => {
  ...
  return {
-    getMagnified: r(getMagnifiedIt),
+    getMagnified: r((count, factor: number) => getMagnifiedIt(count, factor)),
    ...
  };
});

总结

于是,有了 持续快,有了 灵活,便可以 持续快地灵活管理状态 了。

以上便是这款状态库 React Mug。

Repo 放在 GitHub 上,希望大家喜欢,欢迎点 ⭐️ 收藏,或者留言交流。

状态如咖啡,很烫,但是把它装进马克杯中,愿君得到的只是香醇。