State machine advent: The power of null events (20/24)
Tags: javascript, typescript, react
Published: 2019-12-20 22:55:35

Events are what drive our state changes and are typically sent to a state machine by components. There is one event that is different and we have yet to cover. The null event:

```javascript
someStateNode: {
  on: {
    // This is a null event.
    '': {
      target: 'someOtherStateNode',
    },
  },
}
```

It doesn't have a type and is an internal event, meaning no component on the outside can send it to the machine. To elaborate, it is immediately executed once a state node is entered and instantly transitions our state machine to a new state. In the example above, as soon as `someStateNode` is entered, the machine will transition to `someOtherStateNode`.

This doesn't seem particularly useful on its own so let's see how this translates to a real-world example.


If we look at a simplified [thermostat machine from a few days ago](https://dev.to/codingdive/state-machine-advent-one-event-two-possible-state-transitions-15-24-588k), we can see that we've defined an initial temperature of 20°C and set the initial state to `warm`. This is correct since the `SET_TEMPERATURE` event would also transition our state machine to the `warm` state as soon as the temperature hits at least 18°C.

```javascript
import { Machine, assign } = 'xstate';

const thermostatMachine = Machine({
  id: 'thermostat',
  initial: 'warm',
  context: {
    temperature: 20,
  },
  states: {
    cold: {},
    warm: {},
  },
  on: {
    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,
        }),
      },
    ],
  },
});
```

A problem occurs if we lower the initial temperature but forget to change the initial state, or when we don't even control the initial temperature of the machine. You read that right. Similar to how a component can pass a [configuration object to define actions](https://dev.to/codingdive/state-machine-advent-let-the-machine-handle-accessibility-for-you-2n21) and guards, components can also set the initial context of our machine.

```jsx
import React from 'react';
import { useMachine } from 'xstate';
import {thermostatMachine} from './thermostatMachine';

const ColdRoom = () => {
  const [state, send] = useMachine(thermostatMachine.withContext({ temperature: 5 }));

  console.log(state);
  // state.context === 5;
  // state.matches('warm') === true (OH NO)
  return null;
}
```

When calling `thermostatMachine.withContext`, it merges and overwrites the context we have defined in our machine with the values passed to the function. Therefore, the initial temperature is no longer 20°C as specified in our machine definition, but 5°C. Despite the low initial temperature, our thermostat still thinks it's `warm` since the `SET_TEMPERATURE` event was never called to perform the conditional state transition that would rightfully put the machine into the `cold` state.

What I like to do to fix those kinds of problems is to add another state node called `initializing`. It should use a null event and multiple conditional transitions to set the correct initial state of the machine.

```javascript
import { Machine, assign } = 'xstate';

const thermostatMachine = Machine({
  id: 'thermostat',
  initial: 'initializing',
  context: {
    temperature: 20,
  },
  states: {
    initializing: {
      on: {
        '':
          [
            {
              target: 'cold',
              cond: (context) => context.temperature < 18,
            },
            {
              // transition without a guard as a fallback.
              target: 'warm',
            },
          ],
      }
    },
    cold: {},
    warm: {},
  },
  on: {
    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,
        }),
      },
    ],
  },
});
```

We have now ensured that our thermostat will always be in the correct state by setting `initializing` as the initial state which then instantly transitions our machine to `warm` or `cold` depending on the current `context`. Because the `initializing` state immediately transitions into another state, it is also known as a [transient transition](https://xstate.js.org/docs/guides/transitions.html#transient-transitions).

When defining null events, ensure to always work with the `context` as the event itself does not contain any data. If you were to log the event of the null event, it would just print: `{ type: '' }`.

### 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.