Introducing Helux, A React state library that encourages service injection and supports reactive updates
幻魂
Posted on April 17, 2023
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 theuseCallback
dependent annoyance - The state has been changed to 0, so you only need to replace
useObject
withuseSharedObject
to share the state to other components - Avoid forwordRef hell, the built-in
exposeService
mode will easily solve the problem of obscurity and contagion ofref
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
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}));
useForceUpdate
Forcibly update the current component view, which can be used to refresh the view in some special scenarios
const forUpdate = useForceUpdate();
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}));
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
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;
};
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
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.
// 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
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
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
const [ obj, setObj ] = useSharedObject(sharedObj);
// now obj is reactive
setInterval(()=>{
state.a = Date.now(); // trigger view update
}, 2000);
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;
};
}
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>;
}
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);
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>
);
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>
);
}
Conclusion
helux
is a brand new work after extracting and reprocessing all the inner essence of concent
, I hope you will like it. ❤️
Posted on April 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.