Stately
State Machines

Pure transition functions

Pure transition functions allow you to compute the next state and actions of a state machine without creating a live actor or executing any side effects. This is useful for server-side applications, testing, and scenarios where you need to compute state transitions without side effects.

There are two main functions you can use to compute state transitions:

  • initialTransition(machine, input?): Returns a tuple of [initialState, initialActions] that represents the initial state and any entry actions for a state machine.
  • transition(machine, state, event): Returns a tuple of [nextState, actions] that represents the next state and any actions that would be executed during the transition.
import { createMachine, initialTransition, transition } from 'xstate';

const machine = createMachine({
  initial: 'pending',
  states: {
    pending: {
      on: {
        start: { target: 'started' },
      },
    },
    started: {
      entry: { type: 'doSomething' },
    },
  },
});

// Get initial state and actions
const [initialState, initialActions] = initialTransition(machine);

console.log(initialState.value); // 'pending'
console.log(initialActions); // [{ type: 'doSomething', ... }]

// Get next state and actions
const [nextState, actions] = transition(machine, initialState, {
  type: 'start', // The event to send
});

console.log(nextState.value); // 'started'
console.log(actions); // [{ type: 'doSomething', ... }]

This pure functional approach offers several benefits:

  • Deterministic: Same input always produces the same output
  • Testable: Easy to test state logic without managing actor lifecycles
  • Server-friendly: Perfect for server-side workflows and API endpoints
  • Debuggable: Can inspect state changes and actions without side effects

initialTransition(machine, input?)

Returns the initial state and any entry actions for a state machine. If the machine requires input, you should pass it as the second argument to initialTransition.

import { createMachine, initialTransition, transition } from 'xstate';

const machine = createMachine({
  initial: 'pending',
  context: ({ input }) => ({
    count: input.initialCount,
  }),
  states: {
    pending: {
      on: {
        start: { target: 'started' },
      },
    },
    started: {
      entry: { type: 'doSomething' },
    },
  },
});

// Get initial state and actions
const [initialState, initialActions] = initialTransition(machine, {
  initialCount: 0,
});

console.log(initialState.value); // 'pending'
console.log(initialState.context); // { count: 0 }
console.log(initialActions); // [{ type: 'doSomething', ... }]

transition(machine, state, event)

Computes the next state and actions given a current state and event.

import { createMachine, initialTransition, transition } from 'xstate';

const machine = createMachine({
  initial: 'pending',
  states: {
    pending: {
      on: {
        start: { target: 'started' },
      },
    },
    started: {
      entry: { type: 'doSomething' },
    },
  },
});

// Get initial state and actions
const [initialState, initialActions] = initialTransition(machine);

// Get next state and actions
const [nextState, actions] = transition(machine, initialState, {
  type: 'start', // The event to send
});

console.log(nextState.value); // 'started'
console.log(actions); // [{ type: 'doSomething', ... }]

Actions

Actions represent side effects that would be executed during a transition. The pure functions capture these actions but don't execute them, giving you full control over when and how to handle them.

The primary focus should be on custom actions - actions you define in your state machine. These are captured as action objects with type and params:

import { createMachine, setup, transition } from 'xstate';

const machine = setup({
  actions: {
    sendEmail: (_, params: { to: string; subject: string }) => {
      // This won't execute in pure functions
      console.log(`Sending email to ${params.to}: ${params.subject}`);
    },
    updateDatabase: (_, params: { userId: string; data: any }) => {
      // This won't execute in pure functions
      console.log(`Updating user ${params.userId}`, params.data);
    },
  },
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        processUser: {
          target: 'processing',
          actions: [
            {
              type: 'sendEmail',
              params: ({ event }) => ({
                to: event.email,
                subject: 'Processing started',
              }),
            },
            {
              type: 'updateDatabase',
              params: ({ event }) => ({
                userId: event.userId,
                data: { status: 'processing' },
              }),
            },
          ],
        },
      },
    },
    processing: {},
  },
});

const [initialState] = initialTransition(machine);
const [nextState, actions] = transition(machine, initialState, {
  type: 'processUser',
  userId: '123',
  email: 'user@example.com',
});

console.log(actions);
// [
//   {
//     type: 'sendEmail',
//     params: { to: 'user@example.com', subject: 'Processing started' }
//   },
//   {
//     type: 'updateDatabase',
//     params: { userId: '123', data: { status: 'processing' } }
//   }
// ]

Microsteps

The transition() function returns the final state after processing an event, but sometimes you need to see every intermediate state along the way. The getMicrosteps() and getInitialMicrosteps() functions return an array of microsteps: each microstep is a [snapshot, actions] tuple representing an intermediate state and its associated actions.

This is useful when a single event triggers multiple transitions (e.g., via always transitions, raised events, or entry actions that raise events).

getMicrosteps(machine, snapshot, event)

Returns an array of [snapshot, actions] tuples for each intermediate step when processing an event.

import { createMachine, getMicrosteps, initialTransition } from 'xstate';
import { raise } from 'xstate/actions';

const machine = createMachine({
  initial: 'first',
  states: {
    first: {
      on: {
        TRIGGER: {
          target: 'second',
          actions: raise({ type: 'RAISED' })
        }
      }
    },
    second: {
      on: {
        RAISED: 'third'
      }
    },
    third: {
      always: 'fourth'
    },
    fourth: {}
  }
});

const [initialState] = initialTransition(machine);

// Get all intermediate states, not just the final one
const microsteps = getMicrosteps(machine, initialState, { type: 'TRIGGER' });

microsteps.forEach(([snapshot, actions]) => {
  console.log(snapshot.value, actions);
});
// 'second', [{ type: 'xstate.raise', ... }]
// 'third', []
// 'fourth', []

getInitialMicrosteps(machine, input?)

Returns an array of [snapshot, actions] tuples for each intermediate step during the initial transition.

import { createMachine, getInitialMicrosteps } from 'xstate';

const machine = createMachine({
  initial: 'a',
  states: {
    a: {
      entry: () => {/* ... */},
      always: {
        target: 'b',
        actions: () => {/* ... */}
      }
    },
    b: {
      entry: () => {}
    }
  }
});

const microsteps = getInitialMicrosteps(machine);

// First microstep: entering 'a' (entry action)
console.log(microsteps[0][0].value); // 'a'
console.log(microsteps[0][1].length); // 1 (entry action)

// Second microstep: 'a' -> 'b' (always transition + entry action)
console.log(microsteps[1][0].value); // 'b'
console.log(microsteps[1][1].length); // 2 (always action + entry action)

If the machine requires input, pass it as the second argument:

const microsteps = getInitialMicrosteps(machine, { value: 42 });

Resolving Persisted State

When working with persisted state, use machine.resolveState() to restore snapshots:

// Persist state
const stateToPersist = JSON.stringify(currentState);

// Later, restore state
const restoredState = machine.resolveState(JSON.parse(stateToPersist));
const [nextState, actions] = transition(machine, restoredState, event);

On this page