DEV Community

幻魂
幻魂

Posted on

Introducing Helux, A React state library that encourages service injection and supports reactive updates

about Helux

Helux is a brand new data flow solution that encourages service injection and supports responsive change React. Its predecessor was concent (a high-performance state management framework similar to Vue development experience), but Concent itself needs to be compatible with class and function syntax to maintain consistency, and in order to set up its function, the internal code volume is too large, with over 70 kb compressed and the API exposed, resulting in a sharp increase in learning difficulty, In order to better conform to the coding trend of the current popular DDD to build a domain model around business concepts, helux was initially designed as a lightweight react data flow solution that encourages service injection, supports responsive changes, and supports dependency collection.

It has the following advantages:

  • Lightweight, 2kb compressed
  • Simple, only 7 APIs are exposed, and only 4 interfaces createShared, useObject, useSharedObject, useService are used frequently
  • High performance, built-in dependency collection
  • Responsive, supports the creation of responsive objects, changing objects outside the view will update the view synchronously
  • Service injection, with the useService interface to easily control complex business logic, always return a stable reference, can completely avoid the useCallback dependent annoyance
  • The state has been changed to 0, so you only need to replace useObject with useSharedObject to share the state to other components
  • Avoid forwordRef hell, the built-in exposeService mode will easily solve the problem of obscurity and contagion of ref forwarding when the parent drops the child (generational components need to be forwarded layer by layer)
  • ts-friendly, 100% written in ts, providing you with all-round type hints

Image description

All the following APIs correspond to online Example 1 and Example 2, welcome to fork and modify the experience.

Why is it named helux, although I developed it as the concent v3 version in my heart, but because it has changed too much, it does not inherit any concent features except for dependent collection, and it is also developed with me hel-micro has produced a work, I expect it to be a luxury-level contribution to the hel-micro ecology, so I put together the words hel-micro and luxury Became helux.

Welcome to pay attention to helux, although it is relatively new, it has played an indispensable role in my own usage scenarios, It has joined the hel-micro ecological warehouse, and looks forward to becoming a satisfactory data flow solution that you are willing to choose.

Quick start

The ultimate simplicity is the biggest advantage of helux. After understanding the following 6 APIs, you can easily handle any complex scene. The biggest charm lies in the two interfaces of useSharedObject and useService, and see the following API introduction

useObject

There are two benefits to using useObject

  • 1 When defining multiple state values, it is convenient to write a lot less useState
  • 2 An unmount judgment is made internally, so that asynchronous functions can also safely call setState to avoid warnings in react: "Called SetState() on an Unmounted Component" Errors
// Initialize a view state based on the object
const [state, setState] = useObject({a:1});
// Initialize a view state based on the function
const [state, setState] = useObject(()=>({a:1}));
Enter fullscreen mode Exit fullscreen mode

useForceUpdate

Forcibly update the current component view, which can be used to refresh the view in some special scenarios

const forUpdate = useForceUpdate();
Enter fullscreen mode Exit fullscreen mode

createSharedObject

Create a shared object that can be transparently passed to useSharedObject, see useSharedObject for specific usage

// Initialize a shared object
const sharedObj = createSharedObject({a:1, b:2});
// Initialize a shared object based on the function
const sharedObj = createSharedObject(()=>({a:1, b:2}));
Enter fullscreen mode Exit fullscreen mode

createReactiveSharedObject

Create a responsive shared object that can be transparently passed to useSharedObject

// Initialize a shared object
const [reactiveObj, setState] = createReactiveSharedObject({a:1, b:2});

sharedObj.a = 111; // Modify the a property anywhere to trigger view rendering
setSharedObj({a: 111}); // Use this method to modify the a property, which can also trigger view rendering. Deep data modification can use this method
Enter fullscreen mode Exit fullscreen mode

createShared

function signature

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;
};
Enter fullscreen mode Exit fullscreen mode

Create a responsive shared object that can be transparently passed to useSharedObject. It is a combination of createReactiveSharedObject and createSharedObject. When you need to call a service function that is out of the function context (that is, when you do not need to perceive component props), you can use this Interface, the second parameter is whether to create a reactive state, when it is true, the effect is the same as the sharedObj returned by createReactiveSharedObject

  const ret = createShared({ a: 100, b: 2 });
  const ret2 = createShared({ a: 100, b: 2 }, true); // create reactive state
  // ret.state can be transparently passed to useSharedObject
  // ret.setState can directly modify the state
  // ret.call can call the service function and transparently transmit the context
Enter fullscreen mode Exit fullscreen mode

The following will give examples of two specific ways to define service functions, and then users can call these service functions in other places to modify the shared state. If you need to perceive the component context (such as props), you need to use the useService described belowInterface to define service functions.

// The first way to call the service function is to directly call the defined function and modify the state with ret.setState
function changeAv2(a: number, b: number) {
    ret. setState({ a, b });
}
*
// In the second way, use ret.call(srvFn, ...args) to call the service function defined in the first parameter of the call function
function changeA(a: number, b: number) {
    ret.call(async function (ctx) { // ctx is the transparent call context,
      // args: the transparent parameter list when using call to call the function, state: state, setState: update state handle
      // Here you can all perceive the specific type
      // const { args, state, setState } = ctx;
      return { a, b };
    }, a, b);
  }
Enter fullscreen mode Exit fullscreen mode

useSharedObject

function signature

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

Receive a shared object, which will be shared in multiple views. There is a dependency collection mechanism inside, and data changes that do not depend on it will not affect the current component update

const [ obj, setObj ] = useSharedObject(sharedObj);
Enter fullscreen mode Exit fullscreen mode

useSharedObject returns a non-responsive state by default. If you need to use a reactive state, just pass through the second parameter to true

const [ obj, setObj ] = useSharedObject(sharedObj);
// now obj is reactive
  setInterval(()=>{
   state.a = Date.now(); // trigger view update
  }, 2000);
Enter fullscreen mode Exit fullscreen mode

useService

function signature

/**
  * Develop react components using the service model:
  * @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;
   };
}
Enter fullscreen mode Exit fullscreen mode

It can be used together with useObject and useSharedObject. It will create a service object and return it. The service object is a stable reference, and all the methods it contains are also stable references. It can be safely passed to other components and will not Break the pros comparison rules of components to avoid the annoying useMemo and useCallback missing related dependencies

When paired with useObject

function DemoUseService(props: any) {
   const [state, setState] = useObject({ a: 100, b: 2 );
   // srv itself and the methods it contains are a stable reference,
   // It is safe to hand over the srv.change method to other components without breaking the pros comparison rules of the components
   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>;
}
Enter fullscreen mode Exit fullscreen mode

When using useSharedObject, you only need to replace useObject, and other codes do not need to be changed

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

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

getState and getProps

Because state and props are unstable, so the service internal function needs to be retrieved from srv.ctx.getState or srv.ctx.getProps

// abstract service function
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>
   );
Enter fullscreen mode Exit fullscreen mode

exposeService

When the exposeService function is transparently passed on the child component props, useService will automatically transparently pass the service object to the parent component, which is a more convenient mode to escape from forwardRef to complete the parent tune

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

function App() {
   // save the child's service
   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>
   );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

helux is a brand new work after extracting and reprocessing all the inner essence of concent, I hope you will like it. ❤️

Top comments (0)