Migrating from XState v4 to v5
The guide below explains how to migrate from XState version 4 to version 5. Migrating from XState v4 to v5 should be a straightforward process. If you get stuck or have any questions, please reach out to the Stately team on our Discord.
This guide is for developers who want to update their codebase from v4 to v5 and should also be valuable for any developers wanting to know the differences between v4 and v5.
Creating machines and actorsβ
Use createMachine()
, not Machine()
β
The Machine(config)
function is now called createMachine(config)
:
- XState v5 beta
- XState v4
import { createMachine } from 'xstate';
const machine = createMachine({
// ...
});
// β DEPRECATED
import { Machine } from 'xstate';
const machine = Machine({
// ...
});
Use createActor()
, not interpret()
β
The interpret()
function has been renamed to createActor()
:
- XState v5 beta
- XState v4
import { createMachine, createActor } from 'xstate';
const machine = createMachine(/* ... */);
// β
const actor = createActor(machine, {
// actor options
});
import { createMachine, interpret } from 'xstate';
const machine = createMachine(/* ... */);
// β DEPRECATED
const actor = interpret(machine, {
// actor options
});
Use machine.provide()
, not machine.withConfig()
β
The machine.withConfig()
method has been renamed to machine.provide()
:
- XState v5 beta
- XState v4
// β
const specificMachine = machine.provide({
actions: {
/* ... */
},
guards: {
/* ... */
},
actors: {
/* ... */
},
// ...
});
// β DEPRECATED
const specificMachine = machine.withConfig({
actions: {
/* ... */
},
guards: {
/* ... */
},
services: {
/* ... */
},
// ...
});
Set context with input
, not machine.withContext()
β
The machine.withContext(...)
method can no longer be used, as context
can no longer be overridden directly. Use input instead:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
context: ({ input }) => ({
actualMoney: Math.min(input.money, 42),
}),
});
const actor = createActor(machine, {
input: {
money: 1000,
},
});
// β DEPRECATED
const machine = createMachine({
context: {
actualMoney: 0,
},
});
const moneyMachine = machine.withContext({
actualMoney: 1000,
});
Actions ordered by default, predictableActionArguments
no longer neededβ
Actions are now in predictable order by default, so the predictableActionArguments
flag is no longer required. Assign actions will always run in the order they are defined.
- XState v5 beta
- XState v4
// β
const machine = createMachine({
entry: [
({ context }) => {
console.log(context.count); // 0
},
assign({ count: 1 }),
({ context }) => {
console.log(context.count); // 1
},
assign({ count: 2 }),
({ context }) => {
console.log(context.count); // 2
},
],
});
// β DEPRECATED
const machine = createMachine({
predictableActionArguments: true,
entry: [
(context) => {
console.log(context.count); // 0
},
assign({ count: 1 }),
(context) => {
console.log(context.count); // 1
},
assign({ count: 2 }),
(context) => {
console.log(context.count); // 2
},
],
});
Statesβ
Use state.getMeta()
instead of state.meta
β
The state.meta
property has been renamed to state.getMeta()
:
- XState v5 beta
- XState v4
// β
state.getMeta();
// β DEPRECATED
state.meta;
The state.getStrings()
method has been removedβ
Use state._nodes
instead of state.configuration
β
The state.configuration
property has been renamed to state._nodes
:
- XState v5 beta
- XState v4
// β
state._nodes;
// β DEPRECATED
state.configuration;
Events and transitionsβ
Implementation functions receive a single argumentβ
Implementation functions now take in a single argument: an object with context
, event
, and other properties.
- XState v5 beta
- XState v4
// β
const machine = createMachine({
entry: ({ context, event }) => {
// ...
},
});
// β DEPRECATED
const machine = createMachine({
entry: (context, event) => {
// ...
},
});
Use either raise()
or sendTo()
, not send()
β
The send(...)
action creator is removed. Use raise(...)
for sending events to self or sendTo(...)
for sending events to other actors instead.
Read the documentation on the sendTo
action and raise
action for more information.
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
entry: [
// Send an event to self
raise({ type: 'someEvent' }),
// Send an event to another actor
sendTo('someActor', { type: 'someEvent' }),
],
});
// β DEPRECATED
const machine = createMachine({
// ...
entry: [
// Send an event to self
send({ type: 'someEvent' }),
// Send an event to another actor
send({ type: 'someEvent' }, { to: 'someActor' }),
],
});
Pre-migration tip: Update v4 projects to use sendTo
or raise
instead of send
.
actor.send()
no longer accepts string typesβ
String event types can no longer be sent to, e.g., actor.send(event)
; you must send an event object instead:
- XState v5 beta
- XState v4
// β
actor.send({ type: 'someEvent' });
// β DEPRECATED
actor.send('someEvent');
Pre-migration tip: Update v4 projects to pass an object to .send()
.
state.can()
no longer accepts string typesβ
String event types can no longer be sent to, e.g., state.can(event)
; you must send an event object instead:
- XState v5 beta
- XState v4
// β
state.can({ type: 'someEvent' });
// β DEPRECATED
state.can('someEvent');
Guarded transitions use guard
, not cond
β
The cond
transition property for guarded transitions is now called guard
:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
on: {
someEvent: {
guard: 'someGuard',
target: 'someState',
},
},
});
// β DEPRECATED
const machine = createMachine({
on: {
someEvent: {
// renamed to `guard` in v5
cond: 'someGuard',
target: 'someState',
},
},
});
Use params
to pass params to actions & guardsβ
Properties other than type
on action objects and guard objects should be nested under a params
property; { type: 'someType', message: 'hello' }
becomes { type: 'someType', params: { message: 'hello' }}
. These params
are then passed to the 2nd argument of the action or guard implementation:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
entry: {
type: 'greet',
params: {
message: 'Hello world',
},
},
on: {
someEvent: {
guard: { type: 'isGreaterThan', params: { value: 42 } },
},
},
}).provide({
actions: {
greet: ({ context, event }, params) => {
console.log(params.message); // 'Hello world'
},
},
guards: {
isGreaterThan: ({ context, event }, params) => {
return event.value > params.value;
},
},
});
// β DEPRECATED
const machine = createMachine(
{
entry: {
type: 'greet',
message: 'Hello world',
},
on: {
someEvent: {
cond: { type: 'isGreaterThan', value: 42 },
},
},
},
{
actions: {
greet: (context, event, { action }) => {
console.log(action.message); // 'Hello world'
},
},
guards: {
isGreaterThan: (context, event, { guard }) => {
return event.value > guard.value;
},
},
},
);
Pre-migration tip: Update action and guard objects on v4 projects to move properties (other than type
) to a params
object.
Use wildcard *
transitions, not strict modeβ
Strict mode is removed. If you want to throw on unhandled events, you should use a wildcard transition:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
on: {
knownEvent: {
// ...
},
'*': {
// unknown event
actions: ({ event }) => {
throw new Error(`Unknown event: ${event.type}`);
},
},
},
});
// β DEPRECATED
const machine = createMachine({
strict: true,
on: {
knownEvent: {
// ...
},
},
});
Use explicit eventless (always
) transitionsβ
Eventless (βalwaysβ) transitions must now be defined through the always: { ... }
property of a state node; they can no longer be defined via an empty string:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
states: {
someState: {
always: {
target: 'anotherState',
},
},
},
});
// β DEPRECATED
const machine = createMachine({
// ...
states: {
someState: {
on: {
'': {
target: 'anotherState',
},
},
},
},
});
Pre-migration tip: Update v4 projects to use always
for eventless transitions.
Use reenter: true
, not internal: false
β
internal: false
is now reenter: true
External transitions previously specified with internal: false
are now specified with reenter: true
:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
on: {
someEvent: {
target: 'sameState',
reenter: true,
},
},
});
// β DEPRECATED
const machine = createMachine({
// ...
on: {
someEvent: {
target: 'sameState',
internal: false,
},
},
});
Transitions are internal by default, not externalβ
All transitions are implicitly internal. This change is relevant for transitions defined on compound state nodes with entry
or exit
actions, invoked actors, or delayed transitions (after
). If you relied on implicit re-entering of a compound state node, use reenter: true
:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
states: {
compoundState: {
entry: 'someAction',
on: {
someEvent: {
target: 'compoundState.childState',
// Reenters the `compoundState` state,
// just like an external transition
reenter: true,
},
},
initial: 'childState',
states: {
childState: {},
},
},
},
});
// β DEPRECATED
const machine = createMachine({
// ...
states: {
compoundState: {
entry: 'someAction',
on: {
someEvent: {
// implicitly external
target: 'compoundState.childState', // non-relative target
},
},
initial: 'childState',
states: {
childState: {},
},
},
},
});
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
states: {
compoundState: {
after: {
1000: {
target: 'compoundState.childState',
reenter: true, // make it external explicitly!
},
},
initial: 'childState',
states: {
childState: {},
},
},
},
});
// β DEPRECATED
const machine = createMachine({
// ...
states: {
compoundState: {
after: {
1000: {
// implicitly external
target: 'compoundState.childState', // non-relative target
},
},
initial: 'childState',
states: {
childState: {},
},
},
},
});
Child state nodes are always re-enteredβ
Child state nodes are always re-entered when they are targeted by transitions (both external and internal) defined on compound state nodes. This change is relevant only if a child state node has entry
or exit
actions, invoked actors, or delayed transitions (after
). Add a stateIn
guard to prevent undesirable re-entry of the child state:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
states: {
compoundState: {
on: {
someEvent: {
guard: not(stateIn({ compoundState: 'childState' }),
target: '.childState',
},
},
initial: 'childState',
states: {
childState: {
entry: 'someAction',
},
},
},
},
})
// β DEPRECATED
const machine = createMachine({
// ...
states: {
compoundState: {
on: {
someEvent: {
// Implicitly internal; childState not re-entered
target: '.childState',
},
},
initial: 'childState',
states: {
childState: {
entry: 'someAction',
},
},
},
},
});
Use stateIn()
to validate state transitions, not in
β
The in: 'someState'
transition property is removed. Use guard: stateIn(...)
instead:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
on: {
someEvent: {
guard: stateIn({ form: 'submitting' }),
target: 'someState',
},
},
});
// β DEPRECATED
const machine = createMachine({
on: {
someEvent: {
in: '#someMachine.form.submitting'
target: 'someState',
},
},
});
Use actor.subscribe()
instead of state.history
β
The state.history
property is removed. If you want the previous snapshot, you should maintain that via actor.subscribe(...)
instead.
- XState v5 beta
- XState v4
// β
let previousSnapshot = actor.getSnapshot();
actor.subscribe((snapshot) => {
doSomeComparison(previousSnapshot, snapshot);
previousSnapshot = snapshot;
});
// β DEPRECATED
actor.subscribe((state) => {
doSomeComparison(state.history, state);
});
Pre-migration tip: Update v4 projects to track history using actor.subscribe()
.
Actorsβ
Use actor logic creators for invoke.src
instead of functionsβ
The available actor logic creators are:
createMachine
fromPromise
fromObservable
fromEventObservable
fromTransition
fromCallback
See Actors for more information.
- XState v5 beta
- XState v4
// β
import { fromPromise, createMachine } from 'xstate';
const machine = createMachine({
invoke: {
src: fromPromise(async ({ input }) => {
const data = await getData(input.userId);
// ...
return data;
}),
input: ({ context, event }) => ({
userId: context.userId,
}),
},
});
// β DEPRECATED
import { createMachine } from 'xstate';
const machine = createMachine({
invoke: {
src: (context) => async () => {
const data = await getData(context.userId);
// ...
return data;
},
},
});
- XState v5 beta
- XState v4
// β
import { fromCallback, createMachine } from 'xstate';
const machine = createMachine({
invoke: {
src: fromCallback(({ sendBack, receive, input }) => {
// ...
}),
input: ({ context, event }) => ({
userId: context.userId,
}),
},
});
// β DEPRECATED
import { createMachine } from 'xstate';
const machine = createMachine({
invoke: {
src: (context, event) => (sendBack, receive) => {
// context.userId
// ...
},
},
});
- XState v5 beta
- XState v4
// β
import { fromEventObservable, createMachine } from 'xstate';
import { interval, mapTo } from 'rxjs';
const machine = createMachine({
invoke: {
src: fromEventObservable(() =>
interval(1000).pipe(mapTo({ type: 'tick' })),
),
},
});
// β DEPRECATED
import { createMachine } from 'xstate';
import { interval, mapTo } from 'rxjs';
const machine = createMachine({
invoke: {
src: () => interval(1000).pipe(mapTo({ type: 'tick' })),
},
});
Use invoke.input
instead of invoke.data
β
The invoke.data
property is removed. If you want to provide context to invoked actors, use invoke.input
:
- XState v5 beta
- XState v4
// β
const someActor = createMachine({
// The input must be consumed by the invoked actor:
context: ({ input }) => input,
// ...
});
const machine = createMachine({
// ...
invoke: {
src: 'someActor',
input: {
value: 42,
},
},
});
// β DEPRECATED
const someActor = createMachine({
// ...
});
const machine = createMachine({
// ...
invoke: {
src: 'someActor',
data: {
value: 42,
},
},
});
Use output
in final states instead of data
β
To produce output data from a machine which reached its final state, use the output
property instead of data
:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
},
},
output: {
answer: 42,
},
});
// β DEPRECATED
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
data: {
answer: 42,
},
},
},
});
Don't use property mappers in input
or output
β
If you want to provide dynamic context to invoked actors, or produce dynamic output from final states, use a function instead of an object with property mappers.
- XState v5 beta
- XState v4
// β
const machine = createMachine({
// ...
invoke: {
src: 'someActor',
input: ({ context, event }) => ({
value: event.value,
}),
},
});
// The input must be consumed by the invoked actor:
const someActor = createMachine({
// ...
context: ({ input }) => input,
});
// Producing machine output
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
},
},
output: ({ context, event }) => ({
answer: context.value,
}),
});
// β DEPRECATED
const machine = createMachine({
// ...
invoke: {
src: 'someActor',
data: {
value: (context, event) => event.value, // a property mapper
},
},
});
// Producing machine output
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
data: {
answer: (context, event) => context.value, // a property mapper
},
},
},
});
Use actors
property on options
object instead of services
β
services
have been renamed to actors
:
- XState v5 beta
- XState v4
// β
const specificMachine = machine.provide({
actions: {
/* ... */
},
guards: {
/* ... */
},
actors: {
/* ... */
},
// ...
});
// β DEPRECATED
const specificMachine = machine.withConfig({
actions: {
/* ... */
},
guards: {
/* ... */
},
services: {
/* ... */
},
// ...
});
Use subscribe()
for changes, not onTransition()
β
The actor.onTransition(...)
method is removed. Use actor.subscribe(...)
instead.
- XState v5 beta
- XState v4
// β
const actor = createActor(machine);
actor.subscribe((state) => {
// ...
});
// β DEPRECATED
const actor = interpret(machine);
actor.onTransition((state) => {
// ...
});
createActor()
(formerly interpret()
) accepts a second argument to restore stateβ
interpret(machine).start(state)
is now createActor(machine, { state }).start()
To restore an actor at a specific state, you should now pass the state as the state
property of the options
argument of createActor(logic, options)
. The actor.start()
property no longer takes in a state
argument.
- XState v5 beta
- XState v4
// β
const actor = createActor(machine, { state: someState });
actor.start();
// β DEPRECATED
const actor = interpret(machine);
actor.start(someState);
Use actor.getSnapshot()
to get actorβs stateβ
Subscribing to an actor (actor.subscribe(...)
) after the actor has started will no longer emit the current snapshot immediately. Instead, read the current snapshot from actor.getSnapshot()
:
- XState v5 beta
- XState v4
// β
const actor = createActor(machine);
actor.start();
const initialState = actor.getSnapshot();
actor.subscribe((state) => {
// Snapshots from when the subscription was created
// Will not emit the current snapshot until a transition happens
});
// β DEPRECATED
const actor = interpret(machine);
actor.start();
actor.subscribe((state) => {
// Current snapshot immediately emitted
});
Loop over events instead of using actor.batch()
β
The actor.batch([...])
method for batching events is removed.
- XState v5 beta
- XState v4
// β
for (const event of events) {
actor.send(event);
}
// β DEPRECATED
actor.batch(events);
Pre-migration tip: Update v4 projects to loop over events to send them as a batch.
Use snapshot.status === 'done'
instead of snapshot.done
β
The snapshot.done
property, which was previously in the snapshot object of state machine actors, is removed. Use snapshot.status === 'done'
instead, which is available to all actors:
- XState v5 beta
- XState v4
// β
const actor = createActor(machine);
actor.start();
actor.subscribe((snapshot) => {
if (snapshot.status === 'done') {
// ...
}
});
// β DEPRECATED
const actor = interpret(machine);
actor.start();
actor.subscribe((state) => {
if (state.done) {
// ...
}
});
TypeScriptβ
Use types
instead of schema
β
The machineConfig.schema
property is renamed to machineConfig.types
:
- XState v5 beta
- XState v4
// β
const machine = createMachine({
types: {} as {
context: {
/* ...*/
};
events: {
/* ...*/
};
},
});
// β DEPRECATED
const machine = createMachine({
schema: {} as {
context: {
/* ...*/
};
events: {
/* ...*/
};
},
});
Use types.typegen
instead of tsTypes
β
The machineConfig.tsTypes
property has been renamed and is now at machineConfig.types.typegen
.
- XState v5 beta
- XState v4
// β
const machine = createMachine({
types: {} as {
typegen: {};
context: {
/* ...*/
};
events: {
/* ...*/
};
},
});
// β DEPRECATED
const machine = createMachine({
tsTypes: {};
schema: {} as {
context: {
/* ...*/
};
events: {
/* ...*/
};
},
});
New featuresβ
List coming soon
Timelineβ
XState V5 is in beta, so thereβs still work to be done. We anticipate the tools will be fully compatible with XState V5 beta by summer/fall 2023, with XState V5 stable being released by the end of 2023.
When will XState V5 be out of beta?β
We hope to release XState V5 as a stable major release this year (2023) but canβt provide any exact estimates. The rough timeline is:
- Get early feedback on XState V5 beta (
xstate@beta
) - Ensure all related packages (e.g.
@xstate/react
,@xstate/vue
) are fully compatible with XState V5 - Support V5 in typegen,
@xstate/inspect
, and related tools - Support V5 (importing & exporting) in Stately Studio
- Release candidates
- Release XState V5 stable
Upvote or comment on XState V5 in our roadmap to be the first to find out when V5 is released.
When will Stately Studio be compatible with XState V5?β
We are currently working on Stately Studio compatibility with XState V5. Exporting to XState V5 (JavaScript or TypeScript) will happen soonest, followed by support for new XState V5 features, such as higher-order guards, partial event wildcards, and machine input/output.
Upvote or comment on Stately Studio + XState V5 compatibility in our roadmap to stay updated on our progress.
When will the XState VS Code extension be compatible with XState V5?β
The XState VS Code extension is not yet compatible with XState v5. The extension is a priority for us, and work is already underway.
Upvote or comment on XState V5 compatibility for VS Code extension in our roadmap to stay updated on our progress.
When will XState V5 have typegen?β
Typegen automatically generates intelligent typings for XState and is not yet compatible with XState v5. Weβre working on Typegen alongside the VS Code extension.
Upvote or comment on Typegen for XState V5 in our roadmap to stay updated on our progress.