- Notifications
You must be signed in to change notification settings - Fork5
State Lifetime Manager
disjukr/bunja
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Bunja is lightweight State Lifetime Manager.
Heavily inspired byBunshi.
Definition: Bunja (分子 / 분자) - Korean for molecule, member or element.
Global state managers like jotai or signals offer the advantage of declarativelydescribing state and effectively reducing render counts, but they lack suitablemethods for managing resources with a defined start and end.
For example, consider establishing and closing a WebSocket connection or a modalform UI that appears temporarily and then disappears.
Bunja is a library designed to address these weaknesses.
Each state defined with Bunja has a lifetime that begins when it is firstdepended on somewhere in the render tree and ends when all dependenciesdisappear.
Therefore, when writing a state to manage a WebSocket, you only need to create afunction that establishes the WebSocket connection and a disposal handler thatterminates the connection.
The library automatically tracks the actual usage period and calls the init anddispose as needed.
No. Bunja focuses solely on managing the lifetime of state, so jotai and otherstate management libraries are still valuable.
You can typically use jotai or something, and when lifetime management becomesnecessary, you can wrap those states with bunja.
Bunja basically provides two functions:bunja anduseBunja.
You can usebunja to define a state with a finite lifetime and use theuseBunja hook to access that state.
You can define a bunja using thebunja function. When you access the definedbunja with theuseBunja hook, a bunja instance is created.
If all components in the render tree that refer to the bunja disappear, thebunja instance is automatically destroyed.
If you want to trigger effects when the lifetime of a bunja starts and ends, youcan use thebunja.effect function.
import{bunja}from"bunja";import{useBunja}from"bunja/react";constcountBunja=bunja(()=>{constcountAtom=atom(0);bunja.effect(()=>{console.log("mounted");return()=>console.log("unmounted");});return{ countAtom};});functionMyComponent(){const{ countAtom}=useBunja(countBunja);const[count,setCount]=useAtom(countAtom);// Your component logic here}
If you want to manage a state with a broad lifetime and another state with anarrower lifetime, you can create a (narrower) bunja that depends on a (broader)bunja. For example, you can think of a bunja that manages the WebSocketconnection and disconnection, and another bunja that subscribes to a specificresource over the connected WebSocket.
In an application composed of multiple pages, you might want to subscribe to theFoo resource on page A and the Bar resource on page B, while using the sameWebSocket connection regardless of which page you're on. In such a case, you canwrite the following code.
// To simplify the example, code for buffering and reconnection has been omitted.constwebsocketBunja=bunja(()=>{letsocket;constsend=(message)=>socket.send(JSON.stringify(message));constemitter=newEventEmitter();conston=(handler)=>{emitter.on("message",handler);return()=>emitter.off("message",handler);};bunja.effect(()=>{socket=newWebSocket("...");socket.onmessage=(e)=>emitter.emit("message",JSON.parse(e.data));return()=>socket.close();});return{ send, on};});constresourceFooBunja=bunja(()=>{const{ send, on}=bunja.use(websocketBunja);constresourceFooAtom=atom();bunja.effect(()=>{constoff=on((message)=>{if(message.type==="foo")store.set(resourceAtom,message.value);});send("subscribe-foo");return()=>{send("unsubscribe-foo");off();};});return{ resourceFooAtom};});constresourceBarBunja=bunja(()=>{const{ send, on}=bunja.use(websocketBunja);constresourceBarAtom=atom();// ...});functionPageA(){const{ resourceFooAtom}=useBunja(resourceFooBunja);constresourceFoo=useAtomValue(resourceFooAtom);// ...}functionPageB(){const{ resourceBarAtom}=useBunja(resourceBarBunja);constresourceBar=useAtomValue(resourceBarAtom);// ...}
Notice thatwebsocketBunja is not directlyuseBunja-ed. When youuseBunjaeitherresourceFooBunja orresourceBarBunja, since they depend onwebsocketBunja, it has the same effect as ifwebsocketBunja were alsouseBunja-ed.
Note
When a bunja starts, the initialization effect of the bunja with a broaderlifetime is called first.
Similarly, when a bunja ends, the cleanup effect of the bunja with the broaderlifetime is called first.
This behavior is aligned with how React'suseEffect cleanup function isinvoked, where the parent’s cleanup is executed before the child’s in therender tree.
You can use a bunja for local state management.
When you specify a scope as a dependency of the bunja, separate bunja instancesare created based on the values injected into the scope.
import{bunja,createScope}from"bunja";constUrlScope=createScope();constfetchBunja=bunja(()=>{consturl=bunja.use(UrlScope);constqueryAtom=atomWithQuery((get)=>({queryKey:[url],queryFn:async()=>(awaitfetch(url)).json(),}));return{ queryAtom};});
If you bind a scope to a React context, bunjas that depend on the scope canretrieve values from the corresponding React context.
In the example below, there are two React instances (<ChildComponent />) thatreference the samefetchBunja, but since each looks at a different contextvalue, two separate bunja instances are also created.
import{createContext}from"react";import{bunja,createScope}from"bunja";import{bindScope}from"bunja/react";constUrlContext=createContext("https://example.com/");constUrlScope=createScope();bindScope(UrlScope,UrlContext);constfetchBunja=bunja(()=>{consturl=bunja.use(UrlScope);constqueryAtom=atomWithQuery((get)=>({queryKey:[url],queryFn:async()=>(awaitfetch(url)).json(),}));return{ queryAtom};});functionParentComponent(){return(<><UrlContextvalue="https://example.com/foo"><ChildComponent/></UrlContext><UrlContextvalue="https://example.com/bar"><ChildComponent/></UrlContext></>);}functionChildComponent(){const{ queryAtom}=useBunja(fetchBunja);const{ data, isPending, isError}=useAtomValue(queryAtom);// Your component logic here}
You can use thecreateScopeFromContext function to handle both the creation ofthe scope and the binding to the context in one step.
import{createContext}from"react";import{createScopeFromContext}from"bunja/react";constUrlContext=createContext("https://example.com/");constUrlScope=createScopeFromContext(UrlContext);
You might want to use a bunja directly within a React component where the valuesto be injected into the scope are created.
In such cases, you can use the second parameter ofuseBunja hook to injectvalues into the scope without wrapping the context separately.
functionMyComponent(){const{ queryAtom}=useBunja(fetchBunja,[UrlScope.bind("https://example.com/")],);const{ data, isPending, isError}=useAtomValue(queryAtom);// Your component logic here}
You can usebunja.fork to inject scope values from within a bunjainitialization function.
constmyBunja=bunja(()=>{constfooData=bunja.fork(fetchBunja,[UrlScope.bind("https://example.com/foo"),]);constbarData=bunja.fork(fetchBunja,[UrlScope.bind("https://example.com/bar"),]);return{ fooData, barData};});
About
State Lifetime Manager
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
Contributors4
Uh oh!
There was an error while loading.Please reload this page.