DEV Community

Mikey Stengel
Mikey Stengel

Posted on • Edited on

State machine advent: Guard state transitions, guard actions (14/24)

One really powerful concept of statecharts that we haven't covered yet are guards. They are conditional functions that determine whether a state transition should be taken, or actions executed. They are fundamental building blocks when building applications using statecharts. Today, we will cover how to use them.

Guard actions

Let's say we want our thermostat to only work with temperatures below 100°C. We can do so by conditionally executing the action that changes our temperature in context. In XState, we can define this guard within events using the cond keyword. Its value is a function that will be invoked with the context, event, and must always return a boolean value.

import { Machine, assign } = 'xstate';

const thermostatMachine = Machine({
  id: 'thermostat',
  initial: 'inactive',
  context: {
    temperature: 20,
  },
  states: {
    inactive: {
      on: {
        POWER_TOGGLE: 'active'
      }
    },
    active: {
      on: {
        POWER_TOGGLE: 'inactive',
        SET_TEMPERATURE: {
          cond: (context, event) => event.temperature < 100,
          actions: assign({
            temperature: (context, event) => event.temperature,
          }),
        }
      }
    },
  }
});
Enter fullscreen mode Exit fullscreen mode

In plain English, our code above is saying

Once the SET_TEMPERATURE event is called, evaluate the guard. If the temperature of our event is below 100°C, update the temperature of the machine. Should the temperature be higher than 99°C, do not perform any action

For a visual representation, you can even see the guard being visualized by the XState visualizer here 🎉

Guard state transitions

Likewise, should the guard evaluate to false, no state transitions will be taken. In the example above, no state transition was taken as no target was defined but let's say we only want our thermostat to be turned off if the temperature has risen to at least 10°C. Below that point, the thermostat should always actively be monitoring the temperature.

import { Machine, assign } = 'xstate';

const thermostatMachine = Machine({
  id: 'thermostat',
  initial: 'inactive',
  context: {
    temperature: 20,
  },
  states: {
    inactive: {
      on: {
        POWER_TOGGLE: 'active'
      }
    },
    active: {
      on: {
        POWER_TOGGLE: {
          cond: (context, event) => {
            const isWarmEnough = context.temperature >= 10;
            if (!isWarmEnough) console.log("I think it's too cold for you to turn this off");
            // Do not forget to return the boolean :) 
            return isWarmEnough;
          },
          target: 'inactive',
        },
        SET_TEMPERATURE: {
          cond: (context, event) => event.temperature < 100,
          actions: assign({
            temperature: (context, event) => event.temperature,
          }),
        }
      }
    },
  }
});
Enter fullscreen mode Exit fullscreen mode

As seen above, only if the temperature is equal or greater than 10°C, the thermostat will go into the inactive state.


We've now seen how to guard transitions and actions using conditional logic. Guards are very important and especially when using them in conjunction with some other concepts - I'm eager to introduce the upcoming days - guards enable us to safely model a lot of complex behavior. I'm excited to be showing you what one can do with them.

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.

Top comments (3)

Collapse
 
karfau profile image
Christian Bewernitz

I think I do understand your goal of demonstrating features of XState, but I think there is an issue with your (mental?) model of a thermostat.

For a functioning thermostat there need to be two values related to temperature:

  • the one requested by the user
  • the one measured

Although they would ideally be very close to each other, the whole purpose of the device is to male that happen, since they aren't naturally.

This post is the first one where you mix these two perspectives on temperature into a single field.

An example: Would I be able to turn it off if I set the temperature to 25 °C even if the current temperature in the room is 5 °C?

Collapse
 
codingdive profile image
Mikey Stengel • Edited

Thank you for the comment. To keep the examples as simple as possible, they are indeed a bit contrived and do not represent a fully working thermostat in real life. I am indeed mixing the concept of a thermostat tracking the temperature and the temperature that is set by a user according to the explanation at hand. I'm sorry if this made the example more confusing. I did however give a hint to this inconsistency in the 13th post when I showed how context changes are driven by events from a component.

  return (
    <Sensor onChange={(e: {temperature: number }) => void send({ type: 
      SET_TEMPERATURE, temperature: e.temperature }) } />
  )

// or we let the user drive our context changes
  return (
    <input onChange={e => void send({ type: SET_TEMPERATURE, temperature: 
       e.target.value })} placeholder="Set the temperature." />
   )
Collapse
 
karfau profile image
Christian Bewernitz

I was indeed not aware oft the "Sensor" part, THX for the reply