History states
When using statecharts, sometimes you’ll want to relaunch a process in a previous state.
In the example below, when you turn the fan off via POWER_OFF
, then turn it back on, it will always start in the lowPower
state.
import { createMachine } from 'xstate';
const fanMachine = createMachine({
initial: 'powerOn',
states: {
powerOn: {
on: {
TURN_OFF: {
target: 'powerOff',
},
SET_TO_LOW_POWER: {
target: '.lowPower',
},
SET_TO_MEDIUM_POWER: {
target: '.mediumPower',
},
SET_TO_HIGH_POWER: {
target: '.highPower',
},
},
initial: 'lowPower',
states: {
lowPower: {},
mediumPower: {},
highPower: {},
},
},
powerOff: {
on: {
TURN_ON: {
target: 'powerOn',
},
},
},
},
});
The example above isn’t a great user experience. Ideally, the fan would start with the same power level that the user last selected.
We can use a history state inside the powerOn
state to enable that behavior. A history state, when reached, tells the machine to go to the last recorded child of its parent. In our case, when the machine returns to powerOn
, it will select lowPower
, mediumPower
or highPower
.
import { createMachine } from 'xstate';
const fanMachine = createMachine({
initial: 'powerOn',
states: {
powerOn: {
on: {
TURN_OFF: {
target: 'powerOff',
},
SET_TO_LOW_POWER: {
target: '.lowPower',
},
SET_TO_MEDIUM_POWER: {
target: '.mediumPower',
},
SET_TO_HIGH_POWER: {
target: '.highPower',
},
},
initial: 'lowPower',
states: {
hist: {
type: 'history',
},
lowPower: {},
mediumPower: {},
highPower: {},
},
},
powerOff: {
on: {
TURN_ON: {
/**
* Target the history node directly
*/
target: 'powerOn.hist',
},
},
},
},
});
In the example above, we’ve changed the target of TURN_ON
to target powerOn.hist
. You need to target the history node directly. If you targeted powerOn
instead of the history node, TURN_ON
would default to powerOn
's initial state, lowPower
.
Avoid infinite loops with the history state
A history state can’t be specified as its parent’s initial state as this will result in an infinite loop.
Types of history state
You can specify two types of history state:
shallow
, only the state at the same level as the history state node will be remembered.deep
, all the state’s children will also be remembered.
You can specify these types on the state node itself by specifying history: 'deep'
.
{
type: 'history',
history: 'deep',
}
The default behavior of the history state is shallow
, but deep
is useful when you want to remember a complex state.
In the example below, the user can hide/show their video AND mute/unmute their microphone. When they leave the call to go to the notOnCall
state, they can then rejoin the call via JOIN_CALL
, which targets onCall.hist
.
import { createMachine } from 'xstate';
const callMachine = createMachine({
initial: 'onCall',
states: {
onCall: {
type: 'parallel',
on: {
LEAVE_CALL: 'notOnCall',
},
states: {
hist: {
type: 'history',
history: 'deep',
},
microphone: {
initial: 'muted',
states: {
muted: {
on: {
UNMUTE: 'notMuted',
},
},
notMuted: {
on: {
MUTE: 'muted',
},
},
},
},
video: {
initial: 'noVideo',
states: {
noVideo: {
on: {
SHOW_VIDEO: 'hasVideo',
},
},
hasVideo: {
on: {
HIDE_VIDEO: 'noVideo',
},
},
},
},
},
},
notOnCall: {
on: {
JOIN_CALL: 'onCall.hist',
},
},
},
});
In the example above, the deep
history state tracks:
- Whether
video
is in thenoVideo
orhasVideo
state - Whether
microphone
is in themuted
orunmuted
state.
Using the deep
history state here means the user’s settings are automatically retained when they rejoin the call.
The above example doesn't work with a shallow
history as shallow
only remembers one level deep, which means the muted
/unmuted
state would not be preserved.