# States
A state is an abstract representation of a system (such as an application) at a specific point in time. As an application is interacted with, events cause it to change state. A finite state machine can be in only one of a finite number of states at any given time. The current state of a machine is represented by a State
instance:
const lightMachine = Machine({
id: 'light',
initial: 'green',
states: {
green: {
/* ... */
}
// ...
}
});
console.log(lightMachine.initialState);
// State {
// value: 'green',
// actions: [],
// context: undefined,
// // ...
// }
console.log(lightMachine.transition('yellow', '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 4.7.1
It 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 that you can use for a better development experience:
# state.matches(parentStateValue)
This method determines whether the current state.value
is a subset of the given parentStateValue
; that is, if it "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
This specifies the next events that will cause a transition from the current state:
const { initialState } = lightMachine;
console.log(initialState.nextEvents);
// => ['TIMER', 'EMERGENCY']
This 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
This 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, 'TIMER');
console.log(nextState.changed);
// => true
const unchangedState = lightMachine.transition(nextState, 'UNKNOWN_EVENT');
console.log(unchangedState.changed);
// => false
# state.done
This specifies whether the state
is a "final state" - that 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 = Machine({
initial: 'unanswered',
states: {
unanswered: {
on: {
ANSWER: 'answered'
}
},
answered: {
type: 'final'
}
}
});
const { initialState } = answeringMachine;
initialState.done; // false
const answeredState = answeringMachine.transition(initialState, 'ANSWER');
answeredState.done; // true
# state.toStrings()
This 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']
This is useful for representing the current state in string-based environments, such as in CSS classes or data-attributes.
# state.children
This is an object mapping spawned service/actor IDs to their instances. See 📖 Referencing Services for more details.
Example:
const machine = Machine({
// ...
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();
# 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 and resolved using the public machine.resolveState(...)
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);
// Use machine.resolveState() to resolve the state definition to a new State instance relative to the machine
const resolvedState = myMachine.resolveState(previousState);
You can then interpret the machine from this resolved 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(resolvedState);
This will also maintain and restore previous history states and ensures that .events
and .nextEvents
represent the correct values.
# 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 = Machine({
id: 'fetch',
initial: 'idle',
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
after: {
3000: 'failure.timeout'
},
on: {
RESOLVE: 'success',
REJECT: 'failure',
TIMEOUT: '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 and look like this:
const failureTimeoutState = fetchMachine.transition('loading', 'TIMEOUT');
console.log(failureTimeoutState.meta);
// => {
// failure: {
// alert: 'Uh oh.'
// },
// 'failure.timeout': {
// message: 'The request timed out.'
// }
// }
TIP: Aggregating Meta Data
It's up to you for what you want to do with this meta data. Ideally, it should contain JSON-serializable values only. You might want to merge/aggregate the meta data differently; for instance, this 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', 'TIMEOUT');
console.log(mergeMeta(failureTimeoutState.meta));
// => {
// alert: 'Uh oh.',
// message: 'The request timed out.'
// }
# Notes
- You should seldom (never) have to create a
State
instance manually. TreatState
as a read-only object that comes frommachine.transition(...)
orservice.onTransition(...)
only. - To prevent memory leaks,
state.history
will not retain its history; that is,state.history.history === undefined
. Otherwise, we're creating a huge linked list and reinventing blockchain, which I really don't care to do.- This behavior may be configurable in future versions.
← Machines State Nodes →