大家好,我是 OQ(Open Quoll),做前端十载,今天想分享一款全新的 React 状态库。
它是我在 React 社区见过最好用的函数式状态库。
其最大的亮点是:
以纯函数快速创建可直接调用的状态操作。
同时,还做到了:
零步骤与 React 集成,
善用 ES Modules 标准组织代码,
状态组合,
状态分隔,
异步操作,
函数式地测试状态操作,
轻松测试实际状态变化,
强支持类型。
它一改函数式状态管理繁琐的常态,让高可预测性唾手可得。
话不多说,先用它写个计数器感受一下:
npm i react-mug
// CounterMug.ts
import { construction, upon } from 'react-mug';
export interface CounterState {
value: number;
}
const { r, w } = upon<CounterState>({
[construction]: {
value: 0,
},
});
export const getValue = r((state) => state.value);
export const increase = w((state, delta: number) => ({ ...state, value: state.value + delta }));
// CounterDisplay.tsx
import { useR } from 'react-mug';
import { getValue } from './CounterMug';
export function CounterDisplay() {
const value = useR(getValue);
return <div>Value: {value}</div>;
}
// CounterControl.tsx
import { increase } from './CounterMug';
export function CounterControl() {
return (
<div>
<button onClick={() => increase(1)}>Increase by 1</button>
<button onClick={() => increase(5)}>Increase by 5</button>
</div>
);
}
感官上可以说函数式得浑然一体,以极高投产比实现了高可预测性。
实现效果:
接下来就逐步深入一下每个功能。
以纯函数快速创建可直接调用的状态操作
这个库 upon 接口设计得很精妙。
它接收的范型参数是状态类型。
它接收的变量参数则是状态的设置和标识,称为 Mug,遵守状态类型的约束。
它返回的工具能够只以纯函数快速地创建状态操作:
const counterMug = {
[construction]: {
value: 0,
},
};
const { r, w } = upon<CounterState>(counterMug);
// 状态的读操作
export const getValue = r((state) => state.value);
// 状态的写操作
export const increase = w((state, delta: number) => ({ ...state, value: state.value + delta }));
其中纯函数状态参数的类型会自动推断为状态类型,无需显式声明。
而创建出来的状态操作会根据非状态参数自动确定接收参数,可以直接调用:
const value = getValue();
increase(1);
increase(5);
兼具函数式的高可读性和指令式的高易用性。
进一步地,无参调用 r、w 可以分别创建 “读取全量状态“ 和 “合并写入状态” 的操作:
export const get = r();
const counterState = get();
export const set = w();
set({ value: 10 });
从 Mug 引用 [construction] 字段可以创建 “重置” 操作:
export const reset = w(() => counterMug[construction]);
reset();
不引用 Mug 时,可以将其内联到 upon 调用中精简代码量:
const { r, w } = upon<CounterState>({
[construction]: {
value: 0,
},
});
方便灵活。
零步骤与 React 集成
这里的状态与 React 集成不需要任何步骤准备,包括不需要准备 Provider。
读状态时,直接以 useR 指定读操作:
export function CounterDisplay() {
const value = useR(getValue);
return <div>Value: {value}</div>;
}
写状态时,直接调用写操作:
export function CounterControl() {
return (
<div>
<button onClick={() => increase(1)}>Increase by 1</button>
<button onClick={() => increase(5)}>Increase by 5</button>
</div>
);
}
十分省力。
善用 ES Modules 标准组织状态代码
围绕一个状态,会有其类型、Mug、操作,最终会形成一个状态模块。
不过这个库没有盲目造模块机制。
ES Modules,作为 JS 的官方模块标准,今天已经被普遍支持和应用,
因此它直接选择了善用标准,以文件命名规范将状态模块对应为 ES Modules 进行组织:
// CounterMug.ts —— 文件命名规范,`状态名` + `Mug.ts`
interface CounterState {
...
}
const { r, w } = upon<CounterState>(...);
export const getValue = ...;
export const increase = ...;
巧妙实用。
状态组合
在复杂些的情况中,状态不会一直是孤立的,之间的连接经常是不可避免的。
为此,这个库提供了状态组合(State Composition)机制。
凭借着它,可以如下以新状态中动态输入的 delta 值操作计数器状态的值:
// CounterMug.ts
import { construction, upon } from 'react-mug';
export interface CounterState {
value: number;
}
// 导出计数器 Mug 用于组合
export const counterMug {
[construction]: {
value: 0,
},
};
const { r, w } = upon<CounterState>(counterMug);
export const getValue = r((state) => state.value);
export const increase = w((state, delta: number) => ({ ...state, value: state.value + delta }));
// CountEditorMug.ts —— 新状态
import { construction, upon } from 'react-mug';
import { CounterState, counterMug, increase } from './CounterMug';
export interface CountEditorState {
counter: CounterState;
delta: number;
}
const { r, w } = upon<CountEditorState>({
[construction]: {
// 以 counter 字段引用计数器状态
counter: counterMug,
delta: 1,
},
});
export const getDelta = r((state) => state.delta);
export const setDeltaFromStr = w((state, delta: string) => ({ ...state, delta: parseInt(delta) }));
export const increaseByDelta = w((state) => ({
...state,
// 以纯函数形式调用状态操作
counter: increase(state.counter, state.delta),
}));
// CountEditor.tsx
import { useR } from 'react-mug';
import { getDelta, setDeltaFromStr, increaseByDelta } from './CountEditorMug';
export function CountEditor () {
const delta = useR(getDelta);
return (
<div>
<label>Delta: </label>
<input type="number" value={delta} onChange={(e) => setDeltaFromStr(e.target.value)} />
<button onClick={() => increaseByDelta()}>Increase by delta</button>
</div>
);
}
使得状态有序整合起来。
状态分隔
另一方面,随着状态演进,有些字段可能会构成一个关注点分明的逻辑块,
非常值得提炼出来,以供复用或者只是更清晰地组织代码。
为此,这个库提供了状态分隔(State Segregation)机制。
凭借着它,可以如下在计数器状态中实现远程查询时抽象出 “可查询状态”:
// QueryableMug.ts —— 可查询状态
import { onto } from 'react-mug';
export interface QueryableState {
querying: boolean;
}
// 只需指定状态类型
const { r, w } = onto<QueryableState>();
// 通用状态的读操作
export const isQuerying = r((state) => state.querying);
// 通用状态的写操作
export const startQuerying = w((state) => ({ ...state, querying: true }));
export const endQuerying = w((state) => ({ ...state, querying: false }));
// CounterMug.ts
import { construction, upon } from 'react-mug';
import { QueryableState, isQuerying } from './QueryableMug';
export interface CounterState extends QueryableState {
value: number;
}
export const counterMug = {
[construction]: {
querying: false,
value: 0,
},
};
const { r, w } = upon<CounterState>(counterMug);
export const getValue = r((state) => {
if (isQuerying(state)) {
return;
}
return state.value;
});
export const increase = w((state, delta: number) => ({ ...state, value: state.value + delta }));
export const set = w();
// CounterDisplay.tsx
import { useR } from 'react-mug';
import { startQuerying, endQuerying } from './QueryableMug';
import { counterMug, getValue, set } from './CounterMug';
export function CounterDisplay() {
const value = useR(getValue);
useEffect(() => {
(async () => {
startQuerying(counterMug);
const value = await RestfulApi.counter.value.get();
set({ value });
endQuerying(counterMug);
})();
}, []);
return <div>Value: {value}</div>;
}
// CounterControl.tsx
import { useR } from 'react-mug';
import { isQuerying } from './QueryableMug';
import { counterMug, increase } from './CounterMug';
export function CounterControl() {
const querying = useR(isQuerying, counterMug);
return (
<div>
<button onClick={() => increase(1)} disabled={querying}>Increase by 1</button>
<button onClick={() => increase(5)} disabled={querying}>Increase by 5</button>
</div>
);
}
并且在需要的新状态中复用:
// BriefMug.ts —— 新状态
import { construction, upon } from 'react-mug';
import { QueryableState, isQuerying } from './QueryableMug';
export interface BriefState extends QueryableState {
text: string;
}
export const briefMug = {
[construction]: {
querying: false,
text: '',
},
};
const { r, w } = upon<BriefState>(briefMug);
export const getText = r((state) => {
if (isQuerying(state)) {
return;
}
return state.text;
});
export const set = w();
// Brief.tsx
import { useR } from 'react-mug';
import { startQuerying, endQuerying } from './QueryableMug';
import { briefMug, getText, set } from './BriefMug';
export function Brief() {
const text = useR(getText);
useEffect(() => {
(async () => {
startQuerying(briefMug);
const text = await RestfulApi.brief.text.get();
set({ text });
endQuerying(briefMug);
})();
}, []);
return <div>Text: {text}</div>;
}
使得状态有序拆分开来。
异步操作
状态操作是同步的,不过结合了状态操作和异步步骤的异步过程是常见的,这便是异步操作。
由于灵活性上,简单的状态操作可以通过 get、set 实现,
复杂的状态操作值得创建为自定义操作后调用,
使得普通的异步函数调用状态操作就足以实现异步操作,
因此这个库直接选择了以普通的异步函数创建异步操作:
// CounterMug.ts
import { construction, upon } from 'react-mug';
import { QueryableState, isQuerying, startQuerying, endQuerying } from './QueryableMug';
export interface CounterState extends QueryableState {
value: number;
}
export const counterMug = {
[construction]: {
querying: false,
value: 0,
},
};
const { r, w } = upon<CounterState>(counterMug);
...
export const set = w();
// 异步操作
export const queryValue = async () => {
startQuerying(counterMug);
const value = await RestfulApi.counter.value.get();
set({ value });
endQuerying(counterMug);
};
// CounterDisplay.tsx
import { useR } from 'react-mug';
import { getValue, queryValue } from './CounterMug';
export function CounterDisplay() {
const value = useR(getValue);
// 相应调整
useEffect(() => {
queryValue();
}, []);
return <div>Value: {value}</div>;
}
而通用状态也同理,除了可以借助工具辅助定义 Mug 参数类型:
// QueryableMug.ts
import { onto } from 'react-mug';
export interface QueryableState {
querying: boolean;
}
const { r, w, x } = onto<QueryableState>();
...
export const startQuerying = w((state) => ({ ...state, querying: true }));
export const endQuerying = w((state) => ({ ...state, querying: false }));
// 异步操作
export const query = x(async (mug, act: () => Promise<void>) => {
startQuerying(mug);
await act();
endQuerying(mug);
});
// CounterMug.ts
import { construction, upon } from 'react-mug';
import { QueryableState, isQuerying, query } from './QueryableMug';
export interface CounterState extends QueryableState {
value: number;
}
export const counterMug = {
[construction]: {
querying: false,
value: 0,
},
};
const { r, w } = upon<CounterState>(counterMug);
...
export const set = w();
// 相应调整
export const queryValue = async () => {
await query(counterMug, async () => {
const value = await RestfulApi.counter.value.get();
set({ value });
});
};
得当有力。
函数式地测试状态操作
除了高可读性,构成纯函数高可预测性优势的另一点便是高可测试性。
这里的状态操作均可以用测试纯函数的方式测试:
// CounterMug.test.ts
import { getValue, increase } from './CounterMug';
describe('getValue', () => {
test('returns value', () => {
expect(getValue({ value: 1, querying: false })).toBe(1);
});
});
describe('increase', () => {
test('adds up value and delta', () => {
expect(increase({ value: 1, querying: false }, 2)).toStrictEqual({ value: 3, querying: false });
});
});
轻松可靠。
轻松测试实际状态变化
另一方面,对于测试调用到状态操作的部分,这个库也提供了顺手的工具。
比如,异步操作可以如下测试:
// CounterMug.test.ts
import { getIt, resetIt } from 'react-mug';
import { counterMug, queryValue } from './CounterMug';
describe('queryValue', () => {
afterEach(() => {
resetIt(counterMug);
});
test('sets value as got', async () => {
jest.spyOn(RestfulApi.counter.value, 'get').mockResolvedValueOnce(5);
await queryValue();
expect(getIt(counterMug)).toMatchObject({ value: 5 });
});
});
以及 React 组件可以如下测试:
// CounterControl.test.tsx
import { getIt, setIt, resetIt } from 'react-mug';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { counterMug } from './CounterMug';
import { CounterControl } from './CounterControl';
describe('CounterControl', () => {
afterEach(() => {
resetIt(counterMug);
});
test('increases value on button clicks', async () => {
setIt(counterMug, { value: 1 });
render(<CounterControl />);
await userEvent.click(screen.getByText('Increase by 1'));
await userEvent.click(screen.getByText('Increase by 5'));
expect(getIt(counterMug)).toMatchObject({ value: 7 });
});
});
持续轻松可靠。
强支持类型
所有的示例代码都是以 TypeScript 编写的,而这不是偶然。
考虑到类型可以大幅提高效率、减少 Bug,
这个库特意将类型作为整体设计的一部分做了强支持,
使类型的好处极其自然地引入。
很是难得。
结语
以上是这款函数式状态库的全部功能了。
为了方便回顾,再次整理如下:
以纯函数快速创建可直接调用的状态操作,
零步骤与 React 集成,
善用 ES Modules 标准组织代码,
状态组合,
状态分隔,
异步操作,
函数式地测试状态操作,
轻松测试实际状态变化,
强支持类型。
希望帮到大家少加班、多上线。
最后,附上库的 GitHub 链接:
欢迎使用和交流,感谢阅读。