helux,一个鼓励服务注入的响应式react状态库
发布于 2 个月前 作者 fantasticsoul 1266 次浏览 来自 分享

关于helux

helux是一个鼓励服务注入,并支持响应式变更react的全新数据流方案,它的前身是concent(一个类vue开发体验的高性能状态管理框架),但concent自身因为需要兼容class和function保持一致的语法,且为了对其setup功能,导致内部代码量实在太大,压缩后有70多kb,api暴露得也非常多,导致学习难度急剧上升,为了更符合现在非常流行的DDD围绕业务概念构建领域模型的编码趋势,helux一开始就设计为鼓励服务注入支持响应式变更支持依赖收集的轻量级react数据流方案。

它拥有以下优势:

  • 轻量,压缩后2kb
  • 简单,仅暴露7个api,高频使用的仅createShareduseObjectuseSharedObjectuseService4个接口
  • 高性能,自带依赖收集
  • 响应式,支持创建响应式对象,在视图之外变更对象将同步更新视图
  • 服务注入,配合useService接口轻松控制复杂业务逻辑,总是返回稳定的引用,可完全避免useCallback依赖烦扰了
  • 状态提升0改动,所以地方仅需将useObject换为useSharedObject即可提升状态共享到其他组件
  • 避免forwordRef 地狱,内置的exposeService模式将轻松解决父掉子时的ref转发晦涩理解问题和传染性(隔代组件需要层层转发)
  • ts友好,100% ts 编写,为你提供全方位类型提示

2.gif

该gif图和以下所有api均对应有在线示例1示例2,欢迎fork并修改体验。

为什么起名helux,虽然内心上我是把它作为concent v3版本去开发的,但因为它的变化实在太大,除了依赖收集不继承任何concent的特性,同时它也是伴随我开发的hel-micro诞生一款作品,我期望它成为 hel-micro 生态的 luxury 级别的贡献,就将 hel-micro 和 luxury 两个词拼一起成为了 helux

欢迎点星关注helux,它虽然较新,但已在我自己的使用场景中发挥功不可没的作用,现已加入hel-micro生态大仓,期待能成为你愿意挑选的一款可心数据流方案。

快速上手

极致的简单是helux最大的优势,了解以下6个api后,你可以轻松应付任何复杂场景,最大的魅力在于useSharedObjectuseService两个接口,且看如下api介绍,或访问在线示例1示例2fork并修改来体验。

useObject

使用 useObject 有两个好处

  • 1 方便定义多个状态值时,少写很多 useState
  • 2 内部做了 unmount 判断,让异步函数也可以安全的调用 setState,避免 react 出现警告 : “Called SetState() on an Unmounted Component” Errors
// 基于对象初始化一个视图状态
const [state, setState] = useObject({a:1});
// 基于函数初始化一个视图状态
const [state, setState] = useObject(()=>({a:1}));

useForceUpdate

强制更新当前组件视图,某些特殊的场景可以使用它来做视图重刷新

const forUpdate = useForceUpdate();

createSharedObject

创建一个共享对象,可透传给 useSharedObject,具体使用见 useSharedObject

// 初始化一个共享对象
const sharedObj = createSharedObject({a:1, b:2});
// 基于函数初始化一个共享对象
const sharedObj = createSharedObject(()=>({a:1, b:2}));

createReactiveSharedObject

创建一个响应式的共享对象,可透传给 useSharedObject

// 初始化一个共享对象
const [reactiveObj, setState] = createReactiveSharedObject({a:1, b:2});

sharedObj.a = 111; // 任意地方修改 a 属性,触发视图渲染
setSharedObj({a: 111}); // 使用此方法修改 a 属性,同样也能触发视图渲染,深层次的数据修改可使用此方法

createShared

函数签名

function createShared<T extends Dict = Dict>(
  rawState: T | (() => T),
  enableReactive?: boolean,
): {
  state: SharedObject<T>;
  call: <A extends any[] = any[]>(
    srvFn: (ctx: { args: A; state: T; setState: (partialState: Partial<T>) => void }) => Promise<Partial<T>> | Partial<T> | void,
    ...args: A
  ) => void;
  setState: (partialState: Partial<T>) => void;
};

创建一个响应式的共享对象,可透传给 useSharedObject,它是createReactiveSharedObjectcreateSharedObject的结合体,当需要调用脱离函数上下文的服务函数时(即不需要感知组件props时),可使用该接口,第二位参数为是否创建响应式状态,为 true 时效果同 createReactiveSharedObject 返回的 sharedObj

 const ret = createShared({ a: 100, b: 2 });
 const ret2 = createShared({ a: 100, b: 2 }, true); // 创建响应式状态
 // ret.state 可透传给 useSharedObject
 // ret.setState 可以直接修改状态
 // ret.call 可以调用服务函数,并透传上下文

以下将举例两种具体的定义服务函数的方式,之后用户便可在其他其他地方任意调用这些服务函数修改共享状态了,如需感知组件上下文,则需要用到下面介绍的useService接口去定义服务函数。

// 调用服务函数第一种方式,直接调用定义的函数,配合 ret.setState 修改状态
function changeAv2(a: number, b: number) {
   ret.setState({ a, b });
}
*
// 第二种方式,使用 ret.call(srvFn, ...args) 调用定义在call函数参数第一位的服务函数
function changeA(a: number, b: number) {
   ret.call(async function (ctx) { // ctx 即是透传的调用上下文,
     // args:使用 call 调用函数时透传的参数列表,state:状态,setState:更新状态句柄
     // 此处可全部感知到具体的类型
     // const { args, state, setState } = ctx;
     return { a, b };
   }, a, b);
 }

useSharedObject

函数签名

function useSharedObject<T extends Dict = Dict>(sharedObject: T, enableReactive?: boolean): [
  SharedObject<T>,
  (partialState: Partial<T>) => void,
]

接收一个共享对象,多个视图里将共享此对象,内部有依赖收集机制,不依赖到的数据变更将不会影响当前组件更新

const [ obj, setObj ] = useSharedObject(sharedObj);

useSharedObject默认返回非响应式状态,如需要使用响应式状态,透传第二位参数为true即可

const [ obj, setObj ] = useSharedObject(sharedObj);
// now obj is reactive
 setInterval(()=>{
  state.a = Date.now(); // 触发视图更新
 }, 2000);

useService

函数签名

/**
 * 使用用服务模式开发 react 组件:
 * @param compCtx
 * @param serviceImpl
 */
function useService<P extends Dict = Dict, S extends Dict = Dict, T extends Dict = Dict>(
  compCtx: {
    props: P;
    state: S;
    setState: (partialState: Partial<S>) => void;
  },
  serviceImpl: T,
): T & {
  ctx: {
    setState: (partialState: Partial<S>) => void;
    getState: () => S;
    getProps: () => P;
  };
}

它可搭配useObjectuseSharedObject一起使用,会创建服务对象并返回,该服务对象是一个稳定的引用,且它包含的所有方法也是稳定的引用,可安全方法交给其它组件且不会破会组件的pros比较规则,避免烦恼的useMemouseCallback遗漏相关依赖

搭配useObject

function DemoUseService(props: any) {
  const [state, setState] = useObject({ a: 100, b: 2 );
  // srv本身和它包含的方法是一个稳定的引用,
  // 可安全的将 srv.change 方法交给其它组件且不会破会组件的pros比较规则
  const srv = useService({ props, state, setState }, {
    change(a: number) {
      srv.ctx.setState({ a });
    },
  });
  
  return <div>
    DemoUseService:
    <button onClick={() => srv.change(Date.now())}>change a</button>
  </div>;
}

搭配useSharedObject时,只需替换useObject即可,其他代码不用做任何改变

+ const sharedObj = createSharedObject({a:100, b:2})

function DemoUseService(props: any) {
-  const [state, setState] = useObject({ a: 100, b: 2 );
+  const [state, setState] = useSharedObject(sharedObj);

getState 和 getProps

stateprops 是不稳定的,所以服务内部函数取的时候需从srv.ctx.getStatesrv.ctx.getProps

// 抽象服务函数
export function useChildService(compCtx: {
  props: IProps;
  state: S;
  setState: (partialState: Partial<S>) => void;
}) {
  const srv = useService<IProps, S>(compCtx, {
    change(label: string) {
      // !!! do not use compCtx.state or compCtx.state due to closure trap
      // console.log("expired state:", compCtx.state.label);

      // get latest state
      const state = srv.ctx.getState();
      console.log("the latest label in state:", state.label);
      // get latest props
      const props = srv.ctx.getProps();
      console.log("the latest props when calling change", props);

      // your logic
      compCtx.setState({ label });
    }
  });
  return srv;
}

export function ChildComp(props: IProps) {
  const [state, setState] = useObject(initFn);
  const srv = useChildService({ props, state, setState });
}

 return (
    <div>
      i am child <br />
      <button onClick={() => srv.change(`self:${Date.now()}`)}>
        change by myself
      </button>
      <h1>{state.label}</h1>;
    </div>
  );

exposeService

当孩子组件props上透传了exposeService函数时,useService 将自动透传服务对象给父亲组件,是一种比较方便的逃离forwardRef完成父调子的模式

import { ChildSrv, Child } from "./Child";

function App() {
  // 保存孩子的服务
  const childSrv = React.useRef<{ srv?: ChildSrv }>({});
  const seeState = () => {
    console.log("seeState", childSrv.current.srv?.ctx.getState());
  };

  return (
    <div>
      <button onClick={() => childSrv.current.srv?.change(`${Date.now()}`)}>
        call child logic
      </button>
      <Child
        unstableProp={`${Date.now()}`}
        exposeService={(srv) => (childSrv.current.srv = srv)}
      />
    </div>
  );
}

结语

helux是把concent内部精华全部萃取提炼再加工后的全新作品,期望能得到你的喜欢与鼓励。❤️

回到顶部