# Guarded Transitions
Many times, you'll want a transition between states to only take place if certain conditions on the state (finite or extended) or the event are met. For instance, let's say you're creating a machine for a search form, and you only want search to be allowed if:
- the user is allowed to search (
.canSearch
in this example) - the search event
query
is not empty.
This is a good use case for a "guarded transition", which is a transition that only occurs if some condition (cond
) passes. A transition with condition(s) is called a guarded transition.
# Guards (Condition Functions)
A condition function (also known as a guard) specified on the .cond
property of a transition, as a string or condition object with a { type: '...' }
property, and takes 3 arguments:
Argument | Type | Description |
---|---|---|
context | object | the machine context |
event | object | the event that triggered the condition |
condMeta | object | meta data (see below) |
The condMeta
object includes the following properties:
cond
- the original condition objectstate
- the current machine state, before transition_event
- the SCXML event
Returns
true
or false
, which determines whether the transition should be allowed to take place.
const searchValid = (context, event) => {
return context.canSearch && event.query && event.query.length > 0;
};
const searchMachine = createMachine(
{
id: 'search',
initial: 'idle',
context: {
canSearch: true
},
states: {
idle: {
on: {
SEARCH: [
{
target: 'searching',
// Only transition to 'searching' if the guard (cond) evaluates to true
cond: searchValid // or { type: 'searchValid' }
},
{ target: '.invalid' }
]
},
initial: 'normal',
states: {
normal: {},
invalid: {}
}
},
searching: {
entry: 'executeSearch'
// ...
},
searchError: {
// ...
}
}
},
{
guards: {
searchValid // optional, if the implementation doesn't change
}
}
);
Click the EVENTS tab and send an event like { "type": "SEARCH", "query": "something" }
below:
If the cond
guard returns false
, then the transition will not be selected, and no transition will take place from that state node. If all transitions in a child state have guards that evaluate to false
and prevent them from being selected, the event
will be propagated up to the parent state and handled there.
Example of usage with context
:
import { interpret } from 'xstate';
const searchService = interpret(searchMachine)
.onTransition((state) => console.log(state.value))
.start();
searchService.send({ type: 'SEARCH', query: '' });
// => 'idle'
searchService.send({ type: 'SEARCH', query: 'something' });
// => 'searching'
TIP
Guard implementations can be quickly prototyped by specifying the guard cond
function inline, directly in the machine config:
// ...
SEARCH: {
target: 'searching',
cond: (context, event) => context.canSearch && event.query && event.query.length > 0
}
// ...
Refactoring inline guard implementations in the guards
property of the machine options makes it easier to debug, serialize, test, and accurately visualize guards.
# Serializing Guards
Guards can (and should) be serialized as a string or an object with the { type: '...' }
property. The implementation details of the guard are specified on the guards
property of the machine options, where the key
is the guard type
(specified as a string or object) and the value is a function that takes three arguments:
context
- the current machine contextevent
- the event that triggered the (potential) transitionguardMeta
- an object containing meta data about the guard and transition, including:cond
- the originalcond
objectstate
- the current machine state, before transition
Refactoring the above example:
const searchMachine = createMachine(
{
// ...
states: {
idle: {
on: {
SEARCH: {
target: 'searching',
// The 'searchValid' guard implementation details are
// specified in the machine config
cond: 'searchValid' // or { type: 'searchValid' }
}
}
}
// ...
}
},
{
guards: {
searchValid: (context, event) => {
return context.canSearch && event.query && event.query.length > 0;
}
}
}
);
# Custom Guards 4.4+
Sometimes, it is preferable to not only serialize state transitions in JSON, but guard logic as well. This is where serializing guards as objects is helpful, as objects may contain relevant data:
const searchMachine = createMachine(
{
// ...
states: {
idle: {
on: {
SEARCH: {
target: 'searching',
// Custom guard object
cond: {
type: 'searchValid',
minQueryLength: 3
}
}
}
}
// ...
}
},
{
guards: {
searchValid: (context, event, { cond }) => {
// cond === { type: 'searchValid', minQueryLength: 3 }
return (
context.canSearch &&
event.query &&
event.query.length > cond.minQueryLength
);
}
}
}
);
# Multiple Guards
If you want to have a single event transition to different states in certain situations you can supply an array of conditional transitions. Each transition will be tested in order, and the first transition whose cond
guard evaluates to true
will be taken.
For example, you can model a door that listens for an OPEN
event, goes to the 'opened'
state if you are an admin, or goes to the 'closed.error'
state if alert
-ing is true, or goes to the 'closed.idle'
state otherwise.
import { createMachine, actions, interpret, assign } from 'xstate';
const doorMachine = createMachine(
{
id: 'door',
initial: 'closed',
context: {
level: 'user',
alert: false // alert when intrusions happen
},
states: {
closed: {
initial: 'idle',
states: {
idle: {},
error: {}
},
on: {
SET_ADMIN: {
actions: assign({ level: 'admin' })
},
SET_ALARM: {
actions: assign({ alert: true })
},
OPEN: [
// Transitions are tested one at a time.
// The first valid transition will be taken.
{ target: 'opened', cond: 'isAdmin' },
{ target: '.error', cond: 'shouldAlert' },
{ target: '.idle' }
]
}
},
opened: {
on: {
CLOSE: { target: 'closed' }
}
}
}
},
{
guards: {
isAdmin: (context) => context.level === 'admin',
shouldAlert: (context) => context.alert === true
}
}
);
const doorService = interpret(doorMachine)
.onTransition((state) => console.log(state.value))
.start();
// => { closed: 'idle' }
doorService.send({ type: 'OPEN' });
// => { closed: 'idle' }
doorService.send({ type: 'SET_ALARM' });
// => { closed: 'idle' }
// (state does not change, but context changes)
doorService.send({ type: 'OPEN' });
// => { closed: 'error' }
doorService.send({ type: 'SET_ADMIN' });
// => { closed: 'error' }
// (state does not change, but context changes)
doorService.send({ type: 'OPEN' });
// => 'opened'
// (since context.isAdmin === true)
WARNING
The cond
function must always be a pure function that only references the context
and event
arguments.
TIP
Do not overuse guard conditions. If something can be represented discretely as two or more separate events instead of multiple conds
on a single event, it is preferable to avoid cond
and use multiple types of events instead.
# "In State" Guards
The in
property takes a state ID as an argument and returns true
if and only if that state node is active in the current state. For example, we can add a guard to the traffic light machine:
const lightMachine = createMachine({
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' }
}
},
yellow: {
on: {
TIMER: { target: 'red' }
}
},
red: {
initial: 'walk',
states: {
walk: {
/* ... */
},
wait: {
/* ... */
},
stop: {
/* ... */
}
},
on: {
TIMER: [
{
target: 'green',
in: '#light.red.stop'
}
]
}
}
}
});
When an in
-state guard is present with other cond
guards in the same transition, all guards must evaluate to true
for the transition to be taken.
TIP
Using "in state" guards is usually a sign that the machine can be refactored in a way that makes their usage unnecessary. Prefer avoiding "in state" guards when possible.