Skip to content
Version: XState v5

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' } }
// }
// ]

Built-in Actions (Side Note)​

Some built-in actions require special handling:

  • assign and immediate raise: Already reflected in the returned state
  • Delayed raise: Returns action with delay information
  • cancel, sendTo, emit, log: Return action objects for manual handling
  • spawn: Returns spawn action for creating child actors
// Example with delayed raise
const machine = createMachine({
initial: 'waiting',
states: {
waiting: {
after: {
5000: 'timeout',
},
},
timeout: {},
},
});

const [state, actions] = initialTransition(machine);
console.log(actions);
// [{ type: 'xstate.raise', params: { delay: 5000, event: { type: 'xstate.after.5000...' } } }]

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);