DEV Community

Cover image for State machine advent: One event, two possible state transitions (15/24)
Mikey Stengel
Mikey Stengel

Posted on • Updated on

State machine advent: One event, two possible state transitions (15/24)

Conditional logic is everywhere. While state machines reduce conditional logic by eliminating impossible states, there is some conditional logic we want to have within our machines. In particular, when one or the other action should be executed or multiple state transitions exist. We can define such conditional logic using the very concept we've learned yesterday, guards.

By providing an array of possible state transitions, the state transition with the first guard that evaluates to true will determine the next state of our machine. Let's say we want our thermostat to distinctively express whether it is cold or warm. If the temperature is below 18°C, it should go into the cold state and above, transition to the warm state.

import { Machine, assign } = 'xstate';

const thermostatMachine = Machine({
  id: 'thermostat',
  initial: 'inactive',
  context: {
    temperature: 20,
  },
  states: {
    inactive: {
      on: {
        POWER_TOGGLE: 'active'
      }
    },
    active: {
      initial: 'warm',
      states: {
        cold: {},
        warm: {},
      },
      on: {
        POWER_TOGGLE: {
          target: 'inactive',
        },
        SET_TEMPERATURE: [
            {
              target: '.cold',
              cond: (context, event) => event.temperature < 18,
              actions: assign({
                temperature: (context, event) => event.temperature,
              }),
            },
            {
              // transition without a guard as a fallback.
              target: '.warm',
              actions: assign({
                temperature: (context, event) => event.temperature,
              }),
            },
         ]
      }
    },
  }
});

Think of the state transition array as a switch case to determine the next state of a machine. The default transition can be expressed as a state transition without a guard as seen in the example above.

Notice how we had to duplicate the action to assign the temperature. Similar to when machines transition from one state to another, actions are only executed if no guard is defined, or when a guard evaluates to true.

To give one more example of this behavior, the code below will never call the 'log' action.

[
    {
        target: 'cold',
        cond: () => false,
        actions: 'log',
    },
    {
        target: 'warm',
    },
]

Tomorrow we'll refactor the thermostatMachine so that we don't have to define the same action twice.

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 spend on the fundamentals before we'll progress to more advanced concepts.

Discussion (10)

Collapse
jgarplind profile image
Joel

Hi Mikey, really enjoying this series as a thorough introduction to XState. Not having the ability to run some experiments myself right now, I'm curious why you set target: '. warm' rather than just warm. Does it carry a meaning?

Collapse
codingdive profile image
Mikey Stengel Author

Hey Joel, glad you are enjoying the series. 😊
I mostly use target as I like being explicit and it makes adding a guard or action easier. You can totally omit it if you prefer the shorthand notation.

Collapse
jgarplind profile image
Joel

Right, I'm referring in particular to the prefix dot though, didn't see that syntax explained anywhere. Is .warm different to warm in any meaningful way?

Thread Thread
codingdive profile image
Mikey Stengel Author • Edited on

Ah right. The dot is added (and needed) for the transition to be recognized as an internal transition. We don't want the machine to leave the active state. Instead, we are specifying a relative target (e.g .warm) to transition to the active.cold or active.warm state.

Without the relative target, the machine could think that we are transitioning to a warm state by assuming a state structure like the following and would then fail since the state node does not exist.

interface ThermostatStateSchema {
  states: {
    active: {};
    inactive: {};
    // this one doesn't actually exist since it's a child of active 
    warm: {};
  }
}
Thread Thread
jgarplind profile image
Joel

Makes sense! Wasn't trivial to figure out without XState experience though :)

Thread Thread
codingdive profile image
Mikey Stengel Author • Edited on

I'm glad you asked! I couldn't figure out a good way to include it in the post as I found the transition pages of docs the most difficult to grasp and a bit discouraging for beginners.
On day 15 I was also still naive enough to believe I can get away with explaining one concept per day. 😁 Towards the end, I had to ramp up and explain 2-3 things per post to write about most XState features I wanted to cover.

Thread Thread
jgarplind profile image
Joel

Either way, really happy to have come across your calendar, will make a good basis for me to dive into XState myself!

Thread Thread
codingdive profile image
Mikey Stengel Author

Thank you.
It's a high reward decision for sure!

Let me know if you are struggling with anything and feel free to send me machines for feedback.

Collapse
netaisllc profile image
CSSian

Hi there, very helpful series, so many thanks!

A small thing: you mention the use of the default keyword in example above but i couldn't locate it. (.warm is referred to as a fallback but it doesn't appear to have an explicit use of the keyword.). Maybe DavidKPiano will provide a statemachine linter next. LOL!

Collapse
codingdive profile image
Mikey Stengel Author

Thank you a lot. Glad you found it useful.

I meant to say the "default transition" not "default keyword". Fixed it in the post.