Sustainable xState machines
Tags: xstate, react, nextjs, typescript
Published: 2023-02-26 23:26:54


### TLDR
If you are curious about the full-working example, it is [here](https://github.com/g-todorov/sustainable-xstate-machines-example).

### Background
I've been using xState with React for some time already. Throughout the projects' development, I often find myself reusing the same machine in different contexts. Sometimes makes sense to `interpret` the machine from the React component, other times it is the logical choice to `spawn` and store it as a child `actor`.
That's why I came up with a simple abstraction that let's me use the machine in both ways.

### Use case example
Let's say we need a toggle machine for our checkbox component. We create a simple machine with `on` and `off` states and then use it directly in our checkbox component via the `useMachine` hook.
But in another page we have a more complex machine, which can make good use of the toggle machine for displaying a notification based on business logic. This sounds as a good opportunity to `spawn` a toggle actor, which will be controlled by the complex machine.
Now we need the same machine, but utilised in two different ways.

### Checkbox component
Firstly, we need to create our toggle machine.

```javascript
export const toggleMachine = createMachine({
  id: "toggleMachine",
  schema: {
    events: {} as { type: "TOGGLE" },
  },
  initial: "off",
  states: {
    off: {
      on: {
        TOGGLE: {
          target: "on",
        },
      },
    },
    on: {
      on: {
        TOGGLE: {
          target: "off",
        },
      },
    },
  },
});
```
It is a simple `statechart` with `on` and `off` states. This will be sufficient for our checkbox component. We just have to pass it to the `useMachine` hook.

```javascript
interface Props {
  onChange(checked: boolean): void;
}

export function Checkbox({ onChange }: Props) {
  const [state, send] = useMachine(toggleMachine);

  return (
    <>
      <label htmlFor="toggle">Toggle</label>
      <input
        id="toggle"
        type="checkbox"
        checked={state.matches("on")}
        onChange={(event) => {
          send({ type: "TOGGLE" });
          onChange(event.target.checked);
        }}
      />
    </>
  );
}
```
We use the `matches` method to confirm the machine state and control the `checked` attribute. When `onChange` is triggered, the machine state is toggled and the callback from the props is fired.

The only problem that we are facing now is that we cannot control the initial state of the checkbox component. It will always be initialised as `unchecked` until further interaction occurs.

We can easily solve the issue by lazy-loading the machine. By returning the machine from a factory function, we can pass the initial state as a variable. 

The final step, is to lazily-create the machine with the `useMachine` hook. This will prevent the warnings for a new machine instance being passed to the hook on each re-render.  
```javascript
export function toggleMachine({ initial }: { initial: string }) {
  return createMachine({
    id: "toggleMachine",
    initial,
    states: {
     /* ... */
    },
  });
}

/* ... */

const [state, send] = useMachine(
  () => toggleMachine({ initial })
);
```

### Data fetching notification 
Supposing we need to show specific data to the user when downloaded. To make it sure that the user won't miss the information, we put it in a notification component. 

We can start by creating the fetching machine (parent machine) that takes care of downloading the user info.

```javascript
const fetchingMachine = createMachine(
  {
    id: "fetchMachine",
    initial: "fetching",
    on: {
      FETCHING: {
        target: "fetching",
      },
    },
    states: {
      idle: {},
      fetching: {
        invoke: {
          src: "fetchData",
          onDone: {
            target: "idle",
          },
        },
      },
    },
  },
  {
    services: {
      async fetchData() {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/todos/"
        );
        const data = await response.json();

        return data;
      },
    },
  }
);
```

Similarly to the `Checkbox` component, the `Notificaion` one needs `on` and `off` states, in order to control its visibility. The difference now is that instead of interpreting the machine directly in the component, we can `spawn` an `actor` and keep it in the fetching `machine`'s context.

In my opinion, this approach gives us more flexibility when it comes to controlling the toggle machine. We can `spawn` the machine explicitly when needed, instead of relying on the React render cycle.
```javascript
const fetchingMachine = createMachine(
  {
    /* ... */
    states: {
      idle: {},
      fetching: {
        invoke: {
          src: "fetchData",
          onDone: {
            actions: ["assignToggleRef"],
            target: "idle",
          },
        },
      },
    },
  },
  {
    actions: {
      assignToggleRef: assign({
        toggleRef: (context, event) => {
          return spawn(toggleMachine({ initial: "on" }));
        },
      }),
    },
    services: {
      async fetchData() {
        /* ... */
      },
    },
  }
);
```
We can simply pass the `toggleRef` to the `Notification component` and interpret it from there with the guarantee that the fetching of the data is completely finished. 

```javascript
interface Props {
  actor: ActorRefFrom<typeof toggleMachine>;
}

export function Notification({ actor, data }: Props) {
  const [state, send] = useActor(actor);

  return (
    <div>
      {state.matches("on") && <div>Very important notification</div>}
      <button
        onClick={() => {
          send({ type: "TOGGLE" });
        }}
      >
        {state.matches("on") ? "close" : "open"}
      </button>
    </div>
  );
}
```
Now we have access to the actor's `state` and `context`. Also, since the actor reference is kept in the parent's machine context, we can `send` events to the interpreted actor regardless of the parent's machine state.

### Unite both worlds
Using directly the exported machine is perfectly fine, but I fancy adding one more layer of abstraction.

```javascript
import { ActorRefFrom, createMachine, spawn } from "xstate";

export type ToggleMachineActor = ActorRefFrom<typeof toggleMachine>;

function toggleMachine({ initial }: { initial: string }) {
  return createMachine({
    id: "toggleMachine",
    /* ... */
  });
}

export function toggleMachineCreate({ initial = "off" }): {
  machine: ReturnType<typeof toggleMachine>;
  spawn: () => ToggleMachineActor;
} {
  const machine = toggleMachine({ initial });

  return {
    machine,
    spawn: () => spawn(machine, { name: "toggleMachine" }),
  };
}

```
Now, our `toggleMachineCreate` returns both the machine instance and the spawned actor, which I find a bit cleaner when it comes to using the machine.

```javascript
toggleRef: (context, event) => {
  return toggleMachineCreate({ initial: "on" }).spawn();
}

/* or */

const [state, send] = useMachine(
  () => toggleMachineCreate({ initial }).machine
);
```
Another advantage of this approach is that we have a convenient place where we can extend the machines with the `withContext` and `withConfig` utility methods. This gives our code а pinch of readability.

```javascript
export function toggleMachineCreate({ initial = "off", specialService = Promise<any> }): {
  machine: ReturnType<typeof toggleMachine>;
  spawn: () => ToggleMachineActor;
} {
  const machine = toggleMachine({ initial }).withConfig({
    services: { specialService },
  });

  return {
    machine,
    spawn: () => spawn(machine, { name: "toggleMachine" }),
  };
}
```

On top of that, you can easily pass arguments to the `spawn` function too. That might be helpful when multiple actors from a machine are spawned and stored into a single context. Distinguishing the references by name, might be helpful when it comes to communication with them.

```javascript
export function toggleMachineCreate(): {
  /* ... */

  return {
    machine,
    spawn: (name: string) => spawn(machine, { name }),
  };
}

/* ... */

actions: {
  assignToggleRef1: assign({
    toggleRef1: (context, event) => {
      return toggleMachineCreate().spawn("toggleMachine1");
    },
  }),
  assignToggleRef2: assign({
    toggleRef2: (context, event) => {
      return toggleMachineCreate().spawn("toggleMachine2");
    },
  }),
},

/* ... */
```
And last but not least, I find it really handy to have the actor type exported along with the `toggleMachineCreate` function. It is quite useful when it comes to prop drilling the actor and defining the prop types.
```javascript
export type ToggleMachineActor = ActorRefFrom<typeof toggleMachine>;

/* ... */

interface Props {
  actor: ToggleMachineActor;
}
```

### Opinionated conclusion
This pattern scales greatly and gives a good amount of predictability in regards to code organisation and readability.