State Machines: Should this be a state, or in context?
Tags: webdev, xstate
Published: 2021-04-29 07:54:37

State machines offer several API's for expressing state. Like other tools, you can keep arbitrary values in a store (usually expressed as an object) called `context`.

This is handy for values which change over time and you need to keep updated, like the value of a form input:

```ts
import { createMachine, assign } from 'xstate';

const machine = createMachine({
  context: {
    name: '',
  },
  on: {
    CHANGE_NAME: {
      actions: assign((context, event) => {
        return {
          name: event.value,
        };
      }),
    },
  },
});
```

Every time the `CHANGE_NAME` event is sent to the machine, we'll update the value in `context`. We can then use that value to display the value in our UI or send it to an API.

XState also gives you another way of expressing state - through finite states. Let's imagine a modal:

```ts
const machine = createMachine({
  initial: 'closed',
  states: {
    closed: {
      on: {
        OPEN: 'open',
      },
    },
    open: {
      on: {
        CLOSE: 'close',
      },
    },
  },
});
```

Here, the modal's state is expressed through the `states: {}` attribute, which also defines which events can be received during each state. You can only `CLOSE` the modal when it's `open`, and vice versa.

## Which should I choose?

The choice between using `context` and `states` isn't always clear. For instance, the modal machine above could be expressed using `context`:

```ts
const machine = createMachine({
  context: {
    isOpen: false,
  },
  on: {
    OPEN: {
      actions: assign({ isOpen: true }),
    },
    CLOSE: {
      actions: assign({ isOpen: false }),
    },
  },
});
```

This gives you exactly the same functionality as the states-based one above - you can track when the modal is open and closed, and send the same events.

The reason this can be expressed using both `states` and `context` is because _all of the events do the same thing no matter what state you're in_. There are no events you need to declare as impossible in certain states.

To show you what I mean, let's imagine a form input inside a modal. We only want to allow changes to the form input while the modal is open.

```ts
const machine = createMachine({
  initial: 'closed',
  context: {
    name: '',
  },
  states: {
    closed: {
      on: {
        OPEN: 'open',
      },
    },
    open: {
      on: {
        CLOSE: 'close',
        CHANGE_NAME: {
          actions: assign((context, event) => {
            return {
              name: event.value,
            };
          }),
        },
      },
    },
  },
});
```

When the modal is in the `closed` state, the `CHANGE_NAME` event will not change the value in `context`. State machines are great at this - only allowing the things you want to happen to happen. Some other examples might be:

- Not allowing users to submit a form while the previous API call is loading
- Only allowing users to log in if they're not already logged in

## Putting things in context

You might be wondering - but, I _can_ express the above in `context`!

```ts
const machine = createMachine({
  context: {
    name: '',
    isOpen: false,
  },
  on: {
    OPEN: { actions: assign({ isOpen: true }) },
    CLOSE: { actions: assign({ isOpen: false }) },
    CHANGE_NAME: {
      actions: assign((context, event) => {
        // This acts as the guard to prevent editing
        // the name while it's open
        if (!context.isOpen) return {};
        return {
          name: event.value,
        };
      }),
    },
  },
});
```

I think this is incorrect for two reasons. First, as requirements grow, so will the complexity of your logic. Let's imagine that the modal can now be either `closing` (i.e. animating out) or `closed`. We'll soon see an explosion of booleans, as I discussed in [this article on useState/useReducer](https://dev.to/mpocock1/usestate-vs-usereducer-vs-xstate-part-1-modals-569e).

Second, XState is auto-documenting via the [XState visualiser](https://xstate.js.org/viz/). The more your logic is expressed in `states`, the easier it's going to be to visualise. The machine above is basically a single state with its logic expressed in ways that XState can't visualise.

## Rules to live by

You should be keeping most of your state in context. That includes form values, API data - anything which cannot be expressed finitely.

But state machines are powerful _because_ of their states. Use states when you want to express your logic visually, or gate events to certain states.