# States
A state is an abstract representation of a system (such as an application) at a specific point in time. To learn more, read the section on states in our introduction to statecharts.
# API
The current state of a machine is represented by a State
instance:
const lightMachine = createMachine({
id: 'light',
initial: 'green',
states: {
green: {
/* ... */
}
// ...
}
});
console.log(lightMachine.initialState);
// State {
// value: 'green',
// actions: [],
// context: undefined,
// // ...
// }
console.log(lightMachine.transition('yellow', { type: 'TIMER' }));
// State {
// value: { red: 'walk' },
// actions: [],
// context: undefined,
// // ...
// }
# State definition
A State
object instance is JSON-serializable and has the following properties:
value
- the current state value (e.g.,{red: 'walk'}
)context
- the current context of this stateevent
- the event object that triggered the transition to this stateactions
- an array of actions to be executedactivities
- a mapping of activities totrue
if the activity started, orfalse
if stopped.history
- the previousState
instancemeta
- any static meta data defined on themeta
property of the state nodedone
- whether the state indicates a final state
The State
object also contains other properties such as historyValue
, events
, tree
and others that are generally not relevant and are used internally.
# State methods and properties
There are some helpful methods and properties you can use for a better development experience:
# state.matches(parentStateValue)
The state.matches(parentStateValue)
method determines whether the current state.value
is a subset of the given parentStateValue
. The method determines if the parent state value “matches” the state value. For example, assuming the current state.value
is { red: 'stop' }
:
console.log(state.value);
// => { red: 'stop' }
console.log(state.matches('red'));
// => true
console.log(state.matches('red.stop'));
// => true
console.log(state.matches({ red: 'stop' }));
// => true
console.log(state.matches('green'));
// => false
TIP
If you want to match one of multiple states, you can use .some()
(opens new window) on an array of state values to accomplish this:
const isMatch = [{ customer: 'deposit' }, { customer: 'withdrawal' }].some(
state.matches
);
# state.nextEvents
state.nextEvents
specifies the next events that will cause a transition from the current state:
const { initialState } = lightMachine;
console.log(initialState.nextEvents);
// => ['TIMER', 'EMERGENCY']
state.nextEvents
is useful in determining which next events can be taken, and representing these potential events in the UI such as enabling/disabling certain buttons.
# state.changed
state.changed
specifies if this state
has changed from the previous state. A state is considered “changed” if:
- Its value is not equal to its previous value, or:
- It has any new actions (side-effects) to execute.
An initial state (with no history) will return undefined
.
const { initialState } = lightMachine;
console.log(initialState.changed);
// => undefined
const nextState = lightMachine.transition(initialState, { type: 'TIMER' });
console.log(nextState.changed);
// => true
const unchangedState = lightMachine.transition(nextState, {
type: 'UNKNOWN_EVENT'
});
console.log(unchangedState.changed);
// => false
# state.done
state.done
specifies whether the state
is a “final state” - a final state is a state that indicates that its machine has reached its final (terminal) state and can no longer transition to any other state.
const answeringMachine = createMachine({
initial: 'unanswered',
states: {
unanswered: {
on: {
ANSWER: { target: 'answered' }
}
},
answered: {
type: 'final'
}
}
});
const { initialState } = answeringMachine;
initialState.done; // false
const answeredState = answeringMachine.transition(initialState, {
type: 'ANSWER'
});
answeredState.done; // true
# state.toStrings()
The state.toStrings()
method returns an array of strings that represent all of the state value paths. For example, assuming the current state.value
is { red: 'stop' }
:
console.log(state.value);
// => { red: 'stop' }
console.log(state.toStrings());
// => ['red', 'red.stop']
The state.toStrings()
method is useful for representing the current state in string-based environments, such as in CSS classes or data-attributes.
# state.children
state.children
is an object mapping spawned service/actor IDs to their instances. See 📖 Referencing Services for more details.
# Example using state.children
const machine = createMachine({
// ...
invoke: [
{ id: 'notifier', src: createNotifier },
{ id: 'logger', src: createLogger }
]
// ...
});
const service = invoke(machine)
.onTransition((state) => {
state.children.notifier; // service from createNotifier()
state.children.logger; // service from createLogger()
})
.start();
# state.hasTag(tag)
Since 4.19.0
The state.hasTag(tag)
method determines whether the current state configuration has a state node with the given tag.
const machine = createMachine({
initial: 'green',
states: {
green: {
tags: 'go' // single tag
},
yellow: {
tags: 'go'
},
red: {
tags: ['stop', 'other'] // multiple tags
}
}
});
For instance, if the above machine is in the green
or yellow
state, instead of matching the state directly using state.matches('green') || state.matches('yellow')
, it is possible to use state.hasTag('go')
:
const canGo = state.hasTag('go');
// => `true` if in 'green' or 'yellow' state
# state.can(event)
Since 4.25.0
The state.can(event)
method determines whether an event
will cause a state change if sent to the interpreted machine. The method will return true
if the state will change due to the event
being sent; otherwise the method will return false
:
const machine = createMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: 'active'
}
},
active: {
on: {
DO_SOMETHING: { actions: ['something'] }
}
}
}
});
const inactiveState = machine.initialState;
inactiveState.can({ type: 'TOGGLE' }); // true
inactiveState.can({ type: 'DO_SOMETHING' }); // false
// Also takes in full event objects:
inactiveState.can({
type: 'DO_SOMETHING',
data: 42
}); // false
const activeState = machine.transition(inactiveState, { type: 'TOGGLE' });
activeState.can({ type: 'TOGGLE' }); // false
activeState.can({ type: 'DO_SOMETHING' }); // true, since an action will be executed
A state is considered “changed” if state.changed
is true
and if any of the following are true:
- its
state.value
changes - there are new
state.actions
to be executed - its
state.context
changes.
# Persisting state
As mentioned, a State
object can be persisted by serializing it to a string JSON format:
const jsonState = JSON.stringify(currentState);
// Example: persisting to localStorage
try {
localStorage.setItem('app-state', jsonState);
} catch (e) {
// unable to save to localStorage
}
State can be restored using the static State.create(...)
method:
import { State, interpret } from 'xstate';
import { myMachine } from '../path/to/myMachine';
// Retrieving the state definition from localStorage, if localStorage is empty use machine initial state
const stateDefinition =
JSON.parse(localStorage.getItem('app-state')) || myMachine.initialState;
// Use State.create() to restore state from a plain object
const previousState = State.create(stateDefinition);
You can then interpret the machine from this state by passing the State
into the .start(...)
method of the interpreted service:
// ...
// This will start the service at the specified State
const service = interpret(myMachine).start(previousState);
This will also maintain and restore previous history states and ensures that .events
and .nextEvents
represent the correct values.
WARNING
Persisting spawned actors isn't yet supported in XState.
# State meta data
Meta data, which is static data that describes relevant properties of any state node, can be specified on the .meta
property of the state node:
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
states: {
idle: {
on: { FETCH: { target: 'loading' } }
},
loading: {
after: {
3000: 'failure.timeout'
},
on: {
RESOLVE: { target: 'success' },
REJECT: { target: 'failure' },
TIMEOUT: { target: 'failure.timeout' } // manual timeout
},
meta: {
message: 'Loading...'
}
},
success: {
meta: {
message: 'The request succeeded!'
}
},
failure: {
initial: 'rejection',
states: {
rejection: {
meta: {
message: 'The request failed.'
}
},
timeout: {
meta: {
message: 'The request timed out.'
}
}
},
meta: {
alert: 'Uh oh.'
}
}
}
});
The current state of the machine collects the .meta
data of all of the state nodes, represented by the state value, and places them on an object where:
- The keys are the state node IDs
- The values are the state node
.meta
values
For instance, if the above machine is in the failure.timeout
state (which is represented by two state nodes with IDs "failure"
and "failure.timeout"
), the .meta
property will combine all .meta
values as follows:
const failureTimeoutState = fetchMachine.transition('loading', {
type: 'TIMEOUT'
});
console.log(failureTimeoutState.meta);
// => {
// failure: {
// alert: 'Uh oh.'
// },
// 'failure.timeout': {
// message: 'The request timed out.'
// }
// }
TIP: Aggregating meta data
What you do with meta data is up to you. Ideally, meta data should contain JSON-serializable values only. Consider merging/aggregating the meta data differently. For example, the following function discards the state node ID keys (if they are irrelevant) and merges the meta data:
function mergeMeta(meta) {
return Object.keys(meta).reduce((acc, key) => {
const value = meta[key];
// Assuming each meta value is an object
Object.assign(acc, value);
return acc;
}, {});
}
const failureTimeoutState = fetchMachine.transition('loading', {
type: 'TIMEOUT'
});
console.log(mergeMeta(failureTimeoutState.meta));
// => {
// alert: 'Uh oh.',
// message: 'The request timed out.'
// }
# Notes
- You should never have to create a
State
instance manually. TreatState
as a read-only object that only comes frommachine.transition(...)
orservice.onTransition(...)
. state.history
will not retain its history in order to prevent memory leaks.state.history.history === undefined
. Otherwise, you end up creating a huge linked list and reinventing blockchain, which we don't care to do.- This behavior may be configurable in future versions.
← Machines State Nodes →