What is context?
Context is extended state data associated with a state machine. While states represent finite modes, context holds quantitative data that can have infinite values.
Think of states as the “mode” of your application (loading, success, error) and context as the “data” (user info, count, items).
Defining context
Define initial context in your machine:
import { createMachine } from 'xstate' ;
const counterMachine = createMachine ({
id: 'counter' ,
initial: 'active' ,
context: {
count: 0 ,
step: 1
},
states: {
active: {
on: {
INCREMENT: {
actions : ({ context }) => {
context . count += context . step ;
}
}
}
}
}
});
TypeScript context
Type your context with setup():
import { setup } from 'xstate' ;
const machine = setup ({
types: {
context: {} as {
count : number ;
user : { name : string ; email : string } | null ;
items : string [];
}
}
}). createMachine ({
context: {
count: 0 ,
user: null ,
items: []
}
});
Updating context
Use the assign action to update context:
import { setup , assign } from 'xstate' ;
const machine = setup ({
types: {
context: {} as { count : number },
events: {} as { type : 'INCREMENT' ; value : number } | { type : 'RESET' }
}
}). createMachine ({
context: { count: 0 },
initial: 'active' ,
states: {
active: {
on: {
INCREMENT: {
actions: assign ({
count : ({ context , event }) => context . count + event . value
})
},
RESET: {
actions: assign ({
count: 0
})
}
}
}
}
});
Assign patterns
Static values
assign ({
status: 'active' ,
count: 0
})
Dynamic values
assign ({
count : ({ context }) => context . count + 1 ,
timestamp : () => Date . now ()
})
Using event data
assign ({
user : ({ event }) => event . userData ,
lastEvent : ({ event }) => event . type
})
Partial updates
assign ({
user : ({ context , event }) => ({
... context . user ,
name: event . newName
})
})
Reading context
Access context from the snapshot:
import { createActor } from 'xstate' ;
const actor = createActor ( counterMachine );
actor . start ();
const snapshot = actor . getSnapshot ();
console . log ( snapshot . context . count ); // 0
actor . send ({ type: 'INCREMENT' , value: 5 });
console . log ( actor . getSnapshot (). context . count ); // 5
Initial context from input
Provide context at runtime:
import { setup } from 'xstate' ;
const machine = setup ({
types: {
context: {} as { userId : string ; count : number },
input: {} as { userId : string }
}
}). createMachine ({
context : ({ input }) => ({
userId: input . userId ,
count: 0
}),
initial: 'active' ,
states: {
active: {}
}
});
const actor = createActor ( machine , {
input: { userId: 'user-123' }
});
actor . start ();
console . log ( actor . getSnapshot (). context . userId ); // 'user-123'
Context in guards
Use context in conditional logic:
import { setup } from 'xstate' ;
const machine = setup ({
types: {
context: {} as { count : number }
},
guards: {
isMaxReached : ({ context }) => context . count >= 10 ,
isPositive : ({ context }) => context . count > 0
}
}). createMachine ({
context: { count: 0 },
initial: 'active' ,
states: {
active: {
on: {
INCREMENT: {
guard: { type: 'not' , guard: 'isMaxReached' },
actions: assign ({
count : ({ context }) => context . count + 1
})
}
}
}
}
});
Context in actions
Access context in action implementations:
import { setup } from 'xstate' ;
const machine = setup ({
types: {
context: {} as { user : { name : string } }
},
actions: {
greetUser : ({ context }) => {
console . log ( `Hello, ${ context . user . name } !` );
},
logCount : ({ context }) => {
console . log ( `Count: ${ context . count } ` );
}
}
}). createMachine ({
context: {
user: { name: 'Guest' },
count: 0
},
initial: 'active' ,
states: {
active: {
entry: 'greetUser'
}
}
});
Context and actors
Pass context to invoked actors:
import { setup , fromPromise } from 'xstate' ;
const fetchUser = fromPromise ( async ({ input } : { input : { userId : string } }) => {
const response = await fetch ( `/api/users/ ${ input . userId } ` );
return response . json ();
});
const machine = setup ({
types: {
context: {} as { userId : string ; userData : any }
},
actors: {
fetchUser
}
}). createMachine ({
context: {
userId: '' ,
userData: null
},
initial: 'idle' ,
states: {
idle: {
on: {
FETCH: 'loading'
}
},
loading: {
invoke: {
src: 'fetchUser' ,
input : ({ context }) => ({ userId: context . userId }),
onDone: {
target: 'success' ,
actions: assign ({
userData : ({ event }) => event . output
})
}
}
},
success: {}
}
});
Immutability
Always treat context as immutable. Create new objects instead of mutating existing ones.
Good - Immutable update
assign ({
items : ({ context }) => [ ... context . items , newItem ]
})
Bad - Mutation
// Don't do this!
assign ({
items : ({ context }) => {
context . items . push ( newItem );
return context . items ;
}
})
Using Immer
For complex updates, use Immer with @xstate/immer:
import { createMachine } from 'xstate' ;
import { assign } from '@xstate/immer' ;
const machine = createMachine ({
context: {
user: {
profile: {
settings: {
notifications: true
}
}
}
},
initial: 'active' ,
states: {
active: {
on: {
TOGGLE_NOTIFICATIONS: {
actions: assign (( context ) => {
// Mutate with Immer - it creates immutable update
context . user . profile . settings . notifications =
! context . user . profile . settings . notifications ;
})
}
}
}
}
});
Best practices
Keep context minimal. Only store data that affects machine behavior or needs to be persisted.
Use TypeScript to type your context. This prevents errors and improves developer experience.
Avoid storing derived data in context. Calculate it from existing context when needed.
Separate concerns: use states for modes/phases, context for data.
Example: Todo list
import { setup , assign } from 'xstate' ;
interface Todo {
id : string ;
text : string ;
completed : boolean ;
}
const todoMachine = setup ({
types: {
context: {} as {
todos : Todo [];
filter : 'all' | 'active' | 'completed' ;
},
events: {} as
| { type: 'ADD' ; text : string }
| { type: 'TOGGLE' ; id : string }
| { type: 'DELETE' ; id : string }
| { type: 'SET_FILTER' ; filter : 'all' | 'active' | 'completed' }
}
}). createMachine ({
context: {
todos: [],
filter: 'all'
},
initial: 'active' ,
states: {
active: {
on: {
ADD: {
actions: assign ({
todos : ({ context , event }) => [
... context . todos ,
{
id: Math . random (). toString (),
text: event . text ,
completed: false
}
]
})
},
TOGGLE: {
actions: assign ({
todos : ({ context , event }) =>
context . todos . map ( todo =>
todo . id === event . id
? { ... todo , completed: ! todo . completed }
: todo
)
})
},
DELETE: {
actions: assign ({
todos : ({ context , event }) =>
context . todos . filter ( todo => todo . id !== event . id )
})
},
SET_FILTER: {
actions: assign ({
filter : ({ event }) => event . filter
})
}
}
}
}
});
Next steps
Assign action Full assign action reference
TypeScript Type-safe state machines
Actions Learn about actions
Guards Use context in guards