Skip to content
Version: XState v4

@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 machine context in a convenient way
  • createUpdater() - a useful function that allows you to cohesively define a context update event event creator and assign action, all together. (See an example below)
  1. Install immer, xstate, @xstate/immer:
npm install immer xstate @xstate/immer
  1. 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​

ArgumentTypeDescription
recipefunctionA function where "mutations" to context are made. See the Immer docs.

Arguments for recipe​

ArgumentTypeDescription
contextanyThe context data of the current state
eventevent objectThe received event object
metaassign meta objectAn 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.

ArgumentTypeDescription
eventTypestringThe event type for the Immer update event
recipefunctionA 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: the eventType specified
  • input: the "payload" of the update event

The object returned by createUpdater(...) is an updater object containing:

  • type: the eventType passed into createUpdater(eventType, ...). This is used for specifying transitions in which the update will occur.
  • action: the assign action object that will update the context.
  • update: the event creator that takes in the input and returns an event object with the specified eventType and input that will be passed to recipe(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: {
// ...
},
},
});