集atom、signal、依赖追踪为一体的helux发布了,为react带来细粒度响应式更新能力

简介

helux 是一个集atomsignal依赖追踪为一体,支持细粒度响应式更新的状态引擎,兼容所有类 react 库,包括 react18。

btw:helux是目前唯一一个将细粒度响应式更新特性带到react开发者面前的框架

架构

helux包含了core层和适配层,core层基于最快的不可变数据操作库limu构建,包含了状态,动作和副作用3大模块,我们可以把core层理解为状态引擎的核心驱动包。

基于core层我们继续向上构建了适配reacthelux包,该包对接了react基础钩子,实现了atomsignal依赖追踪双向绑定细粒度响应式更新观察派生等常用功能或特性。

注意架构里的红色区域里是react-like,强调helux整体架构并非与react强绑定,只要满足提供了图示中几个api的类react库,core就可以秒适配并导出所有功能。

helux是我们默认适配好react而发布的包体

所以除了react自身,我们为此还适配了preact,同时也支持和现阶段各个生态的其他框架集成使用,例如nextjs,可查看下来各个链接体验。

如果想在其他类react库中使用helux,也可以参考 helux-preact-starter 示例去自行适配。

优势

综合上面的架构图,不难看出,helux相比现阶段开源社区较出名的状态管理库(reduxrecoiljotaizustandmobx等)的优势较为显著:

  • 内置依赖追踪特性,基于最快的不可变 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作为切入点,接入heluxuseShared将其替换掉,就完成了我们需要最小代价共享状态的目的。

注: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 等。

特性一览

此章节我们先了解如何快速开始,然后简单介绍各个重磅特性,包含atomsignal依赖追踪双向绑定细粒度响应式更新观察派生 等特性,同时建议访问官网文档了解更多并体验,每一个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();
}

双向绑定

提供syncersync函数生成数据同步器,可直接绑定到表达相关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];
  },
});

可变派生

由于 atomshare 返回的对象天生自带依赖追踪特性,当共享对象 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],
);

即设置依赖函数也设置立即执行,此时的依赖由 depswatch 共同收集到并合并而得。

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共享上下文提供了actionderivemutateuserStateuserActionLoadinguserMutateLoading等一系列 api 方便用户使用各项功能,但这些 api 比较零碎,处理大型前端应用时用户更希望面向领域模型对状态的statederiveaction建模,故共享上下文还提供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-reactvaltio,采取atom路线的有recoidjotai

而同时做到 atom + signal + 依赖追踪,并支持细粒度响应式只有helux,所以helux的目标是期望重定义react开发范式,并全面提升react应用的 DXUX (开发体验、用户体验),为了这个目标我们耕耘近3年(包含了初代仅支持浅层依赖追踪的 concent,到打磨不可变数据操作库 limu,再到构建 helux 整个历程),现全面开源提供给广大react开发者使用,让我们一起来体验全新的开发模式吧。

友链:

❤️ 你的小星星是我们开源最大的精神动力,欢迎关注以下项目:

helux 集atom、signal、依赖追踪为一体,支持细粒度响应更新的状态引擎

limu 最快的不可变数据js操作库.

hel-micro 工具链无关的运行时模块联邦sdk.

相关推荐
qq_589568103 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈1 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安1 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se2 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235612 小时前
web 渗透学习指南——初学者防入狱篇
前端
z千鑫2 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js
m0_748250743 小时前
Web入门常用标签、属性、属性值
前端