XState v5 brings significant improvements to the API, TypeScript types, and overall developer experience. This guide will help you migrate from XState v4 to v5.
Key Changes
Interpreter renamed to Actor
The interpret() function has been replaced with createActor():
-import { createMachine, interpret } from 'xstate';
+import { createMachine, createActor } from 'xstate';
const machine = createMachine({ /* ... */ });
-const service = interpret(machine).start();
+const actor = createActor(machine).start();
setup() API (Recommended)
XState v5 introduces the setup() API for better type safety and organization:
import { setup , createActor } from 'xstate' ;
const machine = setup ({
types: {} as {
context : { count : number };
events : { type : 'INCREMENT' } | { type : 'DECREMENT' };
},
actions: {
increment: assign ({ count : ({ context }) => context . count + 1 })
},
guards: {
isPositive : ({ context }) => context . count > 0
}
}). createMachine ({
context: { count: 0 },
initial: 'active' ,
states: {
active: {
on: {
INCREMENT: { actions: 'increment' }
}
}
}
});
State to Snapshot
All references to “state” are now “snapshot” for clarity:
-const state = actor.getSnapshot();
+const snapshot = actor.getSnapshot();
-actor.subscribe((state) => {
- console.log(state.value);
+actor.subscribe((snapshot) => {
+ console.log(snapshot.value);
});
Sending Events
Events must now be objects, not strings:
-actor.send('TOGGLE');
+actor.send({ type: 'TOGGLE' });
-actor.send('EVENT', { some: 'payload' });
+actor.send({ type: 'EVENT', some: 'payload' });
Sending string events will throw an error in v5. Always use event objects with a type property.
Schema to Types
The schema property has been renamed to types:
const machine = createMachine({
- schema: {
- context: {} as { count: number },
- events: {} as { type: 'INCREMENT' }
- },
+ types: {} as {
+ context: { count: number };
+ events: { type: 'INCREMENT' };
+ },
context: { count: 0 }
});
Context is Required
If you specify context types, the context property is now required:
// ❌ TypeScript error
createMachine ({
types: {} as {
context : { count : number };
}
// Missing context property
});
// ✅ OK
createMachine ({
types: {} as {
context : { count : number };
},
context: {
count: 0
}
});
Actions are Now Functions
All built-in action creators (assign, sendTo, etc.) now return functions:
import { assign } from 'xstate' ;
// The exact shape is an implementation detail
// Just pass them directly to actions
const machine = createMachine ({
context: { count: 0 },
entry: assign ({ count : ({ context }) => context . count + 1 })
});
Assign Actions Execute in Order
assign() actions are now executed in the order they are defined, rather than being automatically prioritized:
const machine = createMachine ({
context: { count: 0 , doubled: 0 },
entry: [
assign ({ count : ({ context }) => context . count + 1 }), // count = 1
assign ({ doubled : ({ context }) => context . count * 2 }) // doubled = 2 (uses updated count)
]
});
To maintain v4 behavior, ensure assign() actions are defined before other actions.
Data Renamed to Output
Final states now use output instead of data:
const machine = createMachine({
initial: 'loading',
states: {
loading: {
on: { SUCCESS: 'success' }
},
success: {
type: 'final',
- data: { message: 'Success!' }
+ output: { message: 'Success!' }
}
}
});
External Renamed to Reenter
The external property on transitions has been renamed to reenter:
on: {
SOME_EVENT: {
target: 'sameState',
- external: true
+ reenter: true
}
}
In Guards Replaced with stateIn()
The in property for conditional transitions is now a guard:
+import { stateIn } from 'xstate/guards';
on: {
SOME_EVENT: {
target: 'somewhere',
- in: '#someState'
+ guard: stateIn('#someState')
}
}
Higher-Level Guards
XState v5 includes composable guards:
import { and , or , not } from 'xstate/guards' ;
const machine = createMachine ({
on: {
EVENT: {
target: 'somewhere' ,
guard: and ([
'isAuthenticated' ,
or ([ 'hasPermission' , not ( 'isBlocked' )])
])
}
}
});
Actor Systems and systemId
Actors are now part of a “system” and can be referenced via systemId:
const machine = createMachine ({
invoke: {
src: emailMachine ,
systemId: 'emailer' // Register with system
}
});
// Reference from anywhere in the system
const anotherMachine = createMachine ({
entry: sendTo (
({ system }) => system . get ( 'emailer' ),
{ type: 'SEND_EMAIL' }
)
});
Restoring Persisted State
State restoration now uses the snapshot option:
-interpret(machine).start(state);
+createActor(machine, { snapshot }).start();
Get persisted snapshot:
const actor = createActor ( machine ). start ();
const persistedSnapshot = actor . getPersistedSnapshot ();
// Later...
const restoredActor = createActor ( machine , {
snapshot: persistedSnapshot
}). start ();
Input Instead of withContext
Pass initial data via input:
const greetMachine = createMachine ({
context : ({ input }) => ({
greeting: `Hello ${ input . name } !`
}),
entry : ({ event }) => {
console . log ( event . input ); // { name: 'David' }
}
});
const actor = createActor ( greetMachine , {
input: { name: 'David' }
}). start ();
AutoForward Removed
Explicitly forward events instead:
invoke: {
id: 'child',
src: 'someSrc',
- autoForward: true
},
on: {
+ EVENT_TO_FORWARD: {
+ actions: sendTo('child', (_, event) => event)
+ }
}
Delayed Event IDs
Delayed events no longer derive IDs from event types. Use explicit IDs:
-entry: raise({ type: 'TIMER' }, { delay: 200 });
-exit: cancel('TIMER');
+entry: raise({ type: 'TIMER' }, { delay: 200, id: 'myTimer' });
+exit: cancel('myTimer');
Compound States Require Initial
An error is now thrown if compound states don’t specify an initial state:
const machine = createMachine ({
initial: 'red' ,
states: {
red: {
initial: 'walk' , // ✅ Required!
states: {
walk: {},
wait: {}
}
}
}
});
Forgetting initial will throw: “No initial state specified for state node. Try adding initial: 'stateName'”.
Snapshot Shape Consistency
All actor snapshots have consistent properties:
status: 'active' | 'done' | 'error' | 'stopped'
output: Output data when status: 'done'
error: Error when status: 'error'
context: Actor context
const promiseActor = fromPromise ( async () => 42 );
const snapshot = promiseActor . getSnapshot ();
if ( snapshot . status === 'done' ) {
console . log ( snapshot . output ); // 42
}
Removed Methods
The following have been removed:
actorRef.onEvent() - Use subscribe() instead
actorRef.onSend() - Use subscribe() instead
actorRef.onChange() - Use subscribe() instead
actorRef.sender() - Use actorRef.send() instead
actorRef.onStop() - Use subscribe({ complete() {} }) instead
state.toStrings() - Not replaced
state.nextEvents - Not replaced
machine.initialState - Use machine.getInitialState() instead
machine.withConfig() - Use machine.provide() instead
Target Resolution
Targeting siblings from root now requires explicit relative syntax:
createMachine({
id: 'direction',
initial: 'left',
states: {
left: {},
right: {}
},
on: {
- LEFT_CLICK: 'left',
+ LEFT_CLICK: '.left'
}
});
Migration Checklist
Next Steps
Breaking Changes Reference Complete list of all breaking changes in v5
Getting Started Start building with XState v5