Skip to content
Version: XState v4

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 the noVideo or hasVideo state
  • Whether microphone is in the muted or unmuted 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.