Context updates using actions

Synchronous effects like updating the context are called actions in XState.

actions in XState are synchronous functions. actions are responsible to execute side effects, like for example changing context.

actions are executed as part of a state transition.

We saw before that when no next state is defined (no target) the machine performs a self-transition.

actions are executed as part of a transition. The transition can be to a state to another, or to the state to itself (self-transition)
actions are executed as part of a transition. The transition can be to a state to another, or to the state to itself (self-transition)

Any events inside a state accepts and actions property:

import { setup } from "xstate";

type Event = { type: "toggle" };
type Context = { toggleValue: boolean };
const initialContext: Context = { toggleValue: false };

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: {
      on: {
        toggle: {
          target: "Idle",
          actions: // πŸ‘ˆ Actions to execute when the event is triggered
        },
      },
    },
  },
});

It's possible to execute multiple actions in a single event, that's why the parameter is called actions.

assign action

When an action updates the context we need to wrap it with assign:

import { assign, setup } from "xstate";

type Event = { type: "toggle" };
type Context = { toggleValue: boolean };
const initialContext: Context = { toggleValue: false };

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: {
      on: {
        toggle: {
          target: "Idle",
          actions: assign(
            // Update context function here
          ),
        },
      },
    },
  },
});

XState provides different helper functions to perform common actions.

assign is one of them. By calling assign we are telling XState to update the context with the return value of the function.

assign gives access to the current context and requires to return a new context. For our toggle machine we invert toggleValue:

import { assign, setup } from "xstate";

type Event = { type: "toggle" };
type Context = { toggleValue: boolean };
const initialContext: Context = { toggleValue: false };

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: {
      on: {
        toggle: {
          target: "Idle",
          actions: assign(
            // πŸ‘‡ Access to the current `context`
            ({ context }) => ({
              // πŸ‘‡ Invert `toggleValue`
              toggleValue: !context.toggleValue,
            })
          ),
        },
      },
    },
  },
});

With this every time the "toggle" event is triggered from the "Idle" state, actions are executed:

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: { // πŸ‘ˆ From the "Idle" state...
      on: {
        toggle: { // πŸ‘ˆ ...when the "toggle" event is triggered...
          target: "Idle",
          // πŸ‘‡ ...then `actions` are executed
          actions: assign(
            ({ context }) => ({
              toggleValue: !context.toggleValue,
            })
          ),
        },
      },
    },
  },
});

Defining actions inside setup

Actions can be defined inside setup as well. This is useful when you want to define actions that are shared across multiple machines.

We define the action inside setup/actions, give the action a name:

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
  actions: {
    onToggle: // πŸ‘ˆ Action definition
  }
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: {
      on: {
        toggle: {
          target: "Idle",
        },
      },
    },
  },
});

The implementation is the same as before:

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
  actions: {
    onToggle: assign(({ context }) => ({ toggleValue: !context.toggleValue })),
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: {
      on: {
        toggle: {
          target: "Idle",
        },
      },
    },
  },
});

With this we can simply reference the action by name inside the state actions property:

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
  actions: {
    onToggle: assign(({ context }) => ({ toggleValue: !context.toggleValue })),
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: {
      on: {
        toggle: {
          target: "Idle",
          actions: "onToggle",
        },
      },
    },
  },
});

With this every time the "toggle" event is triggered from the "Idle" state, the onToggle action is triggered:

import { assign, setup } from "xstate";

type Event = { type: "toggle" };
type Context = { toggleValue: boolean };
const initialContext: Context = { toggleValue: false };

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
  actions: {
    onToggle: assign(({ context }) => ({ toggleValue: !context.toggleValue })),
  },
}).createMachine({
  context: initialContext,
  initial: "Idle",
  states: {
    Idle: { // πŸ‘ˆ From the "Idle" state...
      on: {
        toggle: { // πŸ‘ˆ ...when the "toggle" event is triggered...
          target: "Idle",
          actions: "onToggle", // πŸ‘ˆ ...then the `onToggle` action is executed
        },
      },
    },
  },
});

When the "toggle" event is triggered inside "Idle" the machine performs a self-transition. "onToggle" is executed as part of the transition to update the context associated to the machine (using "assign").
When the "toggle" event is triggered inside "Idle" the machine performs a self-transition. "onToggle" is executed as part of the transition to update the context associated to the machine (using "assign").