大家好,我是 OQ(Open Quoll),一个前端人,从业几年用 React 写过大大小小的项目,终于无法继续妥协于现有 React 状态库的局限,决定整理思路,新写了一个状态库,旨在帮助同行们和自己在日常工作中早上线、少加班,特性如下,请掘友们过目:
- 简洁,还是简洁!
- 独立于 Hook。
- 函数式内核。
- 基于 Action 的单向数据流。
- 高可组合性。
- 高可测试性。
- 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 setCount
、increment
、decrement
。
最后是 在 React 组件中使用状态只需要使用 useIt
这一个 Hook。
任何没有实际用处的概念、API 一律去掉。
特性 2:独立于 Hook
在简洁的基础上,React Mug 提供独立于 Hook 的状态访问,即 读写状态都直接调用 Action 进行。
比如,案例中的 getCount
可以独立于 useIt
直接读取状态:
tsx
const count = getCount();
比如,setCount
、increment
、decrement
都是直接调用。
独立于 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,内部是与 increment
、decrement
类似的纯函数。
特性 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 组合
从库中引用 r
和 w
可以创建在调用时灵活指定状态容器的通用 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 getIt
和 setIt
可以灵活 返回目标状态 和 以合并逻辑生成目标新状态:
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 上点个 ⭐️ 支持一下,我会尽快完善不足、补全文档,实实在在地帮助同行们和自己在日常工作中早上线、少加班,谢谢!