@xstate/immer
The @xstate/immer package contains utilities for using Immer with XState.
Quick start​
Included in @xstate/immer:
assign()- an Immer action that allows you to immutably assign to machinecontextin a convenient waycreateUpdater()- a useful function that allows you to cohesively define a context update event event creator and assign action, all together. (See an example below)
- Install
immer,xstate,@xstate/immer:
npm install immer xstate @xstate/immer
- Import the Immer utilities:
import { createMachine, interpret } from 'xstate';
import { assign, createUpdater } from '@xstate/immer';
const levelUpdater = createUpdater('UPDATE_LEVEL', (ctx, { input }) => {
ctx.level = input;
});
const toggleMachine = createMachine({
id: 'toggle',
context: {
count: 0,
level: 0,
},
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
// Immutably update context the same "mutable"
// way as you would do with Immer!
actions: assign((ctx) => ctx.count++),
},
},
},
active: {
on: {
TOGGLE: {
target: 'inactive',
},
// Use the updater for more convenience:
[levelUpdater.type]: {
actions: levelUpdater.action,
},
},
},
},
});
const toggleService = interpret(toggleMachine)
.onTransition((state) => {
console.log(state.context);
})
.start();
toggleService.send('TOGGLE');
// { count: 1, level: 0 }
toggleService.send(levelUpdater.update(9));
// { count: 1, level: 9 }
toggleService.send('TOGGLE');
// { count: 2, level: 9 }
toggleService.send(levelUpdater.update(-100));
// Notice how the level is not updated in 'inactive' state:
// { count: 2, level: 9 }
API​
assign(recipe)​
Returns an XState event object that will update the machine’s context to reflect the changes ("mutations") to context made in the recipe function.
The recipe is similar to the function that you would pass to Immer’s produce(val, recipe) function), with the addition that you get the same arguments as a normal XState assigner passed to assign(assigner) (context, event, meta).
Arguments for assign​
| Argument | Type | Description |
|---|---|---|
recipe | function | A function where "mutations" to context are made. See the Immer docs. |
Arguments for recipe​
| Argument | Type | Description |
|---|---|---|
context | any | The context data of the current state |
event | event object | The received event object |
meta | assign meta object | An object containing meta data such as the state, SCXML _event, etc. |
import { createMachine } from 'xstate';
import { assign } from '@xstate/immer';
const userMachine = createMachine({
id: 'user',
context: {
name: null,
address: {
city: null,
state: null,
country: null,
},
},
initial: 'active',
states: {
active: {
on: {
CHANGE_COUNTRY: {
actions: assign((context, event) => {
context.address.country = event.value;
}),
},
},
},
},
});
const { initialState } = userMachine;
const nextState = userMachine.transition(initialState, {
type: 'UPDATE_COUNTRY',
country: 'USA',
});
nextState.context.address.country;
// => 'USA'
createUpdater(eventType, recipe)​
Returns an object that is useful for creating context updaters.
| Argument | Type | Description |
|---|---|---|
eventType | string | The event type for the Immer update event |
recipe | function | A function that takes in the context and an Immer update event object to "mutate" the context |
An Immer update event object is an object that contains:
type: theeventTypespecifiedinput: the "payload" of the update event
The object returned by createUpdater(...) is an updater object containing:
type: theeventTypepassed intocreateUpdater(eventType, ...). This is used for specifying transitions in which the update will occur.action: the assign action object that will update thecontext.update: the event creator that takes in theinputand returns aneventobject with the specifiedeventTypeandinputthat will be passed torecipe(context, event).
import { createMachine } from 'xstate';
import { createUpdater } from '@xstate/immer';
// The second argument is an Immer update event that looks like:
// {
// type: 'UPDATE_NAME',
// input: 'David' // or any string
// }
const nameUpdater = createUpdater('UPDATE_NAME', (context, { input }) => {
context.name = input;
});
const ageUpdater = createUpdater('UPDATE_AGE', (context, { input }) => {
context.age = input;
});
const formMachine = createMachine({
initial: 'editing',
context: {
name: '',
age: null,
},
states: {
editing: {
on: {
// The updater.type can be used directly for transitions
// where the updater.action function will be applied
[nameUpdater.type]: { actions: nameUpdater.action },
[ageUpdater.type]: { actions: ageUpdater.action },
},
},
},
});
const service = interpret(formMachine)
.onTransition((state) => {
console.log(state.context);
})
.start();
// The event object sent will look like:
// {
// type: 'UPDATE_NAME',
// input: 'David'
// }
service.send(nameUpdater.update('David'));
// => { name: 'David', age: null }
// The event object sent will look like:
// {
// type: 'UPDATE_AGE',
// input: 100
// }
service.send(ageUpdater.update(100));
// => { name: 'David', age: 100 }
TypeScript​
To properly type the Immer assign action creator, pass in the context and event types as generic types:
interface SomeContext {
name: string;
}
interface SomeEvent {
type: 'SOME_EVENT';
value: string;
}
// ...
{
actions: assign<SomeContext, SomeEvent>((context, event) => {
context.name = event.value;
// ... etc.
});
}
To properly type createUpdater, pass in the context and the specific ImmerUpdateEvent<...> (see below) types as generic types:
import { createUpdater, ImmerUpdateEvent } from '@xstate/immer';
// This is the same as:
// {
// type: 'UPDATE_NAME';
// input: string;
// }
type NameUpdateEvent = ImmerUpdateEvent<'UPDATE_NAME', string>;
const nameUpdater = createUpdater<SomeContext, NameUpdateEvent>(
'UPDATE_NAME',
(ctx, { input }) => {
ctx.name = input;
},
);
// You should use NameUpdateEvent directly as part of the event type
// in createMachine<SomeContext, SomeEvent>.
Here is a fully typed example of the previous form example:
import { createMachine } from 'xstate';
import { createUpdater, ImmerUpdateEvent } from '@xstate/immer';
interface FormContext {
name: string;
age: number | undefined;
}
type NameUpdateEvent = ImmerUpdateEvent<'UPDATE_NAME', string>;
type AgeUpdateEvent = ImmerUpdateEvent<'UPDATE_AGE', number>;
const nameUpdater = createUpdater<FormContext, NameUpdateEvent>(
'UPDATE_NAME',
(ctx, { input }) => {
ctx.name = input;
},
);
const ageUpdater = createUpdater<FormContext, AgeUpdateEvent>(
'UPDATE_AGE',
(ctx, { input }) => {
ctx.age = input;
},
);
type FormEvent =
| NameUpdateEvent
| AgeUpdateEvent
| {
type: 'SUBMIT';
};
const formMachine = createMachine({
schema: {
context: {} as FormContext,
events: {} as FormEvent,
},
initial: 'editing',
context: {
name: '',
age: undefined,
},
states: {
editing: {
on: {
[nameUpdater.type]: { actions: nameUpdater.action },
[ageUpdater.type]: { actions: ageUpdater.action },
SUBMIT: 'submitting',
},
},
submitting: {
// ...
},
},
});