简介
helux 是一个集atom
、signal
、依赖追踪
为一体,支持细粒度响应式更新的状态引擎,兼容所有类 react 库,包括 react18。
btw:
helux
是目前唯一一个将细粒度响应式更新
特性带到react
开发者面前的框架
架构
helux
包含了core
层和适配层,core
层基于最快的不可变数据操作库limu构建,包含了状态,动作和副作用3大模块,我们可以把core
层理解为状态引擎的核心驱动包。
基于core
层我们继续向上构建了适配react
的helux
包,该包对接了react基础钩子,实现了atom 、signal 、依赖追踪 、双向绑定 、细粒度响应式更新 、观察 、派生等常用功能或特性。
注意架构里的红色区域里是react-like
,强调helux
整体架构并非与react
强绑定,只要满足提供了图示中几个api的类react
库,core
就可以秒适配并导出所有功能。
helux
是我们默认适配好react
而发布的包体
所以除了react
自身,我们为此还适配了preact
,同时也支持和现阶段各个生态的其他框架集成使用,例如nextjs
,可查看下来各个链接体验。
-
helux-react-starter helux & react 在线示例
-
helux-preact-starter helux & preact 在线示例
-
helux-nextjs-starter helux & nextjs 示例仓库
如果想在其他类
react
库中使用helux
,也可以参考 helux-preact-starter 示例去自行适配。
优势
综合上面的架构图,不难看出,helux
相比现阶段开源社区较出名的状态管理库(redux
,recoil
,jotai
,zustand
,mobx
等)的优势较为显著:
- 内置依赖追踪特性,基于最快的不可变 js 库limu开发,拥有超强性能
atom
支持任意数据结构且自带依赖收集功能, 无需拆分很细,天然对 DDD 领域驱动设计友好- 内置
signal
响应机制,实现 0 hook 编码 dom 粒度或块粒度的更新 - 内置
loading
模块,可管理所有异步任务的运行状态、并捕捉错误抛给组件、插件 - 内置
sync
系列 api,支持双向绑定,轻松应对表单处理 - 内置
reactive
响应式对象,支持数据变更直接驱动关联 ui 渲染 - 内置
define
系列 api,方便对状态模块化抽象,轻松驾驭大型前端应用架构 - 内置事件系统
- 支持可变派生
mutate derive
,适用于当共享对象 a 部分节点变化需引起其他节点自动变化的场景,数据更新粒度更小 - 支持全量派生
full derive
,适用于不需要对数据做细粒度更新的场景 - 全量派生、可变派生均支持异步任务
- 全量派生、可变派生除数据变更驱动执行外,还支持人工重新触发运行
- 支持中间件、插件系统,可无缝对接 redux 生态相关工具库
- 100% ts 编码,类型安全
落地场景
腾讯新闻web
腾讯新闻web是一个迭代了很多年的老项目,在7年前就引入了react技术栈,采用了csr
+ssr
混合渲染架构,在实际开发过程中,很多老组件在尽可能不动代码的情况下需要共享状态,即同一个组件的多个实例状态是通用的,例如这样一个运行多年的关注按钮。
旧代码类似
js
function FlowButton(){
const [state, setState] = useState({...});
const clickButton = ()=>{
// 逻辑略
// setState({ isFollowed: true})
};
}
这样一个按钮刚开始只显示一个,随着需求变化,按钮需要底部显示,或者其他排版显示时出现了一屏2个关注按钮同时存在,这时候旧代码面临着需要状态提升的问题,在改造老代码时尤为慎重,故如何已最小的代价完成状态共享,早点下班回家才是我们想要达成的目标。
为了不动原有代码,我们以useState
作为切入点,接入helux
的useShared
将其替换掉,就完成了我们需要最小代价共享状态的目的。
注:useShared 是v2版本提供的接口,v3已命名为 useAtom
diff
import React from 'react';
+ import { createShared, useShared } from 'helux';
+ const { state: sharedObj } = createShared({a:100, b:2});
function HelloHelux(props: any) {
- const [state, setState] = React.useState({ a: 100, b: 2 });
+ const [state, setState] = useShared(sharedObj);
return <div>{state.a}</div>; // 当前组件仅依赖a变更才触发重渲染
}
腾讯新闻运营平台
C端对包体大小敏感,故使用了的是裁剪了大量功能,只关注状态共享的v2版本(gzip后2kb),在对内使用的运营平台上,则可以放开手脚,尽一切可能提高开发体验和运行效率,故在>=v3
版本后我们基于limu
继续构建完全颠覆了传统开发模式的新版本。
在构建新版本helux
的同时,还引入了工具链无关的微模块技术hel-micro搭建了一套全新开发模式的react 微前端架构的运营平台。
基于 helux + hel-micro 构建的基于微模块的 react 微前端元框架
helra
我们也将在上半年开源出去,敬请期待。
在这个模式下,我们可以精选化的管理动态模块资源,做到面向不同场景灵活组合出定制的应用(例如灰度、按地域放量、按分支提供某个子应用的测试链接等)。
继续结合公司的ci&cd体系可做到全生命周期的模块管控流程闭环(开发、部署、上线、运维)。
其他
其他内外部其他小伙伴也在使用中的项目,这里就不再一一提及,感谢大家的信任与支持,同时也感谢一些使用者积极共建生态,贡献了其他面向特定场景的封装库,例如面向表单的speed-form 等。
特性一览
此章节我们先了解如何快速开始,然后简单介绍各个重磅特性,包含atom 、signal 、依赖追踪 、双向绑定 、细粒度响应式更新 、观察 、派生 等特性,同时建议访问官网文档了解更多并体验,每一个api我们都提供了保姆级的配套demo代码和渲染好的可演示组件。
快速开始
阅读此章节可简单了解helux
常用接口并快速学会使用它们。
定义 atom
支持定义任意数据结构 atom 对象,被包装为{val:T}
结构
ts
import { atom } from 'helux';
// 原始类型 atom
const [numAtom] = atom(1);
// 字典对象类型 atom
const [objAtom] = atom({ a: 1, b: { b1: 1 } });
修改 atom
原始值修改
ts
const [numAtom, setAtom] = atom(1);
setAtom(100);
字典对象修改,基于setAtom
接口回调里的草稿对象直接修改即可
ts
const [numAtom, setAtom] = atom({ a: 1, b: { b1: 1 } });
setAtom((draft) => {
// draft 已拆箱 { val: T } 为 T
draft.b.b1 += 1;
});
或基于reactive
响应式对象修改,数据变更在下一次事件循环微任务开始前被提交。
ts
const [numAtom, setAtom, {reactive}] = atom({ a: 1, b: { b1: 1 } });
function change(){
reactive.b.b1 += 1;
}
或定义action
修改
ts
const [numAtom, setAtom, { action, defineActions }] = atom({ a: 1, b: { b1: 1 } });
// 方式1:裸写 action
const change = action()(({draft})=>{
draft.b.b1 += 1;
}, 'change');
change(); // 触发变更
// 方式2:调用可读性更友好的 defineActions
const { actions } = defineActions()({
change({draft}){
draft.b.b1 += 1;
},
// 可以继续定义其他 action
});
actions.change(); // 触发变更
通过 action 函数调用有 2 大好处
- 接入 devltool 后状态修改历史可详细追溯
- 异步函数可自动享受下文提到的
loading
管理能力
观察 atom
可观察整个根对象变化,也可以观察部分节点变化
ts
import { atom, watch, getSnap } from 'helux';
watch(
() => {
console.log(`change from ${getSnap(numAtom).val} to ${numAtom.val}`);
},
() => [atom],
);
watch(
() => {
console.log(
`change from ${getSnap(numAtom).val.b.b1} to ${numAtom.val.b.b1}`,
);
},
() => [objAtom.val.b.b1],
);
派生 atom
- 1 全量派生
derive
接口接受一个派生函数实现,返回一个全新的派生值对象,该对象是一个只可读的稳定引用,全局使用可总是读取到最新值。
ts
import { atom, derive } from 'helux';
const [numAtom, setAtom] = atom(1);
const plus100 = derive(() => atom.val + 100);
setAtom(100);
console.log(plus100); // { val: 200 }
setAtom(100); // 设置相同结果,派生函数不会再次执行
使用已派生结果继续派生新的结果
ts
const plus100 = derive(() => atom.val + 100);
const plus200 = derive(() => plus100.val + 200);
- 2 可变派生
当共享对象 a 的发生变化后需要自动引起共享状态 b 的某些节点变化时,可定义 mutate
函数来完成这种变化的连锁反应关系,对数据做最小粒度的更新
ts
import { atom, derive } from 'helux';
const [ objAtom1, setAtom ] = atom({a:1,b:{b1:1}});
const [objAtom2] = atom(
{ plusA100: 0 }
{
// 当 objAtom1.val.a 变化时,重计算 plusA100 节点的值
mutate: {
changePlusA100: (draft) => draft.plusA100 = objAtom1.val.a + 100,
}
},
);
setAtom(draft=>{ draft.a=100 });
console.log(objAtom2.val.plusA100); // 200
使用 atom
react 组件通过useAtom
钩子可使用 atom 共享对象,该钩子返回一个元组,使用方式和 react.useState
类似,区别在于对于非原始对象,回调提供草稿供用户直接修改,内部会生成结构化共享的新状态
tsx
import { atom, useAtom } from 'helux';
const [numAtom] = atom(1);
export default function Demo() {
// 返回结果自动拆箱
const [num, setAtom] = useAtom(numAtom);
return <h1 onClick={() => setAtom(Math.random())}>{num}</h1>;
}
atom 对象天然是全局共享的,可将 atom 对象提供给多个组件实例使用
tsx
import { atom, useAtom } from 'helux';
const [objAtom, setAtom] = atom({ name: 'hello helux', info: { age: 1 } });
function Demo() {
const [obj, setAtom] = useAtom(objAtom);
const changeName = () =>
setAtom((draft) => {
draft.info.age += 1;
});
return (
<h1 onClick={() => setAtom(Math.random())}>
{obj.name} {obj.info.age}
<button onClick={changeName}>changeName</button>
</h1>
);
}
export default () => (
<>
<Demo />
<Demo />
</>
);
Signal
signal
响应机制允许用户跳过useAtom
直接将数据绑定到视图,实现 0 hook 编码、dom 粒度 或块粒度更新。
dom 粒度更新
使用$
符号绑定单个原始值创建信号响应块,实现 dom 粒度更新
tsx
import { $ } from 'helux';
// 数据变更仅触发 $符号区域内重渲染
<h1>{$(numAtom)}</h1> // 包含原始值的atom可安全绑定
<h1>{$(shared.b.b1)}</h1>// 对象型需自己取到原始值绑定
块粒度更新
使用block
绑定多个原始值创建局部响应块,实现块粒度更新
ts
// UserBlock 已被 memo
const UserBlock = block(() => (
<div>
name: {user.name}
desc: {user.detail.desc}
</div>
));
// 其他地方使用 UserBlock
<UserBlock />;
依赖追踪
除了对$
、block
这些静态节点建立起视图对数据变化的依赖关系,使用useAtom
方式的组件渲染期间将实时收集到数据依赖
依赖收集
组件时读取数据节点值时就产生了依赖,这些依赖被收集到helux
内部为每个组件创建的实例上下文里暂存着,作为更新凭据来使用。
helux 内部默认的收集深度为6,可自己按需调节。
tsx
const { state, setDraft, useState } = atomx({ a: 1, b: { b1: 1 } });
// 修改草稿,生成具有数据结构共享的新状态,当前修改只会触发 Demo1 组件渲染
const changeObj = () => setDraft((draft) => (draft.a = Math.random()));
function Demo1() {
const [obj] = useState();
// 仅当 obj.a 发生变化时才触发重渲染
return <h1>{obj.a}</h1>;
}
function Demo2() {
const [obj] = useState();
// 仅当 obj.b.b1 发生变化时才触发重渲染
return <h1>{obj.b.b1}</h1>;
}
依赖变更
存在 if
条件时,每一轮渲染期间收集的依赖将实时发生变化
tsx
import { atomx } from 'helux';
const { state, setDraft, useState } = atomx({ a: 1, b: { b1: 1 } });
const changeA = () => setDraft((draft) => (draft.a += 1));
const changeB = () => setDraft((draft) => (draft.a.b1 += 1));
function Demo1() {
const [obj] = useState();
// 大于 3 时,依赖为 a, b.b1
if (obj.a > 3) {
return (
<h1>
{obj.a} - {obj.b.b1}
</h1>
);
}
return <h1>{obj.a}</h1>;
}
依赖比较
得益于limu产生的结构共享数据,helux
内部可以高效的比较快照变更部分,当用户重复设置相同的值组件将不被渲染
tsx
import { atomx } from 'helux';
const { state, setDraft, useState } = atomx({
a: 1,
b: { b1: { b2: 1, ok: true } },
});
const changeB1 = () => setDraft((draft) => (draft.b.b1 = { ...draft.b.b1 }));
const changeB1_Ok_oldValue = () =>
setDraft((draft) => (draft.b.b1.ok = draft.b.b1.ok));
const changeB1_Ok_newValue = () =>
setDraft((draft) => (draft.b.b1.ok = !draft.b.b1.ok));
// 调用 changeB1_Ok_oldValue changeB1 Demo1 不会被重渲染
// 调用 changeB1_Ok_newValue ,Demo1 被重渲染
function Demo1() {
const [obj] = useState();
return <h1>obj.b.b1.ok {`${obj.b.b1.ok}`}</h1>;
}
响应式
因atom
返回的 state
是只可读数据,变更必须配合setState
,同时 atom
也提供响应式对象,可直接操作修改,变化部分数据会在下一次事件循环微任务开始前执行
直接修改
ts
import { atom } from 'helux';
import { delay } from '@helux/demo-utils';
// reactive 已自动拆箱
const { state, reactive } = atomx({ a: 1, b: { b1: { b2: 1, ok: true } } });
async function change() {
reactive.a = 100;
console.log(state.val.a); // 1
await delay(1);
console.log(state.val.a); // 100
}
组件中使用
组件中可使用useReactive
钩子来获得响应式对象
tsx
import { sharex } from 'helux';
const { reactive, useReactive } = sharex({
a: 1,
b: { b1: { b2: 1, ok: true } },
});
// 定时修改 a b2
setTimeout(() => {
reactive.a += 1;
reactive.b.b1.b2 += 1;
}, 2000);
// 组件外部修改 ok
function toogleOkOut() {
reactive.b.b1.ok = !reactive.b.b1.ok;
}
function Demo() {
const [reactive] = useReactive();
return <h1>{reactive.a}</h1>;
}
function Demo2() {
const [reactive] = useReactive();
return <h1>{reactive.b.b1.b2}</h1>;
}
function Demo3() {
const [reactive] = useReactive();
// 组件内部切换 ok
const toogle = () => (reactive.b.b1.ok = !reactive.b.b1.ok);
return <h1>{`${reactive.b.b1.ok}`}</h1>;
}
signal 中使用
可直接将 reactive
值传给 $
原始值响应或block
块响应
tsx
import { $, block, sharex } from 'helux';
const { reactive } = sharex({
a: 1,
b: { b1: { b2: 1, ok: true } },
});
function InSignalZone() {
return <h1>{$(reactive.a)}</h1>;
}
const InBlockZone = block(() => {
return (
<div>
<h3>{reactive.a}</h3>
<h3>{reactive.b.b1.b2}</h3>
</div>
);
});
主动flush
input
组件实时输入过程中,需主动调用flush
接口刷新状态,避免中文输入法出现中文无法提示的问题。
tsx
import { sharex } from 'helux';
const { reactive, useState, flush } = sharex({ str: '' });
function change(e) {
reactive.str = e.target.value;
// 去掉 flush 调用,中文输入法无法录入汉字
flush();
}
双向绑定
提供syncer
和 sync
函数生成数据同步器,可直接绑定到表达相关onChange
事件,同步器会自动提取事件值并修改共享状态,达到双向绑定的效果!
浅层数据绑定
只有一层 json path 的对象,可以使用 syncer
生成数据同步器来绑定
tsx
const { syncer, state } = sharex({ a: 1, b: { b1: 1 }, c: true });
<input value={state.a} onChange={syncer.a} />;
<input type="checkbox" checked={state.c} onChange={syncer.c} />;
syncer
会自动分析是否是事件对象,是就提取值不是就直接传值,所以也可以很方便的绑定 ui 组件库
tsx
import { Select } from 'antd';
<Select value={state.a} onChange={syncer.a} />;
原始值 atom 绑定时,传递syncer
自身即可
tsx
const { syncer, useState } = atomx('');
function Demo1() {
const [state] = useState();
return <input value={state} onChange={syncer} />;
}
深层数据绑定
多层 json path 的对象,使用 sync
生成数据同步器来绑定,可通过回调设定绑定节点
tsx
// 数据自动同步到 to.b.b1 下
<input value={state.b.b1} onChange={sync((to) => to.b.b1)} />
可传递路径字符串数组定义绑定目标节点
tsx
<input value={state.b.b1} onChange={sync(['b', 'b1'])} />
拦截修改
sync
函数提供 before
回调给用户,支持数据提交前做二次修改
tsx
<input
value={num}
onChange={sync(
(to) => to.b.b1,
(val) => {
return val === '888' ? 'boom' : val;
},
)}
/>
支持before
回调里修改其他值
tsx
<input
value={num}
onChange={sync(
(to) => to.b.b1,
(val, params) => {
if (val === '888') {
params.draft.b2 = 'b2 changed';
return 'boom';
}
},
)}
/>
派生
支持全量派生
和可变派生
,两种派生
都支持定义异步计算任务,执行时间除了观察数据变化自执行以外,均可人工触发。
全量派生
derive
接口该接受一个派生函数实现,返回一个全新的派生值对象,该对象是一个只可读的稳定引用,全局使用可总是读取到最新值。
ts
import { atom, derive } from 'helux';
const [numAtom] = atom(5);
const [info] = share({
a: 50,
c: { c1: 100, c2: 1000 },
list: [{ name: 'one', age: 1 }],
});
// 仅在 numAtom.val 或 info.c.c1 发生变化后才会重运行计算出新的 result
const result = derive(() => {
return numAtom.val + info.c.c1;
});
// 定义异步全量派生
const resultAsync = derive({
fn: ()=>0, // 初始值
deps: ()=>[numAtom.val, info.c.c1], // 依赖函数,返回值会透传给 input
task: async({ input }){
await delay(1000);
return input[0] + input[1];
},
});
可变派生
由于 atom
和 share
返回的对象天生自带依赖追踪特性,当共享对象 a 的发生变化后需要自动引起共享状态 b 的某些节点变化时,可定义 mutate
函数来完成这种变化的连锁反应关系,对数据做最小粒度的更新
ts
import { atom, share, mutate } from 'helux';
const [baseAtom] = atom(1);
const [numAtom] = atom(3000);
// baseAtom 变化计算 numAtom
mutate(numAtom)({
fn: (draft) => (draft.val = baseAtom.val + 100),
desc: 'mutateNumAtomVal',
});
// 定义异步可变派生
mutate(numAtom)({
deps: ()=>[baseAtom.val], // 依赖函数,返回值会透传给 input
task: async({draftRoot, input}){
await delay(1000);
draftRoot.val = input[0] + 100; // 直接修改 draft
},
desc: 'mutateNumAtomVal',
});
观察
helux
在内部为实现更智能的自动观察变化做了大量优化工作,同时也暴露了相关接口支持用户在一些特殊场景做人工的观察变化。
watch
使用watch
可观察 atom 对象自身变化或任意多个子节点的变化。
观察函数立即执行,首次执行时收集到相关依赖
ts
import { share, watch, getSnap } from 'helux';
const [priceState, setPrice] = share({ a: 1 });
watch(
() => {
// 首次执行日志如下
// price change from 1 to 1
//
// 反复调用 changePrice,日志变化如下
// price change from 1 to 101
// price change from 101 to 201
console.log(
`price change from ${getSnap(priceState).a} to ${priceState.a}`,
);
},
{ immediate: true },
);
const changePrice = () =>
setPrice((draft) => {
draft.a += 100;
});
观察函数不立即执行,通过 deps 函数定义需要观察的数据,观察的粒度可以任意定制
ts
const [priceState, setPrice] = share({ a: 1 });
const [numAtom, setNum] = atom(3000);
// 观察 priceState.a 的变化
watch(
() => {
console.log(`found price.a changed: () => [priceState.a]`);
},
() => [priceState.a],
// 或写为
// { deps: () => [priceState.a] }
);
// 观察整个 priceState 的变化
watch(
() => {
console.log(`found price changed: [ priceState ]`);
},
() => [priceState],
);
// 观察整个 priceState 和 numAtom 的变化
watch(
() => {
console.log(`found price or numAtom changed: ()=>[ priceState, numAtom ]`);
},
() => [priceState, numAtom],
);
即设置依赖函数也设置立即执行,此时的依赖由 deps
和 watch
共同收集到并合并而得。
ts
watch(
() => {
const { a } = priceState;
console.log(`found one of them changed: [ priceState.a, numAtom ]`);
},
{ deps: () => [numAtom], immediate: true },
);
watchEffect
watchEffect
回调会立即执行,自动对首次运行时函数内读取到的值完成变化监听
ts
import { watchEffect, getSnap } from ' helux ';
const [priceState, setPrice] = share({ a: 1 });
// 观察 priceState.a 的变化
watchEffect(() => {
console.log(`found price.a changed from ${getSnap(priceState).a} to ${priceState.a}`);
});
useWatch
提供useWatch
让开发者在组件内部观察变化
tsx
import { getSnap, share, useWatch } from 'helux';
const [priceState, setPrice] = share({ a: 1 });
function changeA() {
setPrice((draft) => void (draft.a += 1));
}
function Comp(props: any) {
const [tip, setTip] = React.useState('');
// watch 回调随组件销毁会自动取消监听
useWatch(
() => {
setTip(
`priceState.a changed from ${getSnap(priceState).a} to ${priceState.a}`,
);
},
() => [priceState.a],
);
return <h1>watch tip: {tip}</h1>;
}
useWatch 无闭包陷阱问题,总能感知闭包外的最新值
tsx
import { $, share, useObject, useWatch } from 'helux';
const [priceState, setPrice] = share({ a: 1 });
function changeA() {
setPrice((draft) => void (draft.a += 1));
}
function Comp(props: any) {
const [obj, setObj] = useObject({ num: 1 });
const [tip, setTip] = React.useState('');
useWatch(
() => {
// priceState.a changed, here can read the latest num
setTip(`num in watch cb is ${obj.num}`);
},
() => [priceState.a],
);
}
useWatchEffect
在组件中使用 useWatchEffect
来完成状态变化监听,会在组件销毁时自动取消监听。
useWatchEffect
功能同 watchEffect``一样,区别在于
useWatchEffect` 会立即执行回调,自动对首次运行时函数内读取到的值完成变化监听。
tsx
import { share, useMutable, useWatchEffect, getSnap } from 'helux';
const [priceState, setState, ctx] = share({ a: 1, b: { b1: { b2: 200 } } });
function changeA() {
setState((draft) => {
draft.a += 1;
});
}
export default function Comp(props: any) {
const [ state, setState] = useMutable({tip:'1'})
useWatchEffect(() => {
// 自动收集到 priceState.a 依赖
setState(draft=>{
draft.tip = `priceState.a changed from ${getSnap(priceState).a} to ${priceState.a}`;
});
});
}
模块化
尽管atom
共享上下文提供了action
、derive
、mutate
、userState
、userActionLoading
、userMutateLoading
等一系列 api 方便用户使用各项功能,但这些 api 比较零碎,处理大型前端应用时用户更希望面向领域模型对状态的state
、derive
、action
建模,故共享上下文还提供define
系列 api 来轻松驾驭此类场景。
为了开发者工具能够查看模块化
相关变更动作记录,配置moduleName
即可
defineActions
批量定义状态对应的修改函数,返回 { actions, eActions, getLoading, useLoading, useLoadingInfo }
, 组件中可通过 useLoading 读取异步函数的执行中状态 loading、是否正常执行结束 ok、以及执行出现的错误 err, 其他地方可通过 getLoading 获取
ts
// 【可选】约束各个函数入参 payload 类型
type Payloads = {
changeA1: number;
foo: boolean | undefined;
// 不强制要求为每一个action key 都定义 payload 类型约束,但为了可维护性建议都补上
};
// 不约束 payloads 类型时写为 ctx.defineActions()({ ... });
const { actions, eActions, useLoading, getLoading } =
ctx.defineActions<Payloads>()({
// 同步 action,直接修改草稿
changeA1({ draft, payload }) {
draft.a.b.c += payload;
},
// 同步 action,返回结果
changeA2({ draft, payload }) {
draft.a.b.c += payload;
return true;
},
// 同步 action,直接修改草稿深节点数据,使用 merge 修改浅节点数据
changeA3({ draft, payload, merge }) {
draft.a.b.c += payload;
merge({ c: 'new desc' }); // 等效于 draft.c = 'new desc';
return true;
},
// 异步 action,直接修改草稿
async foo1({ draft, payload }) {
await delay(3000);
draft.a.b.c += 1000;
},
// 异步 action,多次直接修改草稿,合并修改多个状态,同时返回一个结果
async foo2({ draft, payload, merge }) {
draft.a.b.c += 1000;
await delay(3000); // 进入下一次事件循环触发草稿提交
draft.a.b.c += 1000;
await delay(3000); // 再次进入下一次事件循环触发草稿提交
const { list, total } = await fetchList();
merge({ list, total }); // 等价于 draft.list = list, draft.tatal = total
return true;
},
});
多个 action 组合为一个新的 action
ts
const { actions, eActions, useLoading, getLoading } =
ctx.defineActions<Payloads>()({
foo() {},
bar() {},
baz() {
actions.foo();
actions.bar();
},
});
调用 actions.xxx
执行修改动作,actions 方法调用只返回结果,如出现异常则抛出,同时也会发送给插件和伴生 loading 状态
defineFullDerive
批量定义状态对应的全量派生函数,返回结果形如
{ result, helper: { [key]: runDeriveFn, runDeriveTask, useDerived, useDerivedInfo } }
ts
type DR = {
a: { result: number };
c: { deps: [number, string]; result: number };
// 不强制要求为每一个 result key 都定义 deps 返回类型约束和 result 类型约束,但为了可维护性建议都补上
};
const df = ctx.defineFullDerive<DR>()({
a: () => priceState.a.b.c + 10000,
b: () => priceState.a.b.c + 20000,
c: {
// DR['c']['result'] 将约束此处的 deps 返回类型
deps: () => [priceState.a.b1.c1, priceState.info.name],
fn: () => 1,
async task(params) {
const [c1, name] = params.input; // 获得类型提示
await delay(2000);
return 1 + c1;
},
},
});
结语
一直以来,支持细粒度响应式
更新成为react
开发者梦寐以求的特性,而支持此特性,就需要singal
原语和依赖收集
特性,本质来说这和react
追求不可变是相互矛盾的,而helux
则跳出常规思维,保持react
不可变的精髓,把可变放置到另一个空间去操作,每次生成一份全新的具有结构共享特性的数据快照后,再传递给react
即可。
注意这样去做的不只是helux
,采取开辟新空间做可变修改,再生成快照给react
策略还有mobx-react
、valtio
,采取atom
路线的有recoid
、jotai
。
而同时做到 atom
+ signal
+ 依赖追踪
,并支持细粒度响应式
只有helux
,所以helux
的目标是期望重定义react开发范式,并全面提升react应用的 DX
和 UX
(开发体验、用户体验),为了这个目标我们耕耘近3年(包含了初代仅支持浅层依赖追踪的 concent,到打磨不可变数据操作库 limu,再到构建 helux 整个历程),现全面开源提供给广大react
开发者使用,让我们一起来体验全新的开发模式吧。
友链:
❤️ 你的小星星是我们开源最大的精神动力,欢迎关注以下项目:
helux 集atom、signal、依赖追踪为一体,支持细粒度响应更新的状态引擎
limu 最快的不可变数据js操作库.
hel-micro 工具链无关的运行时模块联邦sdk.