Communicating with spawned and invoked xstate actors in React

Ismayil Khayredinov
ITNEXT
Published in
5 min readFeb 2, 2020

--

I have recently started working with finite state machines, while rethinking our checkout funnel implementation. I must admit I am really fascinated with how well state machines are suited for front-end engineering. It takes a bit of getting used to the paradigm, and you might struggle to understand all the caveats at first, but the more you work with these machines, the easier it gets to reason about your otherwise complex application.

If you worked with Redux, you know how cumbersome it gets early in the process, trying to bootstrap the store, writing all the actions and reducers, and mapping the state to props inside your components. If you have a multi-step form wizard, you end up with quite a lot of conditional logic, trying to determine which components are to be shown at which stage of your user journey. You start creating a number of fancy validators to ensure that the current state allows the user to go on to the next screen.

If you think of your application from a user interface perspective, you soon realize that it’s nothing more than a finite state machine. You have a set of screens and transitions between them — a classic statechart!

Each screen can be defined as a state that a finite state machine can be in. At a top level your application is a machine that defines the rules of navigation between these screens. Because your application can only be in only one single state at a time, i.e. you can only display a single screen at a time, each screen is a machine of its own with entry and exit points: your top level machine doesn’t really care about the internal state of each screen and you can only transition out of a specific screen when the child machine informs the parent that it reached its final state. For example, you may have a base checkout funnel in the form: Product > Shipping > Payment > Done. Each of these steps is independent from the other, they don’t exist in parallel, but in sequence, so a machine inside the Shipping screen doesn’t care about the state inside the Payment screen state machine. Top level machine doesn’t concern itself with the state inside of the Payment machine, all it cares about is knowing that the Payment screen has been entered, and it waits for it to send a signal back that it’s done (i.e. it reached one of its final states, e.g. payment completed successfully).

xstate is a great statechart implementation in JavaScript and it offers an adapter for React. While there are plenty of examples of creating state machines in React, I couldn’t find much about dealing with nested machines and spawned actors, so it was more of a trial and error. Let me demonstrate my findings with two examples: using an invoked child machine to represent a state of a parent machine, and using a spawned actor to deal with generic concerns, such as alerts.

Invoking child machines

Before going into the details of our implementation, let’s spec out our statecharts to help us visualize the process:

When our checkout machine enters the product state, we want to invoke a product machine as a child service and wait for it to reach it’s final state at which point we send the data back to the parent. Note the use of data property in productMachine.states.final and the invocation of the product machine inside checkoutMachine.states.product. When invoking a child machine, we can assign an id, which can be used to access the child machine inside React components, as well as pass the context data to the child machine (in this example productCategory is sent to the child machine).

We can now create our React components.

We create a memoized root level machine using useMachine hook. The key to parent-child communication is in ProductPicker component: we can access our child machine instance (i.e. invoked child service) by its id. The trick to making your child component reactive is to run this child service through useService hook, which will provide you the state and transition callback of your child service. Once the final state of the product machine is reached, an array of selected products is sent to the parent machine and stored in its context. You can later pass those products to the payment screen to determine the final price.

Spawning child actors

An actor, simply put, is just a service that runs inside your machine — it can be an invoked machine, or promise, or callback. You can spawn new actors at any stage and access them in the same way I demonstrated above. Let’s now create a notification service that we can use to display errors/alerts to the user.

Let’s presume something goes wrong when we fetch the products. We want to be able to capture an error in our product machine, notify the parent machine, and display an alert through our notifications machine.

In the root entry of our checkout machine, we can spawn a new actor, i.e. instantiate a notifications machine as a service. We can then communicate with the notifications machine via send() by specifying the recipient {to: 'childServiceId'} . The product machine can talk to its parent checkout machine by using sendParent(). One thing to note here is that both send and sendParent are not actual function calls, but rather action generators, so do not try to use them inside another action callback, as it will not do anything. You can use something like {actions: [ callback1, send(), callback2 ]}.

When our checkout machine is in a product state, it will listen to NOTIFY transition (so product machine can use sendParent({type: 'NOTIFY', ...payload})) and send an ALERT transition to the notificationsMachine.

We can now hook up our notifications machine to our alert component.

I hope this provides some insights into using parent-child communication patterns in @xstate/react. I had to discover most of it through trial and error, and I will be pleased if it saves you some time in your work.

--

--

Full-stack developer, passionate about front-end frameworks, design systems and UX.