XState action with input

With XState we always start by setting up the state machine using types, states and initial. We define the initial state as "Editing":

import { setup } from "xstate";
import { initialContext, type Context } from "./shared";

const machine = setup({
  types: {
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {},
  },
});

We also add an event to update the username and capture it inside the "Editing" state:

import { setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event = { type: "update-username"; username: string };

const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {
      on: {
        "update-username": // 👈 Capture the event inside "Editing"
      }
    },
  },
});

Since we want to update the context, we need to use assign inside actions.

assign provides access to the event payload (the username property inside update-username) that we use to update the context:

import { setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event = { type: "update-username"; username: string };

const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {
      on: {
        "update-username": {
          actions: assign(({ event }) => ({
            // 👇 Access to the `event` payload and assign it to context
            username: event.username,
          })),
        },
      }
    },
  },
});

With this we can already implement the initial component code:

export default function Machine() {
  const [snapshot, send] = useActor(machine);
  return (
    <form>
      <input
        type="text"
        value={snapshot.context.username}
        onChange={(e) =>
          send({ type: "update-username", username: e.target.value })
        }
      />
      <button>Confirm</button>
    </form>
  );
}

Action with input inside setup

As we saw before, actions can be defined inside setup as well.

We first add a new onUpdateUsername action to setup/actions, which uses assign since we want to update context:

import { assign, setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event = { type: "update-username"; username: string };

const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
  actions: {
    onUpdateUsername: assign(() => /* TODO */),
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {},
  },
});

Passing an input to an action is achieved by typing the second argument of assign as an object with a username property:

import { assign, setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event = { type: "update-username"; username: string };

const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
  actions: {
    onUpdateUsername: assign((_, { username }: { username: string }) => ({
      username,
    })),
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {},
  },
});

The first parameter contains the current context and other metadata. This second argument represents the required input for the action.

When we now intercept the update-username event inside the "Editing" state we will notice a type error:

import { assign, setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event = { type: "update-username"; username: string };

const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
  actions: {
    onUpdateUsername: assign((_, { username }: { username: string }) => ({
      username,
    })),
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {
      on: {
        "update-username": { // 👈 Type error
          actions: "onUpdateUsername"
        },
      },
    },
  },
});

When an action defines an input as the second argument, XState now requires the input value when executing the action.
When an action defines an input as the second argument, XState now requires the input value when executing the action.

This type error is telling us that we are required to pass the username as an input when executing the action.

We achieve this by converting actions to an object:

  • type: name of the action
  • params: pass the input to the action by extracting the event payload (the username property)
import { assign, setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event = { type: "update-username"; username: string };

const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
  actions: {
    // 👇 Name of the action
    onUpdateUsername: assign((_, { username }: { username: string }) => ({
      username,
    })),
  },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    Editing: {
      on: {
        "update-username": {
          actions: {
            type: "onUpdateUsername",
            // 👇 `param` must return the required input for the action
            params: ({ event }) => ({
              username: event.username,
            }),
          },
        },
      },
    },
  },
});

"params" gives access to the event payload and the current context. We can use these values as input to actions.
"params" gives access to the event payload and the current context. We can use these values as input to actions.