Context (Extended State)
While finite states are well-defined in finite state machines and statecharts, state that represents quantitative data (e.g., arbitrary strings, numbers, objects, etc.) that can be potentially infinite is represented as extended state instead. This makes statecharts much more useful for real-life applications.
In XState, extended state is known as context. Below is an example of how context
is used to simulate filling a glass of water:
import { Machine, actions } from 'xstate';
const { assign } = actions;
// Action to increment the context amount
const addWater = assign({
amount: (ctx, event) => ctx.amount + 1
});
// Guard to check if the glass is full
function glassIsFull(ctx, event) {
return ctx.amount >= 10;
}
const glassMachine = Machine({
id: 'glass',
// the initial context (extended state) of the statechart
context: {
amount: 0
},
initial: 'empty',
states: {
empty: {
on: {
FILL: {
target: 'filling',
actions: 'addWater'
}
}
},
filling: {
on: {
'': {
target: 'full',
cond: 'glassIsFull'
},
FILL: {
target: 'filling',
actions: 'addWater'
}
}
},
full: {}
}
}, {
actions: { addWater },
guards: { glassIsFull }
});
The current context is referenced on the State
as state.context
:
const nextState = glassMachine
.transition(glassMachine.initialState, 'FILL');
nextState.context;
// => { count: 1 }
Initial context
The initial context is specified on the context
property of the Machine
:
const counterMachine = Machine({
id: 'counter',
// initial context
context: {
count: 0,
message: 'Currently empty',
user: {
name: 'David'
},
allowedToIncrement: true,
// ... etc.
},
states: {
// ...
}
});
For dynamic context
, you can either provide the context in the third argument of Machine(...)
(for new machines):
// retrieved dynamically
const dynamicContext = { count: 42 };
const counterMachine = Machine({
id: 'counter',
// ...
}, {
actions: { /* ... */ }
// ... machine options
}, dynamicContext); // provide dynamic context as 3rd argument
Or for existing machines, machine.withContext(...)
should be used:
const counterMachine = Machine({ /* ... */ });
// retrieved dynamically
const dynamicContext = { count: 42 };
const dynamicCounterMachine = counterMachine
.withContext(dynamicContext);
The initial context of a machine can be retrieved from its initial state:
dynamicCounterMachine.initialState.context;
// => { count: 42 }
assign
Updating context with
The assign()
action is used to update the machine's context
. It takes the context "updater", which represents how the current context should be updated.
The "updater" can be an object (recommended):
import { Machine, actions } from 'xstate';
const { assign } = actions;
// example: property updater
// ...
actions: assign({
// increment the current count by the event value
count: (ctx, event) => ctx.count + event.value,
// update the message statically (no function needed)
message: 'Count changed'
}),
// ...
Or it can be a function that returns the updated state:
// example: context updater
// ...
// return a partial (or full) updated context
actions: assign((ctx, event) => ({
count: ctx.count + event.value,
message: 'Count changed'
})),
// ...
Both the property updater and context updater function signatures above are given two arguments:
context
(TContext): the current context (extended state) of the machineevent
(EventObject): the event that caused theassign
action
Notes
- Never mutate the machine's
context
externally. Everything happens for a reason, and every context change should happen explicitly due to an event. - Prefer the object syntax of
assign({ ... })
. This makes it possible for future analysis tools to predict how certain properties can change declaratively. - Assignments can be stacked, and will run sequentially:
// ...
actions: [
assign({ count: 3 }), // ctx.count === 3
assign({ count: ctx => ctx.count * 2 }) // ctx.count === 6
],
// ...
- Just like with
actions
, it's best to representassign()
actions as strings, and then reference them in the machine options:
const countMachine = Machine({
initial: 'start',
context: { count: 0 }
states: {
start: {
onEntry: 'increment'
}
}
}, {
actions: {
increment: assign({ count: ctx => ctx.count + 1 }),
decrement: assign({ count: ctx => ctx.count - 1 })
}
});
- Ideally, the
context
should be representable as a plain JavaScript object; i.e., it should be serializable as JSON.