Always
Sometimes you’ll need to make checks in your statechart’s current state without receiving an event. You can do this with an eventless transition.
Eventless transitions are transitions without events. These transitions are always taken after any transition in their state if enabled. Eventless transitions are labeled “always” and often referred to as “always” transitions.
A simple example is a state that always transitions from a
to b
:
import { createMachine } from 'xstate';
const machine = createMachine({
initial: 'a',
states: {
a: {
always: [
{
target: 'b',
},
],
},
b: {},
},
});
Using the always
transition means that a
will instantly transition to b
when the machine enters the a
state.
Adding guards
You can also pair always
transitions with a guard:
import { createMachine } from 'xstate';
const machine = createMachine(
{
initial: 'a',
states: {
a: {
always: [
{
cond: 'shouldTransition',
target: 'b',
},
],
},
b: {},
},
},
{
guards: {
shouldTransition: (ctx) => ctx.user.role === 'admin',
},
}
);
In the example above, the transition will only happen when shouldTransition
returns true. Otherwise, the machine will stay in the same state.
“Always” transitions are checked immediately when the machine enters the state node, after checking for regular transitions and before checking if there are any transitions for any other queued events.
Reducing duplication
“Always” transitions are extremely useful for reducing duplication in guards, along with other uses.
Example without always
:
import { createMachine, assign } from 'xstate';
const gameMachine = createMachine(
{
initial: 'playing',
context: {
points: 0,
},
states: {
playing: {
on: {
AWARD_POINTS: [
{
cond: 'didPlayerWin',
actions: 'awardPoints',
target: 'win',
},
{
cond: 'didPlayerLose',
actions: 'awardPoints',
target: 'lose',
},
{
actions: 'awardPoints',
},
],
MAKE_MOVE: [
{
cond: 'didPlayerWin',
actions: 'doMove',
target: 'win',
},
{
cond: 'didPlayerLose',
actions: 'doMove'
target: 'lose',
},
{
actions: 'doMove'
},
],
},
},
win: {},
lose: {},
},
},
{
actions: {
awardPoints: assign({
points: (context) => context.points + 100,
}),
makeMove: assign({
points: (context) => context.points + Math.floor(Math.random() * 10),
}),
},
guards: {
didPlayerWin: (context, event) => {
return context.points > 99;
},
didPlayerLose: (context, event) => {
return context.points < 0;
},
},
}
);
Example with always
:
import { createMachine, assign } from 'xstate';
const gameMachine = createMachine(
{
initial: 'playing',
context: {
points: 0,
},
states: {
playing: {
always: [
{ target: 'win', cond: 'didPlayerWin' },
{ target: 'lose', cond: 'didPlayerLose' },
],
on: {
AWARD_POINTS: {
actions: 'awardPoints',
},
MAKE_MOVE: {
actions: 'makeMove',
},
},
},
win: {},
lose: {},
},
},
{
actions: {
awardPoints: assign({
points: (context) => context.points + 100,
}),
makeMove: assign({
points: (context) => context.points + Math.floor(Math.random() * 10),
}),
},
guards: {
didPlayerWin: (context, event) => {
return context.points > 99;
},
didPlayerLose: (context, event) => {
return context.points < 0;
},
},
}
);
Beware of infinite loops
Since unguarded “always” transitions always run, you should be careful not to create an infinite loop.
Let’s revisit our initial example and add a transition back to a
from b
. Here we add it using an “always” transition, which is not good. This machine will run forever and just keep transitioning between the two states.
import { createMachine } from 'xstate';
/* 🚨 Don't do this at home 🚨 */
const infiniteLoopMachine = createMachine({
initial: 'a',
states: {
a: {
always: [
{
target: 'b',
},
],
},
b: {
always: [
{
target: 'a',
},
],
},
},
});