如果觉得状态烫手,不要担心,把它装进马克杯里,用带把的杯子就能轻松地拿起来烫手的咖啡。
(友情提示:本文中部分 API 仅适用于 react-mug@0.5.x 版本。)
大家好,我是 OQ(Open Quoll),一个专注于状态管理的前端人。
今天想聊的是一款全新的状态库 React Mug,它的核心理念是 持续快 地 灵活 管理状态。
持续快是指:
- 用法简洁
- 纯函数主导
- 强支持类型
灵活则是:
- 独立于 Hook
- 组合机制
不过展开之前,我们先实现一个 “计数器” 看一下它的基本用法:
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>
</>
);
}
实现效果:
持续快
代码写得快才可能少加班。但是快又不能只在一时。业务时常发展,代码时常变更,快还要在持续。
这便是 React Mug 放在第一位的理念:持续快。为此,它同时做到了:
- 用法简洁
- 纯函数主导
- 强支持类型
用法简洁
库的用法简洁能够减少写代码的量。实现一个功能,写的代码量少了,自然就写得快了。
而用法简洁的基础是概念简洁。
因此 React Mug 从数据流上就保证了简洁:
随后,用法的简洁便水到渠成了。
通过 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 的第二个理念:灵活。
这包括了:
- 独立于 Hook
- 组合机制
独立于 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 })),
}));
预期效果:
Mug 与 Action 组合
其次,Mug 可以在位置、效果上与 Action 关联进行组合。
在位置上,Action 可以 附属 或 游离 于 Mug。
在效果上,Action 可以 绑定 或 不拘 于 Mug。
之前,通过 create 回调中 r、w 创建的,便是在位置上附属、效果上绑定于 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();
另外,通过从库中直接引用的 r、w,可以创建在位置上游离、效果上不拘于 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 回调中的 r、w 转化为附属特殊 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),
})),
...
}));
预期效果:
如果需要调用的是游离通用 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 上,希望大家喜欢,欢迎点 ⭐️ 收藏,或者留言交流。
状态如咖啡,很烫,但是把它装进马克杯中,愿君得到的只是香醇。