Guards (Conditional 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 statechart 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 "transition guard", which determines if a transition can occur given the state and the event. A guard is a function defined on the cond
property that takes 2 arguments:
context
- the machine contextevent
- the event, represented as an object
and returns either true
or false
, which signifies whether the transition should be allowed to take place:
import { Machine } from 'xstate';
const searchMachine = Machine({
id: 'search',
initial: 'idle',
context: {
canSearch: true
},
states: {
idle: {
on: {
SEARCH: {
target: 'searching',
// only transition to 'searching' if cond is true
cond: (ctx, event) => {
return ctx.canSearch && event.query && event.query.length > 0;
}
}
}
},
searching: {
onEntry: 'executeSearch'
// ...
},
searchError: {
// ...
}
}
});
If the cond
guard returns false
, then the transition will not be selected, and no transition will take place from that state node.
Example of usage with context:
import { interpret } from 'xstate/lib/interpreter';
const searchService = interpret(searchMachine)
.onTransition(state => console.log(state.value))
.start();
searchService.send({ type: 'SEARCH', query: '' });
// => 'idle'
searchService.send({ type: 'SEARCH', query: 'something' });
// => 'searching'
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.
For example, you can model a door that listens for an OPEN
event, and opens if you are an admin and errors if you are not:
import { Machine, actions } from 'xstate';
import { interpret } from 'xstate/lib/interpreter';
const { assign } = actions;
const doorMachine = Machine({
id: 'door',
initial: 'closed',
context: {
isAdmin: false
},
states: {
closed: {
initial: 'idle',
states: {
idle: {},
error: {}
},
on: {
SET_ADMIN: assign({ isAdmin: true }),
OPEN: [
{ target: 'opened', cond: ctx => ctx.isAdmin },
{ target: 'closed.error' }
]
}
},
opened: {
on: {
CLOSE: 'closed'
}
}
}
});
const doorService = interpret(doorMachine)
.onTransition(state => console.log(state.value))
.start();
// => { closed: 'idle' }
doorService.send('OPEN');
// => { closed: 'error' }
doorService.send('SET_ADMIN');
// => { closed: 'error' }
// (state does not change, but context changes)
doorService.send('OPEN');
// => 'opened'
// (since ctx.isAdmin === true)
Notes:
- The
cond
function must always be a pure function that only references thecontext
andevent
arguments. - ⚠️ Warning: 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 avoidcond
and use multiple types of events instead.