# Transitions
These XState v4 docs are no longer maintained
XState v5 is out now! Read more about XState v5 (opens new window)
🆕 Find more about transitions in XState (opens new window) as well as a no-code introduction to transitions (opens new window).
Transitions define how the machine reacts to events. To learn more, see the section in our introduction to statecharts.
# API
State transitions are defined on state nodes, in the on
property:
import { createMachine } from 'xstate';
const promiseMachine = createMachine({
id: 'promise',
initial: 'pending',
states: {
pending: {
on: {
// state transition (shorthand)
// this is equivalent to { target: 'resolved' }
RESOLVE: 'resolved',
// state transition (object)
REJECT: {
target: 'rejected'
}
}
},
resolved: {
type: 'final'
},
rejected: {
type: 'final'
}
}
});
const { initialState } = promiseMachine;
console.log(initialState.value);
// => 'pending'
const nextState = promiseMachine.transition(initialState, { type: 'RESOLVE' });
console.log(nextState.value);
// => 'resolved'
In the above example, when the machine is in the pending
state and it receives a RESOLVE
event, it will transition to the resolved
state.
A state transition can be defined as:
- a string, e.g.,
RESOLVE: 'resolved'
, which is equivalent to... - an object with a
target
property, e.g.,RESOLVE: { target: 'resolved' }
, - an array of transition objects, which are used for conditional transitions (see guards)
# Machine .transition
Method
As seen above, the machine.transition(...)
method is a pure function that takes two arguments:
It returns a new State
instance, which is the result of taking all the transitions enabled by the current state and event.
const lightMachine = createMachine({
/* ... */
});
const greenState = lightMachine.initialState;
// determine next state based on current state and event
const yellowState = lightMachine.transition(greenState, { type: 'TIMER' });
console.log(yellowState.value);
// => 'yellow'
# Selecting Enabled Transitions
An enabled transition is a transition that will be taken conditionally, based upon the current state and event. It will be taken if and only if:
- it is defined on a state node that matches the current state value
- the transition guard (
cond
property) is satisfied (evaluates totrue
) - it is not superseded by a more specific transition.
In a hierarchical machine, transitions are prioritized by how deep they are in the tree; deeper transitions are more specific and thus have higher priority. This works similar to how DOM events work: if you click a button, the click event handler directly on the button is more specific than a click event handler on the window
.
const wizardMachine = createMachine({
id: 'wizard',
initial: 'open',
states: {
open: {
initial: 'step1',
states: {
step1: {
on: {
NEXT: { target: 'step2' }
}
},
step2: {
/* ... */
},
step3: {
/* ... */
}
},
on: {
NEXT: { target: 'goodbye' },
CLOSE: { target: 'closed' }
}
},
goodbye: {
on: {
CLOSE: { target: 'closed' }
}
},
closed: {
type: 'final'
}
}
});
// { open: 'step1' }
const { initialState } = wizardMachine;
// the NEXT transition defined on 'open.step1'
// supersedes the NEXT transition defined
// on the parent 'open' state
const nextStepState = wizardMachine.transition(initialState, { type: 'NEXT' });
console.log(nextStepState.value);
// => { open: 'step2' }
// there is no CLOSE transition on 'open.step1'
// so the event is passed up to the parent
// 'open' state, where it is defined
const closedState = wizardMachine.transition(initialState, { type: 'CLOSE' });
console.log(closedState.value);
// => 'closed'
# Event Descriptors
An event descriptor is a string describing the event type that the transition will match. Often, this is equivalent to the event.type
property on the event
object sent to the machine:
// ...
{
on: {
// "CLICK" is the event descriptor.
// This transition matches events with { type: 'CLICK' }
CLICK: 'someState',
// "SUBMIT" is the event descriptor.
// This transition matches events with { type: 'SUBMIT' }
SUBMIT: 'anotherState'
}
}
// ...
Other event descriptors include:
- Null event descriptors (
""
), which match no events (i.e., "null" events) and represent transitions taken immediately after the state is entered - Wildcard event descriptors (
"*"
) 4.7+, which match any event if the event is not matched explicitly by any other transition in the state
# Self Transitions
A self-transition is when a state transitions to itself, in which it may exit and then reenter itself. Self-transitions can either be an internal or external transition:
- An internal transition will neither exit nor re-enter itself, but may enter different child states.
- An external transition will exit and re-enter itself, and may also exit/enter child states.
By default, all transitions with a specified target are external.
See actions on self-transitions for more details on how entry/exit actions are executed on self-transitions.
# Internal Transitions
An internal transition is one that does not exit its state node. Internal transitions are created by specifying a relative target (e.g., '.left'
) or by explicitly setting { internal: true }
on the transition. For example, consider a machine that sets a paragraph of text to align 'left'
, 'right'
, 'center'
, or 'justify'
:
import { createMachine } from 'xstate';
const wordMachine = createMachine({
id: 'word',
initial: 'left',
states: {
left: {},
right: {},
center: {},
justify: {}
},
on: {
// internal transitions
LEFT_CLICK: '.left',
RIGHT_CLICK: { target: '.right' }, // same as '.right'
CENTER_CLICK: { target: '.center', internal: true }, // same as '.center'
JUSTIFY_CLICK: { target: '.justify', internal: true } // same as '.justify'
}
});
The above machine will start in the 'left'
state, and based on what is clicked, will internally transition to its other child states. Also, since the transitions are internal, entry
, exit
or any of the actions
defined on the parent state node are not executed again.
Transitions that have { target: undefined }
(or no target
) are also internal transitions:
const buttonMachine = createMachine({
id: 'button',
initial: 'inactive',
states: {
inactive: {
on: { PUSH: 'active' }
},
active: {
on: {
// No target - internal transition
PUSH: {
actions: 'logPushed'
}
}
}
}
});
Summary of internal transitions:
EVENT: '.foo'
- internal transition to childEVENT: { target: '.foo' }
- internal transition to child (starts with'.'
)EVENT: undefined
- forbidden transitionEVENT: { actions: [ ... ] }
- internal self-transitionEVENT: { actions: [ ... ], internal: true }
- internal self-transition, same as aboveEVENT: { target: undefined, actions: [ ... ] }
- internal self-transition, same as above
# External Transitions
External transitions will exit and reenter the state node in which the transition is defined. In the above example, the parent word
state node (the root state node) will have its exit
and entry
actions executed on its transitions.
By default, transitions are external, but any transition can be made external by explicitly setting { internal: false }
on the transition.
// ...
on: {
// external transitions
LEFT_CLICK: 'word.left',
RIGHT_CLICK: 'word.right',
CENTER_CLICK: { target: '.center', internal: false }, // same as 'word.center'
JUSTIFY_CLICK: { target: 'word.justify', internal: false } // same as 'word.justify'
}
// ...
Every transition above is external and will have its exit
and entry
actions of the parent state executed.
Summary of external transitions:
EVENT: { target: 'foo' }
- all transitions to siblings are external transitionsEVENT: { target: '#someTarget' }
- all transitions to other nodes are external transitionsEVENT: { target: 'same.foo' }
- external transition to own child node (equivalent to{ target: '.foo', internal: false }
)EVENT: { target: '.foo', internal: false }
- external transition to child node- This would otherwise be an internal transition
EVENT: { actions: [ ... ], internal: false }
- external self-transitionEVENT: { target: undefined, actions: [ ... ], internal: false }
- external self-transition, same as above
# Transient Transitions
WARNING
The empty string syntax ({ on: { '': ... } }
) will be deprecated in version 5. The new always
syntax in version 4.11+ should be preferred. See below section on eventless transitions, which are the same as transient transitions.
A transient transition is a transition that is enabled by a null event. In other words, it is a transition that is immediately taken (i.e., without a triggering event) as long as any conditions are met:
const gameMachine = createMachine(
{
id: 'game',
initial: 'playing',
context: {
points: 0
},
states: {
playing: {
on: {
// Transient transition
// Will transition to either 'win' or 'lose' immediately upon
// (re)entering 'playing' state if the condition is met.
'': [
{ target: 'win', cond: 'didPlayerWin' },
{ target: 'lose', cond: 'didPlayerLose' }
],
// Self-transition
AWARD_POINTS: {
actions: assign({
points: 100
})
}
}
},
win: { type: 'final' },
lose: { type: 'final' }
}
},
{
guards: {
didPlayerWin: (context, event) => {
// check if player won
return context.points > 99;
},
didPlayerLose: (context, event) => {
// check if player lost
return context.points < 0;
}
}
}
);
const gameService = interpret(gameMachine)
.onTransition((state) => console.log(state.value))
.start();
// Still in 'playing' state because no conditions of
// transient transition were met
// => 'playing'
// When 'AWARD_POINTS' is sent, a self-transition to 'PLAYING' occurs.
// The transient transition to 'win' is taken because the 'didPlayerWin'
// condition is satisfied.
gameService.send({ type: 'AWARD_POINTS' });
// => 'win'
Just like transitions, transient transitions can be specified as a single transition (e.g., '': 'someTarget'
), or an array of conditional transitions. If no conditional transitions on a transient transition are met, the machine stays in the same state.
Null events are always "sent" for every transition, internal or external.
# Eventless ("Always") Transitions 4.11+
An eventless transition is a transition that is always taken when the machine is in the state where it is defined, and when its cond
guard evaluates to true
. They are checked:
- immediately when the state node is entered
- every time the machine receives an actionable event (regardless of whether the event triggers internal or external transition)
Eventless transitions are defined on the always
property of the state node:
const gameMachine = createMachine(
{
id: 'game',
initial: 'playing',
context: {
points: 0
},
states: {
playing: {
// Eventless transition
// Will transition to either 'win' or 'lose' immediately upon
// entering 'playing' state or receiving AWARD_POINTS event
// if the condition is met.
always: [
{ target: 'win', cond: 'didPlayerWin' },
{ target: 'lose', cond: 'didPlayerLose' }
],
on: {
// Self-transition
AWARD_POINTS: {
actions: assign({
points: 100
})
}
}
},
win: { type: 'final' },
lose: { type: 'final' }
}
},
{
guards: {
didPlayerWin: (context, event) => {
// check if player won
return context.points > 99;
},
didPlayerLose: (context, event) => {
// check if player lost
return context.points < 0;
}
}
}
);
const gameService = interpret(gameMachine)
.onTransition((state) => console.log(state.value))
.start();
// Still in 'playing' state because no conditions of
// transient transition were met
// => 'playing'
// When 'AWARD_POINTS' is sent, a self-transition to 'PLAYING' occurs.
// The transient transition to 'win' is taken because the 'didPlayerWin'
// condition is satisfied.
gameService.send({ type: 'AWARD_POINTS' });
// => 'win'
# Eventless vs. wildcard transitions
- Wildcard transitions are not checked on entering state nodes. Eventless transitions are. Guards for eventless transitions are evaluated before doing anything else (even before evaluating guards of entry actions).
- Re-evaluation of eventless transitions is triggered by any actionable event. Re-evaluation of wildcard transitions is triggered only by an event not matched by explicit event descriptors.
WARNING
It is possible to create infinite loops if eventless transitions are misused.
Eventless transitions should be defined either with target
, cond
+ target
, cond
+ actions
, or cond
+ target
+ actions
. Target, if declared, should be different than the current state node. Eventless transitions with no target
nor cond
will cause an infinite loop. Transitions with cond
and actions
may run into an infinite loop if its cond
guard keeps returning true
.
TIP
When eventless transitions are checked, their guards are evaluated repeatedly until all of them return false, or a transition with target is validated. Every time some guard evaluates to true
during this process, its associated actions are going to be executed once. Thus it is possible that during a single microtask some transitions without targets are executed multiple times.
This contrasts with common transitions, where always maximum one transition can be taken.
# Forbidden Transitions
In XState, a "forbidden" transition is one that specifies that no state transition should occur with the specified event. That is, nothing should happen on a forbidden transition, and the event should not be handled by parent state nodes.
A forbidden transition is made by specifying the target
explicitly as undefined
. This is the same as specifying it as an internal transition with no actions:
on: {
// forbidden transition
LOG: undefined,
// same thing as...
LOG: {
actions: []
}
}
For example, we can model that telemetry can be logged for all events except when the user is entering personal information:
const formMachine = createMachine({
id: 'form',
initial: 'firstPage',
states: {
firstPage: {
/* ... */
},
secondPage: {
/* ... */
},
userInfoPage: {
on: {
// explicitly forbid the LOG event from doing anything
// or taking any transitions to any other state
LOG: undefined
}
}
},
on: {
LOG: {
actions: 'logTelemetry'
}
}
});
TIP
Note that when defining multiple transitions with the same event name in a hierarchical ancestor-descendant chain, the most inner transition will exclusively be taken. In the example above, this is why the logTelemetry
action defined in the parent LOG
event won't execute as soon as the machine reaches the userInfoPage
state.
# Multiple Targets
A transition based on a single event can have multiple target state nodes. This is uncommon, and only valid if the state nodes are legal; e.g., a transition to two sibling state nodes in a compound state node is illegal, since a (non-parallel) state machine can only be in one state at any given time.
Multiple targets are specified as an array in target: [...]
, where each target in the array is a relative key or an ID to a state node, just like single targets.
const settingsMachine = createMachine({
id: 'settings',
type: 'parallel',
states: {
mode: {
initial: 'active',
states: {
inactive: {},
pending: {},
active: {}
}
},
status: {
initial: 'enabled',
states: {
disabled: {},
enabled: {}
}
}
},
on: {
// Multiple targets
DEACTIVATE: {
target: ['.mode.inactive', '.status.disabled']
}
}
});
# Wildcard Descriptors 4.7+
A transition that is specified with a wildcard event descriptor ("*"
) is activated by any event. This means that any event will match the transition that has on: { "*": ... }
, and if the guards pass, that transition will be taken.
Explicit event descriptors will always be chosen over wildcard event descriptors, unless the transitions are specified in an array. In that case, the order of the transitions determines which transition gets chosen.
// For SOME_EVENT, the explicit transition to "here" will be taken
on: {
"*": "elsewhere",
"SOME_EVENT": "here"
}
// For SOME_EVENT, the wildcard transition to "elsewhere" will be taken
on: [
{ event: "*", target: "elsewhere" },
{ event: "SOME_EVENT", target: "here" },
]
TIP
Wildcard descriptors do not behave the same way as transient transitions (with null event descriptors). Whereas transient transitions will be taken immediately whenever the state is active, wildcard transitions still need some event to be sent to its state to be triggered.
Example:
const quietMachine = createMachine({
id: 'quiet',
initial: 'idle',
states: {
idle: {
on: {
WHISPER: undefined,
// On any event besides a WHISPER, transition to the 'disturbed' state
'*': 'disturbed'
}
},
disturbed: {}
}
});
quietMachine.transition(quietMachine.initialState, { type: 'WHISPER' });
// => State { value: 'idle' }
quietMachine.transition(quietMachine.initialState, { type: 'SOME_EVENT' });
// => State { value: 'disturbed' }
# FAQ's
# How do I do if/else logic on transitions?
Sometimes, you'll want to say:
- If something is true, go to this state
- If something else is true, go to this state
- Else, go to this state
You can use guarded transitions to achieve this.
# How do I transition to any state?
You can transition to any state by giving that state a custom id, and using target: '#customId'
. You can read the full docs on custom IDs here.
This allows you to transition from child states to siblings of parents, for example in the CANCEL
and done
events in this example: