Introducing Helux, A React state library that encourages service injection and supports reactive updates
Published: 2023-04-17 09:15:56

## about Helux
[Helux](https://github.com/tnfe/hel/tree/main/packages/helux) is a brand new data flow solution that encourages service injection and supports responsive change React. Its predecessor was [concent](https ://github.com/concentjs/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kmntskxvnfskuzs962gp.gif)

> All the following APIs correspond to online [Example 1](https://codesandbox.io/s/demo-show-service-dev-mode-ikybly?file=/src/App.tsx) and [Example 2]( https://codesandbox.io/p/sandbox/use-service-to-replace-ref-e5mgr4?file=%2Fsrc%2FApp.tsx), 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](https://github.com/tnfe/hel) 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](https://github.com/tnfe/hel/tree/main/packages/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
 
```ts
// 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}));
```

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

```ts
const forUpdate = useForceUpdate();
```

### createSharedObject
Create a shared object that can be transparently passed to `useSharedObject`, see useSharedObject for specific usage
```ts
// 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}));
```

### createReactiveSharedObject
Create a responsive shared object that can be transparently passed to useSharedObject
```ts
// 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
```


### createShared
function signature
```ts
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;
};
```

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`

```ts
  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
```

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 below `Interface to define service functions.

```ts
// 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);
  }
```

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

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
```ts
const [ obj, setObj ] = useSharedObject(sharedObj);
```

`useSharedObject` returns a non-responsive state by default. If you need to use a reactive state, just pass through the second parameter to true
```ts
const [ obj, setObj ] = useSharedObject(sharedObj);
// now obj is reactive
  setInterval(()=>{
   state.a = Date.now(); // trigger view update
  }, 2000);
```

### useService
function signature
```ts
/**
  * 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;
   };
}
```


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`

```ts
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>;
}
```

When using `useSharedObject`, you only need to replace `useObject`, and other codes do not need to be changed
``` diff
+ 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 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`

```ts
// 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>
   );
```

#### 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

```ts
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>
   );
}
```

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