DEV Community

Mikey Stengel
Mikey Stengel

Posted on • Updated on

State machine advent: How to match against nested states in XState (10/24)

In the 5th post of the series, we saw how to invoke a state machine in React. Now that we have learned about statecharts to model nested and parallel states, we want to learn how to invoke it and determine its current state.

As seen in the codesandbox for today's lecture, the code to invoke the statechart does not differ from invoking a regular state machine in XState. This is one of many reasons why XState is awesome. By abstracting both state machines and statecharts into the same machine object that is being accepted by the useMachine hook from the @xstate/react package, XState provides a really clean API surface to define and invoke machines. As a result, it removes a lot of cognitive overhead whether we are dealing with a regular state machine (Day 2 - 8) or some sophisticated statechart (Day 9).
The API stays the same:

const [state, send] = useMachine(videoChatMachine);

One thing that does change however is the way we assert the current state of our statechart.

Just to recap, our state structure of the videoChatMachine looked like this:

interface VideoChatStateSchema {
  states: {
    audio: {
      states: {
        enabled: {};
        disabled: {};
      };
    };
    video: {
      states: {
        enabled: {};
        disabled: {};
      };
    };
  };
}

Due to the fact that video and audio are parallel to each other (you can't see this within the state schema as the type of the machine only needs to be specified in the implementation of the machine), they both must have an initial state and are stateful at all times.

As a consequence, state.matches("audio") will always return true. If we want to assert if the audio is enabled or disabled, we have to match against the nested state structure. Luckily, the state.matches API does not only accept a string value, it's an overloaded function (are you allowed to say that in JavaScript? 😁) that also accepts an object. The object should replicate our exact state structure up until the state node that we want to match against.

state.matches({audio: 'enabled'})

Should your state architecture grow deeper, you can apply the same concept - of creating an object that replicates the state structure - to match deeply nested states.

interface SampleStateSchema {
  states: {
    grandparentStateNode: {
      states: {
        parentStateNode: {
          states: {
            childStateNode: {};
            someOtherChildStateNode: {};
          };
        };
      };
    };
  };
}

// define & invoke machine ...

If you then want to assert that the invoked machine is inside the childStateNode, you can do so by passing a deeply nested object to the matches function.

state.matches({ grandparentStateNode: { parentStateNode: 'childStateNode' } });

About this series

Throughout the first 24 days of December, I'll publish a small blog post each day teaching you about the ins and outs of state machines and statecharts.

The first couple of days will be spent on the fundamentals before we'll progress to more advanced concepts.

Discussion (2)

Collapse
fredyc profile image
Daniel K. • Edited on

If I am not mistaken, dot notation is supported too for matches call right? I find the object too verbose.

state.matches('grandparentStateNode.parentStateNode.childStateNode')

feels somewhat easier to read. Obviously it's not possible to type check correctly, so that's a disadvantage for sure.

Collapse
codingdive profile image
Mikey Stengel Author • Edited on

Thank you for pointing out. I just recently learned about the dot notation and find it cleaner too.

I believe state.matches does not yet have strict type support. For the time being, it's just a matter of preference which syntax to use.