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 immediateraise
: Already reflected in the returned state- Delayed
raise
: Returns action with delay information cancel
,sendTo
,emit
,log
: Return action objects for manual handlingspawn
: 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);