一款既简洁又独立于 Hook 的函数式 React 状态库

大家好,我是 OQ(Open Quoll),一个前端人,从业几年用 React 写过大大小小的项目,终于无法继续妥协于现有 React 状态库的局限,决定整理思路,新写了一个状态库,旨在帮助同行们和自己在日常工作中早上线、少加班,特性如下,请掘友们过目:

  1. 简洁,还是简洁!
  2. 独立于 Hook。
  3. 函数式内核。
  4. 基于 Action 的单向数据流。
  5. 高可组合性。
  6. 高可测试性。
  7. TypeScript 优先。

库命为 React Mug,用喜欢的包管理器安装即可:

sh 复制代码
npm i react-mug

如莱老所说,接下来就快速实现一个 "计数器" 的案例先一起来看一下基本用法,然后展开聊一聊每个特性。

计数器案例实现

实现代码:

tsx 复制代码
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>
    </>
  );
}

实现效果:

特性 1:简洁,还是简洁!

简洁是 React Mug 最重要的特性。曾经被 某 dux 的繁琐深深困扰过的我深知:

简洁 等于 负担小 等于 效率高 等于 早上线 等于 少加班

React Mug 的简洁首先体现在 零设置,即不用设置任何 Context Provider 直接使用。

其次是 盛装状态的容器直接以 POJO (Plain Old JavaScript Object)定义,以 [construction] 字段声明状态初始值,透明、简单。

然后是 用于状态访问的读写 Action 可以一步到位 地创建。比如,案例中创建的 读 Action getCount 和 写 Action setCountincrementdecrement

最后是 在 React 组件中使用状态只需要使用 useIt一个 Hook

任何没有实际用处的概念、API 一律去掉。

特性 2:独立于 Hook

在简洁的基础上,React Mug 提供独立于 Hook 的状态访问,即 读写状态都直接调用 Action 进行

比如,案例中的 getCount 可以独立于 useIt 直接读取状态:

tsx 复制代码
const count = getCount();

比如,setCountincrementdecrement 都是直接调用。

独立于 Hook 的状态访问可以有效破除前端应用的整体结构被渲染框架机制牵着走的窘境 ,避免不同关注点的代码不得不混合在一起变得越来越难以维护,进而让渲染代码只关注渲染、让状态代码只关注状态成为可能,给复杂应用所需的关注点分离的结构创造土壤

特性 3:函数式内核

函数式编程有一个很大的好处,高可预测性,即给定相同的输入总是能得到相同的输出。React Mug 善用了这个优点,读写 Action 都以传入纯函数的方式创建

创建读 Action 时传入的是以当前状态为首个参数、以任意值为返回值的纯函数。调用读 Action 时,当前状态和其他调用参数会传入到这个纯函数中,然后透传返回返回值,从而在当前状态和其他调用参数相同时总能读取相同的值,实现高可预测性

案例中无参调用 r 创建的 getCount 是直接返回当前状态的读 Action,与下面的读 Action 等价:

tsx 复制代码
const [r, w] = upon(countMug);

const getCountToo = r((n) => n);

如果传入的纯函数有额外的参数,那么创建的读 Action 也可以接收对应的额外参数:

tsx 复制代码
const getMagnifiedCount = r((n, factor: number) => n * factor);

const magnifiedCount = getMagnifiedCount(5);

相对应地,创建写 Action 时传入的是以当前状态为首个参数、以新状态为返回值的纯函数。调用写 Action 时,当前状态和其他调用参数会传入到这个纯函数中,然后将返回值作为新状态保存起来,从而在当前状态和其他调用参数相同时总能写入相同的值,实现高可预测性

案例中无参调用 w 创建的 setCount 是以合并逻辑生成新状态的写 Action,内部是与 incrementdecrement 类似的纯函数。

特性 4:基于 Action 的单向数据流

在 React Mug 中,读写状态都只能调用 Action 进行,而产生的数据也只会单向流动 ,这保障了每一次状态访问不论是读还是写都是有意调用的并且一定会在有限步骤内完成,不会触发意料之外的状态变化,行为十分确定

特性 5:高可组合性

高可组合性是指状态与状态、状态与 Action、Action 与 Action 可以灵活组合实现各种的状态访问。

状态与状态组合

比如,某个弹窗的状态可以这样引用案例中的 countMug 作为子状态:

tsx 复制代码
const someDialogMug = {
  [construction]: {
    draft: 0,
    value: countMug,
  },
};

在读这个状态时 value 字段会替换为 countMug 中的状态,在写这个状态时 value 字段的值会写入 countMug

tsx 复制代码
const [r, w] = upon(someDialogMug);

const getSomeDialogState = r();

const someDialogState = getSomeDialogState(); // 类型为 { draft: number, count: number }

const applyDraft = w((state) => ({ ...state, value: state.draft }));

applyDraft(); // `value` 字段的值会写入 `countMug`

状态与 Action 组合

从库中引用 rw 可以创建在调用时灵活指定状态容器的通用 Action:

tsx 复制代码
import { w } from 'react-mug';

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

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

incrementIt(someOtherCountMug);

或者通过 flat 调用可以把普通 Action 转化为通用 Action 复用:

tsx 复制代码
import { flat } from 'react-mug';

flat(increment)(someOtherCountMug);

而且 React Mug 内置提供了 通用 Action getItsetIt 可以灵活 返回目标状态 和 以合并逻辑生成目标新状态:

tsx 复制代码
import { getIt, setIt } from 'react-mug';

const count = getIt(countMug); // 等价于 const count = getCount();

setIt(countMug, 0); // 等价于 setCount(0);

Action 与 Action 组合

在 Action 内部可以通过 pure 调用使用其他 Action 内部的纯函数:

tsx 复制代码
import { pure, upon } from 'react-mug';

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

const step = w((n, direction: boolean) => (direction ? pure(increment)(n) : pure(decrement)(n)));
tsx 复制代码
import { construction, pure, upon } from 'react-mug';

const factorMug = { [construction]: 5 };

const getDynamicallyMagnifiedCount = upon([countMug, factorMug]).r(([count, factor]) =>
  pure(getMagnifiedCount)(count, factor)
);

注意,不应该在 Action 内部直接使用 Action,避免破坏单向数据流引入行为不确定性。

特性 6:高可测试性

在测试上,得益于函数式内核的特性,以测试纯函数的方式即可有效验证 Action,不需要 Mock,非常容易:

tsx 复制代码
import { pure } from 'react-mug';

describe('step', () => {
  test('adds one on forward direction', () => {
    expect(pure(step)(0, true)).toBe(1);
  });

  test('subtracts one on backward direction', () => {
    expect(pure(step)(0, false)).toBe(-1);
  });
});

特性 7:TypeScript 优先

上面的代码片段都是 TS 编写的,都是强类型,有强类型保障 等于 用得爽 等于 写得快 等于 效率高 等于 早上线 等于 少加班

总结

以上便是 React Mug 的全部特性,有任何建议还请掘友们不吝赐教!

如果掘友们觉得这个思路管理状态靠谱,麻烦在 Repo 上点个 ⭐️ 支持一下,我会尽快完善不足、补全文档,实实在在地帮助同行们和自己在日常工作中早上线、少加班,谢谢!

相关推荐
石小石Orz8 分钟前
Three.js + AI:AI 算法生成 3D 萤火虫飞舞效果~
javascript·人工智能·算法
小行星12511 分钟前
前端预览pdf文件流
前端·javascript·vue.js
join812 分钟前
解决vue-pdf的签章不显示问题
javascript·vue.js·pdf
小行星12517 分钟前
前端把dom页面转为pdf文件下载和弹窗预览
前端·javascript·vue.js·pdf
Lysun00127 分钟前
[less] Operation on an invalid type
前端·vue·less·sass·scss
土豆湿34 分钟前
拥抱极简主义前端开发:NoCss.js 引领无 CSS 编程潮流
开发语言·javascript·css
J总裁的小芒果42 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen961 小时前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
辣条小哥哥1 小时前
electron主进程和渲染进程之间的通信
javascript·electron·ecmascript
咖喱鱼蛋1 小时前
Electron一些概念理解
前端·javascript·electron