DEV Community

Mikey Stengel
Mikey Stengel

Posted on • Updated on

State machine advent: Update XState context with actions (13/24)

Yesterday, we introduced extended state or context to express dynamic data in our statechart. We also learned how to set the initial state of our context.
Today we want to take a look at how we can dynamically change the context value of our statechart.

When we send events in XState, we can not only react to the events by instructing our machine to go from one state to another, we can also perform side effects. Side effects go into the actions property of our statechart. One of the most commonly used actions is to change the context.

type ISetTemperatureEvent = {
  type: 'SET_TEMPERATURE';
  temperature: number;
};

We defined our event to set the temperature as seen above. We now want to implement the event within our thermostat machine. We do so by assigning the temperature value of our event to the context of the machine.

import { Machine, assign } = 'xstate';

const thermostatMachine = Machine<ThermostatContext, ThermostatStateSchema, ThermostatEvent>({
  id: 'thermostat',
  initial: 'inactive',
  context: {
    temperature: 20,
  },
  states: {
    inactive: {
      on: {
        POWER_TOGGLE: 'active'
      }
    },
    active: {
      on: {
        POWER_TOGGLE: 'inactive',
        SET_TEMPERATURE: {
          // event that doesn't have a target
          // Not depicted below but one common practice that you'll come across
          // is to replace `context` with `_` if it's not needed
          actions: assign({
            temperature: (context, event: ISetTemperatureEvent) => event.temperature,
          }),
        }
      }
    },
  }
});

Note how we can only set the temperature when the thermostat is in the active state. Sending the event when our thermostat is inactive will do nothing.

Moving on, assign is a function that takes in an object with each key representing one context variable we want to update. The value of each variable is a callback function that is invoked with the current context as a first argument and the event as a second argument. The return value of our callback function will be the new context value. As a result, we never mutate the context directly and always use the return value of our callback function to update it.

In our React code, we would listen to some kind of sensor or external event (such as user input) before sending the event to our invoked machine.

import { useMachine } from '@xstate/react';
const Thermostat = () => {
  const [state, send] = useMachine(thermostatMachine);

  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." />
   )
}

In short:

  • side effects such as changing the context take place inside actions
  • actions are executed after an event is sent to an invoked machine

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 (0)