Timed Finite State Machines with React and XState


For a while now I keep noticing tweets showing up on my timeline about state machines in React. Especially I keep stumbling upon the XState library. XState is a library for creating state machines and interpreting them. I’ve never used XState or other similar libraries before but a long time ago during my studies I've had to deal a bit with finite state machines. In particular I remember I enjoyed playing around with Timed Automatons. A timed automaton is a finite-state machine extended with clock variables. To demonstrate what a timed automaton is I will use XState with React.

We are going to build the following machine depicting a pretty simple lamp.

Lamp Demo

The lamp is composed of three states: Off, Low and Bright. We move through the states by clicking on the button. If we click on the button then the lamp turns on. If we click on the button again then the lamp will turn off. However, if we are fast and we rapidly click the button twice, the lamp turns on and becomes bright. The clock of the lamp is used to detect if we were fast t < 3 or slow t >= 3.

The clock t is used as a guard during the transitions. The clock starts ticking (is reset) when we move from the Off state to the Low state. While we are in the Low state the clock continues ticking and depending how long we stay in that state determines our next transition. In this case if we act in less than 3 seconds t < 3 we would transition to the Bright state, and from the Bright state on the next transition we end up in the Off state. Otherwise if we stay longer than t >= 3 in the Low state on the next transition from there we end up in the Off state.

Implementation

Now that we have the model, let's see how the LampMachine.ts looks like with React and XState.

We create a simple machine with an id: "lamp" that has only three states represented by the LampState enum, and we set the initial state to LampState.Off

import { assign, createMachine, send } from "xstate"; enum LampState {
 Off = "Off",
 Low = "Low",
 Bright = "Bright",
}
enum Event {
 Tick = "Tick",
 Reset = "Reset",
}
interface LampContext {
 elapsed: number;
 clockGuard: number;
 interval: number;
} export const createLampMachine = ({ elapsed, interval, clockGuard }: LampContext) =>
 createMachine<LampContext>({
 id: "lamp",
 initial: LampState.Off,
 context: {
 elapsed,
 interval,
 clockGuard,
 },
 states: {
 [LampState.Off]: {
 ...
 },
 [LampState.Low]: {
 ...
 },
 [LampState.Bright]: {
 ...
 },
 },
 });

The LampState.Off and LampState.Bright transition implementation are pretty simple so we will look only into the LampState.Low node where the interesting stuff happens. Upon entering this node we invoke a service that starts a timer. Inside the interval we send an event Event.Tick that is used to update the elapsed counter in the context.

[LampState.Low]: {
 // The timer service
 invoke: {
 src: (context) => (cb) => {
 const interval = setInterval(() => {
 // Send the event
 cb(Event.Tick);
 }, 1000 * context.interval);
 return () => {
 clearInterval(interval);
 };
 },
 },  on: {
 // On every tick
 [Event.Tick]: {
 actions: assign({
 // Update the elapsed counter
 elapsed: (context) => context.elapsed + context.interval,
 }),
 },
 ...
 }
}

Additionally in the LampState.Low state we also have the click event implementation where we handle the guards. In the click event we specify an array with two conditions. In XState a condition function (also known as a guard) is specified on the .cond property of a transition. From the guard functions we return a boolean true or false, which determines whether the transition should be allowed to take place.

[LampState.Low]: {
 ...
 on: {
 ...
 click: [
 {
 // We transition to the `Bright` state if `t < 3`
 target: [LampState.Bright],
 cond: ({ elapsed, clockGuard }): boolean => {
 return elapsed < clockGuard;
 },
 },
 {
 // Otherwise transition to the `Off` state if `t >= 3`
 target: [LampState.Off],
 cond: ({ elapsed, clockGuard }): boolean => {
 return elapsed >= clockGuard;
 },
 },
 ],
 }
}

In the above code we can see that if we satisfy the condition t < 3 we transition to the Bright state. Otherwise if the t >= 3 condition is satisfied we transition to the Off state. And that is more or less how we handle clock variables in the lamp state machine.

Finally if we take a look at the React component, the usage is straight forward. I am using Jotai for this example just for fun, but it can totally work without it.

import { atom, Provider, useAtom } from "jotai";
import { atomWithMachine } from "jotai/xstate"; // We initialize the `lampMachineAtom` with some default values.
// We can specify the guard depending how long we want to stay in the `Low` state.
// In this example we are waiting 3 seconds
const defaultAtom = atom({ elapsed: 0, interval: 0.1, clockGuard: 3 });
const lampMachineAtom = atomWithMachine((get) => createLampMachine(get(defaultAtom))); const Lamp: React.FC = () => {
 const [state, send] = useAtom(lampMachineAtom);
 const { elapsed, clockGuard } = state.context;
 ...  const getLampState = () => {
 if (state.matches(LampState.Off)) {
 return LampState.Off;
 }
 if (state.matches(LampState.Low)) {
 return LampState.Low;
 }
 if (state.matches(LampState.Bright)) {
 return LampState.Bright;
 }
 return LampState.Off;
 }; return (
 <>
 ...
 {/* The Button we use to control the lamp */}
 <Switch onClick={send}>{getLampState()}</Switch>
 {/* Timer to see the clock running */}
 <h2 className={styles.counter}>t={elapsed.toFixed(1)}s</h2>
 {/* The Automaton to show the simulation step by step */}
 <Automaton light={getLampState()} clockGuard={clockGuard} elapsed={elapsed} />  <LightBulb light={getLampState()} />
 ...
 </>
 );
}

And pretty much that's it. You can find the entire source code on GitHub if you want to check it out.

Demo Codesandbox

References

Finite-state Machines

Timed Automaton

A Tutorial on Uppaal 4.0

I had a lot of fun playing around with XState this weekend, most likely I will use it in the future as well.

Are you using XState in your projects? Tweet me at @altrimbeqiri and let me know the cool stuff you build with it.

Happy Coding!