# Delayed events and 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 delayed (after) transitions in XState (opens new window) as well as a no-code introduction to delayed transitions (opens new window) in our new docs.
Delays and timeouts can be handled declaratively with statecharts. To learn more, see the section in our introduction to statecharts.
# Delayed transitions
Transitions can be taken automatically after a delay. This is represented in a state definition in the after
property, which maps millisecond delays to their transitions:
const lightDelayMachine = createMachine({
id: 'lightDelay',
initial: 'green',
states: {
green: {
after: {
// after 1 second, transition to yellow
1000: { target: 'yellow' }
}
},
yellow: {
after: {
// after 0.5 seconds, transition to red
500: { target: 'red' }
}
},
red: {
after: {
// after 2 seconds, transition to green
2000: { target: 'green' }
}
}
}
});
Delayed transitions can be specified in the same way that you specify them on the on: ...
property. They can be explicit:
// ...
states: {
green: {
after: {
1000: { target: 'yellow' }
}
}
}
// ...
Delayed transitions can also be conditional with regard to a single delay value:
// ...
states: {
green: {
after: {
1000: [
{ target: 'yellow', cond: 'trafficIsLight' },
{ target: 'green' } // reenter 'green' state
]
}
}
}
// ...
Or delayed transitions can be conditional for multiple delays. The first selected delayed transition will be taken, which will prevent later transitions from being taken. In the following example, if the 'trafficIsLight'
condition is true
, then the later 2000: 'yellow'
transition will not be taken:
// ...
states: {
green: {
after: {
1000: { target: 'yellow', cond: 'trafficIsLight' },
2000: { target: 'yellow' } // always transition to 'yellow' after 2 seconds
}
}
}
// ...
Conditional delayed transitions can also be specified as an array:
// ...
states: {
green: {
after: [
{ delay: 1000, target: 'yellow', cond: 'trafficIsLight' },
{ delay: 2000, target: 'yellow' }
];
}
}
// ...
# Delay expressions on transitions 4.4+
Delayed transitions specified on the after: { ... }
property can have dynamic delays, specified either by a string delay reference:
const lightDelayMachine = createMachine(
{
id: 'lightDelay',
initial: 'green',
context: {
trafficLevel: 'low'
},
states: {
green: {
after: {
// after 1 second, transition to yellow
LIGHT_DELAY: { target: 'yellow' }
}
},
yellow: {
after: {
YELLOW_LIGHT_DELAY: { target: 'red' }
}
}
// ...
}
},
{
// String delays configured here
delays: {
LIGHT_DELAY: (context, event) => {
return context.trafficLevel === 'low' ? 1000 : 3000;
},
YELLOW_LIGHT_DELAY: 500 // static value
}
}
);
Or directly by a function, just like conditional delayed transitions:
// ...
green: {
after: [
{
delay: (context, event) => {
return context.trafficLevel === 'low' ? 1000 : 3000;
},
target: 'yellow'
}
]
},
// ...
However, prefer using string delay references, just like the first example, or in the delay
property:
// ...
green: {
after: [
{
delay: 'LIGHT_DELAY',
target: 'yellow'
}
]
},
// ...
# Delayed events
If you just want to send an event after a delay, you can specify the delay
as an option in the second argument of the send(...)
action creator:
import { actions } from 'xstate';
const { send } = actions;
// action to send the 'TIMER' event after 1 second
const sendTimerAfter1Second = send({ type: 'TIMER' }, { delay: 1000 });
You can also prevent those delayed events from being sent by canceling them. This is done with the cancel(...)
action creator:
import { actions } from 'xstate';
const { send, cancel } = actions;
// action to send the 'TIMER' event after 1 second
const sendTimerAfter1Second = send(
{ type: 'TIMER' },
{
delay: 1000,
id: 'oneSecondTimer' // give the event a unique ID
}
);
const cancelTimer = cancel('oneSecondTimer'); // pass the ID of event to cancel
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
entry: sendTimerAfter1Second,
on: {
TIMER: { target: 'active' },
CANCEL: { actions: cancelTimer }
}
},
active: {}
}
});
// if the CANCEL event is sent before 1 second, the TIMER event will be canceled.
# Delay Expressions 4.3+
The delay
option can also be evaluated as a delay expression, which is a function that takes in the current context
and event
that triggered the send()
action, and returns the resolved delay
(in milliseconds):
const dynamicDelayMachine = createMachine({
id: 'dynamicDelay',
context: {
initialDelay: 1000
},
initial: 'idle',
states: {
idle: {
on: {
ACTIVATE: { target: 'pending' }
}
},
pending: {
entry: send(
{ type: 'FINISH' },
{
// delay determined from custom event.wait property
delay: (context, event) => context.initialDelay + event.wait || 0
}
),
on: {
FINISH: { target: 'finished' }
}
},
finished: { type: 'final' }
}
});
const dynamicDelayService = interpret(dynamicDelayMachine);
dynamicDelayService.subscribe({ complete: () => console.log('done!') });
dynamicDelayService.start();
dynamicDelayService.send({
type: 'ACTIVATE',
// arbitrary property
wait: 2000
});
// after 3000ms (1000 + 2000), console will log:
// => 'done!'
# Interpretation
With the XState interpreter, delayed actions will use the nativesetTimeout
and clearTimeout
functions:
import { interpret } from 'xstate';
const service = interpret(lightDelayMachine).onTransition((state) =>
console.log(state.value)
);
service.start();
// => 'green'
// (after 1 second)
// => 'yellow'
For testing, the XState interpreter provides a SimulatedClock
:
import { interpret } from 'xstate';
// import { SimulatedClock } from 'xstate/lib/interpreter'; // < 4.6.0
import { SimulatedClock } from 'xstate/lib/SimulatedClock'; // >= 4.6.0
const simulatedClock = new SimulatedClock();
const service = interpret(lightDelayMachine, {
clock: simulatedClock
}).onTransition((state) => console.log(state.value));
service.start();
// => 'green'
// move the SimulatedClock forward by 1 second
simulatedClock.increment(1000);
// => 'yellow'
You can create your own “clock” to provide to the interpreter. The clock interface is an object with two functions/methods:
setTimeout
- same arguments aswindow.setTimeout(fn, timeout)
clearTimeout
- same arguments aswindow.clearTimeout(id)
# Behind the scenes
The after: ...
property does not introduce anything new to statechart semantics. Instead, it creates normal transitions that look like this:
// ...
states: {
green: {
entry: [
send({ type: after(1000, 'light.green') }, { delay: 1000 }),
send({ type: after(2000, 'light.green') }, { delay: 2000 })
],
onExit: [
cancel(after(1000, 'light.green')),
cancel(after(2000, 'light.green'))
],
on: {
[after(1000, 'light.green')]: {
target: 'yellow',
cond: 'trafficIsLight'
},
[after(2000, 'light.green')]: {
target: 'yellow'
}
}
}
}
// ...
The interpreted statechart will send(...)
the after(...)
events after their delay
, unless the state node is exited, which will cancel(...)
those delayed send(...)
events.