Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Sustainable xState machines
Georgi Todorov
Georgi Todorov

Posted on • Edited on

Sustainable xState machines

TLDR

If you are curious about the full-working example, it ishere.

Background

I've been using xState with React for some time already. Throughout the projects' development, I often find myself reusing the same machine in different contexts. Sometimes makes sense tointerpret the machine from the React component, other times it is the logical choice tospawn and store it as a childactor.
That's why I came up with a simple abstraction that let's me use the machine in both ways.

Use case example

Let's say we need a toggle machine for our checkbox component. We create a simple machine withon andoff states and then use it directly in our checkbox component via theuseMachine hook.
But in another page we have a more complex machine, which can make good use of the toggle machine for displaying a notification based on business logic. This sounds as a good opportunity tospawn a toggle actor, which will be controlled by the complex machine.
Now we need the same machine, but utilised in two different ways.

Checkbox component

Firstly, we need to create our toggle machine.

exportconsttoggleMachine=createMachine({id:"toggleMachine",schema:{events:{}as{type:"TOGGLE"},},initial:"off",states:{off:{on:{TOGGLE:{target:"on",},},},on:{on:{TOGGLE:{target:"off",},},},},});
Enter fullscreen modeExit fullscreen mode

It is a simplestatechart withon andoff states. This will be sufficient for our checkbox component. We just have to pass it to theuseMachine hook.

interfaceProps{onChange(checked:boolean):void;}exportfunctionCheckbox({onChange}:Props){const[state,send]=useMachine(toggleMachine);return(<><labelhtmlFor="toggle">Toggle</label><inputid="toggle"type="checkbox"checked={state.matches("on")}onChange={(event)=>{send({type:"TOGGLE"});onChange(event.target.checked);}}/></>);}
Enter fullscreen modeExit fullscreen mode

We use thematches method to confirm the machine state and control thechecked attribute. WhenonChange is triggered, the machine state is toggled and the callback from the props is fired.

The only problem that we are facing now is that we cannot control the initial state of the checkbox component. It will always be initialised asunchecked until further interaction occurs.

We can easily solve the issue by lazy-loading the machine. By returning the machine from a factory function, we can pass the initial state as a variable.

The final step, is to lazily-create the machine with theuseMachine hook. This will prevent the warnings for a new machine instance being passed to the hook on each re-render.

exportfunctiontoggleMachine({initial}:{initial:string}){returncreateMachine({id:"toggleMachine",initial,states:{/* ... */},});}/* ... */const[state,send]=useMachine(()=>toggleMachine({initial}));
Enter fullscreen modeExit fullscreen mode

Data fetching notification

Supposing we need to show specific data to the user when downloaded. To make it sure that the user won't miss the information, we put it in a notification component.

We can start by creating the fetching machine (parent machine) that takes care of downloading the user info.

constfetchingMachine=createMachine({id:"fetchMachine",initial:"fetching",on:{FETCHING:{target:"fetching",},},states:{idle:{},fetching:{invoke:{src:"fetchData",onDone:{target:"idle",},},},},},{services:{asyncfetchData(){constresponse=awaitfetch("https://jsonplaceholder.typicode.com/todos/");constdata=awaitresponse.json();returndata;},},});
Enter fullscreen modeExit fullscreen mode

Similarly to theCheckbox component, theNotificaion one needson andoff states, in order to control its visibility. The difference now is that instead of interpreting the machine directly in the component, we canspawn anactor and keep it in the fetchingmachine's context.

In my opinion, this approach gives us more flexibility when it comes to controlling the toggle machine. We canspawn the machine explicitly when needed, instead of relying on the React render cycle.

constfetchingMachine=createMachine({/* ... */states:{idle:{},fetching:{invoke:{src:"fetchData",onDone:{actions:["assignToggleRef"],target:"idle",},},},},},{actions:{assignToggleRef:assign({toggleRef:(context,event)=>{returnspawn(toggleMachine({initial:"on"}));},}),},services:{asyncfetchData(){/* ... */},},});
Enter fullscreen modeExit fullscreen mode

We can simply pass thetoggleRef to theNotification component and interpret it from there with the guarantee that the fetching of the data is completely finished.

interfaceProps{actor:ActorRefFrom<typeoftoggleMachine>;}exportfunctionNotification({actor,data}:Props){const[state,send]=useActor(actor);return(<div>{state.matches("on")&&<div>Veryimportantnotification</div>}<buttononClick={()=>{send({type:"TOGGLE"});}}>{state.matches("on")?"close":"open"}</button></div>);}
Enter fullscreen modeExit fullscreen mode

Now we have access to the actor'sstate andcontext. Also, since the actor reference is kept in the parent's machine context, we cansend events to the interpreted actor regardless of the parent's machine state.

Unite both worlds

Using directly the exported machine is perfectly fine, but I fancy adding one more layer of abstraction.

import{ActorRefFrom,createMachine,spawn}from"xstate";exporttypeToggleMachineActor=ActorRefFrom<typeoftoggleMachine>;functiontoggleMachine({initial}:{initial:string}){returncreateMachine({id:"toggleMachine",/* ... */});}exportfunctiontoggleMachineCreate({initial="off"}):{machine:ReturnType<typeoftoggleMachine>;spawn:()=>ToggleMachineActor;}{constmachine=toggleMachine({initial});return{machine,spawn:()=>spawn(machine,{name:"toggleMachine"}),};}
Enter fullscreen modeExit fullscreen mode

Now, ourtoggleMachineCreate returns both the machine instance and the spawned actor, which I find a bit cleaner when it comes to using the machine.

toggleRef:(context,event)=>{returntoggleMachineCreate({initial:"on"}).spawn();}/* or */const[state,send]=useMachine(()=>toggleMachineCreate({initial}).machine);
Enter fullscreen modeExit fullscreen mode

Another advantage of this approach is that we have a convenient place where we can extend the machines with thewithContext andwithConfig utility methods. This gives our code а pinch of readability.

exportfunctiontoggleMachineCreate({initial="off",specialService=Promise<any>}):{machine:ReturnType<typeoftoggleMachine>;spawn:()=>ToggleMachineActor;}{constmachine=toggleMachine({initial}).withConfig({services:{specialService},});return{machine,spawn:()=>spawn(machine,{name:"toggleMachine"}),};}
Enter fullscreen modeExit fullscreen mode

On top of that, you can easily pass arguments to thespawn function too. That might be helpful when multiple actors from a machine are spawned and stored into a single context. Distinguishing the references by name, might be helpful when it comes to communication with them.

exportfunctiontoggleMachineCreate():{/* ... */return{machine,spawn:(name:string)=>spawn(machine,{name}),};}/* ... */actions:{assignToggleRef1:assign({toggleRef1:(context,event)=>{returntoggleMachineCreate().spawn("toggleMachine1");},}),assignToggleRef2:assign({toggleRef2:(context,event)=>{returntoggleMachineCreate().spawn("toggleMachine2");},}),},/* ... */
Enter fullscreen modeExit fullscreen mode

And last but not least, I find it really handy to have the actor type exported along with thetoggleMachineCreate function. It is quite useful when it comes to prop drilling the actor and defining the prop types.

exporttypeToggleMachineActor=ActorRefFrom<typeoftoggleMachine>;/* ... */interfaceProps{actor:ToggleMachineActor;}
Enter fullscreen modeExit fullscreen mode

Opinionated conclusion

This pattern scales greatly and gives a good amount of predictability in regards to code organisation and readability.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software developer.
  • Location
    Sofia, Bulgaria
  • Joined

More fromGeorgi Todorov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp