Stately

@xstate/store

Simple state management for JavaScript/TypeScript apps.

XState Store is a small library for simple state management in JavaScript/TypeScript applications. It is meant for updating store data using events for vanilla JavaScript/TypeScript apps, React apps, and more. It is comparable to libraries like Zustand, Redux, and Pinia. For more complex state management, you should use XState instead, or you can use XState Store with XState.

These are the docs for the latest version 4.x of XState Store. For version 3.x, see Version 3.x docs. For version 2.x, see Version 2.x docs.

Migrating from v3? See the migration guide.

The @xstate/store library requires TypeScript version 5.4 or above.

Installation

npm install @xstate/store
pnpm install @xstate/store
yarn add @xstate/store

Quick start

import { createStore } from '@xstate/store';
import { z } from 'zod';

const store = createStore({
  schemas: {
    emitted: {
      increased: z.object({ by: z.number(), total: z.number() }),
    },
  },
  context: { count: 0 },
  on: {
    inc: (context, event: { by: number }, enqueue) => {
      const count = context.count + event.by;

      enqueue.emit.increased({ by: event.by, total: count });

      return { count };
    },
  },
});

store.subscribe((snapshot) => {
  console.log(snapshot.context);
});

store.on('increased', (event) => {
  console.log(`Count increased by ${event.by}`);
});

store.trigger.inc({ by: 2 });
// logs { count: 2 }
// logs "Count increased by 2"

Stores

To create a store, pass a configuration object to the createStore(…) function with:

  1. The initial context
  2. An on object for transitions where the keys are event types and the values are context update functions

When updating context in transitions, you must return the complete context object with all properties:

import { createStore } from '@xstate/store';

const store = createStore({
  context: { count: 0, name: 'David' },
  on: {
    inc: (context) => ({
      ...context, // Preserve other context properties
      count: context.count + 1,
    }),
  },
});

Transitions receive the current context and event payload. Returning a new context updates the store snapshot, and calling store.trigger.*(...) sends typed events to the store:

import { createStore } from '@xstate/store';

const store = createStore({
  context: { count: 0 },
  on: {
    increment: (context, event: { by: number }) => {
      return {
        count: context.count + event.by,
      };
    },
  },
});

store.getSnapshot().context;
// => { count: 0 }

store.trigger.increment({ by: 2 });

store.getSnapshot().context;
// => { count: 2 }

Use store.can to check whether an event is allowed without updating the store:

const store = createStore({
  context: { count: 0 },
  on: {
    increment: (context, event: { by: number }) => {
      if (context.count + event.by > 10) {
        return;
      }

      return {
        count: context.count + event.by,
      };
    },
  },
});

store.can.increment({ by: 4 });
// => true

store.can.increment({ by: 11 });
// => false

Returning undefined (or void return;) from a transition marks the event as not allowed. Returning the same context object is still allowed, and transitions that enqueue effects, emitted events, or triggered events are allowed.

Schemas

XState Store v4 supports Standard Schema compatible schema libraries. Schemas define the store's runtime-readable contract: the shape of its context, accepted event payloads, and emitted event payloads.

Store uses schemas for type inference and metadata by default. It does not validate schema-declared values unless you opt in with validateSchemas().

import { createStore } from '@xstate/store';
import { z } from 'zod';

const store = createStore({
  schemas: {
    context: z.object({
      count: z.number(),
      label: z.string(),
    }),
    events: {
      rename: z.object({
        label: z.string(),
      }),
      reset: z.object({}),
    },
    emitted: {
      renamed: z.object({
        label: z.string(),
      }),
    },
  },
  context: { count: 0, label: 'ready' },
  on: {
    rename: (context, event, enqueue) => {
      enqueue.emit.renamed({ label: event.label });

      return {
        ...context,
        label: event.label,
      };
    },
  },
});

store.trigger.rename({ label: 'done' });
store.trigger.reset();

Event and emitted-event schemas define payload objects. Use an empty object schema, such as z.object({}), for events without payload.

When events are declared through schemas.events, matching handlers in on are optional. Missing handlers are no-ops, but the event still exists for typing and store.trigger.

fromStore(...) also accepts schemas and can infer context, event, and emitted-event types from schema definitions.

Validating schemas

Use validateSchemas() from @xstate/store/validate to opt into runtime validation:

import { createStore } from '@xstate/store';
import { validateSchemas } from '@xstate/store/validate';
import { z } from 'zod';

const store = createStore({
  schemas: {
    context: z.object({
      count: z.number(),
    }),
    events: {
      increment: z.object({ by: z.number() }),
    },
  },
  context: { count: 0 },
  on: {
    increment: (context, event) => ({
      count: context.count + event.by,
    }),
  },
}).with(validateSchemas());

validateSchemas() validates the event sent to the store, the final context after the transition completes, and emitted events before effects execute. It throws a StoreValidationError for invalid send(...), trigger.*(...), and transition(...) calls. store.can.*(...) remains boolean-only and returns false for validation errors.

You can turn off specific validation areas:

store.with(
  validateSchemas({
    context: false,
    events: false,
    emitted: true,
  }),
);

Emitting events

You can emit events from transitions by defining them in schemas.emitted and using enqueue.emit:

import { createStore } from '@xstate/store';
import { z } from 'zod';

const store = createStore({
  context: { count: 0 },
  schemas: {
    emitted: {
      increased: z.object({ by: z.number() }),
    },
  },
  on: {
    inc: (context, event: { by: number }, enqueue) => {
      enqueue.emit.increased({ by: event.by });

      return {
        ...context,
        count: context.count + event.by,
      };
    },
  },
});

// Listen for emitted events
store.on('increased', (event) => {
  console.log(`Count increased by ${event.by}`);
});

schemas.emitted is used for type inference and runtime-readable metadata. It does not validate emitted events unless you opt into validation with validateSchemas().

Effects

You can enqueue effects in state transitions using the enqueue argument:

const store = createStore({
  context: { count: 0 },
  on: {
    incrementDelayed: (context, event, enqueue) => {
      enqueue.effect(() => {
        setTimeout(() => {
          store.trigger.increment();
        }, 1000);
      });

      return context;
    },
    increment: (context) => ({
      count: context.count + 1,
    }),
  },
});

You can also enqueue another store event with enqueue.trigger:

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context) => ({
      count: context.count + 1,
    }),
    incTwice: (context, _event, enqueue) => {
      enqueue.trigger.inc();
      enqueue.trigger.inc();

      return context;
    },
  },
});

Important: Enqueued effects (enqueue.effect(), enqueue.emit(), and enqueue.trigger()) must be called synchronously within the transition function. Calling them outside the function's synchronous execution (e.g., inside an async callback or Promise) will have no effect, since the transition function must complete synchronously.

For async operations, send an event back to the store:

const store = createStore({
  context: { data: null },
  schemas: {
    emitted: {
      loaded: z.object({ data: z.string() }),
    },
  },
  on: {
    fetchData: (context, event, enqueue) => {
      enqueue.effect(async () => {
        const result = await fetch('/api/data');
        const data = await result.json();

        store.trigger.dataLoaded({ data });
      });
      return context;
    },
    dataLoaded: (context, event: { data: string }, enqueue) => {
      enqueue.emit.loaded({ data: event.data });
      return { ...context, data: event.data };
    },
  },
});

This ensures determinism: all state changes, effects, and emitted events must be the direct result of an event being sent to the store. Async callbacks cannot enqueue effects or emit events because they happen at unpredictable times, breaking the deterministic event-driven model.

Pure transitions

You can use store.transition(state, event) to compute a tuple of the next state and any effects from a given state and event. This is useful for debugging and testing, or for having full control over the state transitions in your application.

const store = createStore({
  context: { count: 0 },
  schemas: {
    emitted: {
      incremented: z.object({ by: z.number() }),
    },
  },
  on: {
    inc: (context, event: { by: number }, enqueue) => {
      enqueue.emit.incremented({ by: event.by });

      enqueue.effect(() => {
        setTimeout(() => {
          console.log('incremented');
        }, 1000);
      });

      return {
        ...context,
        count: context.count + event.by,
      };
    },
  },
});

const snapshot = store.getSnapshot();

const [nextState, effects] = store.transition(snapshot, {
  type: 'inc',
  by: 1,
});

console.log(nextState.context);
// => { count: 1 }

console.log(effects);
// => [
//   { type: 'incremented', by: 1 },
//   Function
// ]

// The store's state is unchanged
console.log(store.getSnapshot().context);
// => { count: 0 }

If you need to determine the next state from the store's initial state, you can get the initial snapshot using store.getInitialSnapshot():

const initialSnapshot = store.getInitialSnapshot();

const [nextState, effects] = store.transition(initialSnapshot, {
  type: 'inc',
  by: 1,
});

Selectors

Store selectors provide an efficient way to select and subscribe to specific parts of your store's state. With store selectors, you can:

  • Get the current value of a specific part of state via selector.get()
  • Subscribe to changes of only that specific part via selector.subscribe(observer)
  • Optimize performance by only notifying subscribers when the selected value actually changes via selector.subscribe(observer, equalityFn)

You can create a selector using store.select(selector):

import { createStore } from '@xstate/store';

const store = createStore({
  context: {
    position: { x: 0, y: 0 },
    name: 'John',
    age: 30,
  },
  on: {
    positionUpdated: (
      context,
      event: { position: { x: number; y: number } },
    ) => ({
      ...context,
      position: event.position,
    }),
  },
});

// Create a selector for the position
const position = store.select((context) => context.position);

// Get current value
console.log(position.get()); // { x: 0, y: 0 }

// Subscribe to changes
position.subscribe((position) => {
  console.log('Position updated:', position);
});

// When position updates, only position subscribers are notified
store.trigger.positionUpdated({ position: { x: 100, y: 200 } });
// Logs: Position updated: { x: 100, y: 200 }

Custom Equality Functions

You can provide a custom equality function as the second argument to store.select(selector, equalityFn) to control when subscribers should be notified:

const position = store.select(
  (context) => context.position,
  // Only notify if x coordinate changes
  (prev, next) => prev.x === next.x,
);

XState Store also provides a shallowEqual function that can be used as a default equality function:

import { shallowEqual } from '@xstate/store';

const position = store.select((context) => context.position, shallowEqual);

Store logic

Use createStoreLogic(...) to create reusable store definitions. Store logic can create store instances directly, and framework hooks can use it to create component-scoped stores.

import { createStoreLogic } from '@xstate/store';

const counterLogic = createStoreLogic({
  context: (input: { initialCount: number }) => ({
    count: input.initialCount,
  }),
  selectors: {
    count: (context) => context.count,
    doubled: (context) => context.count * 2,
  },
  on: {
    inc: (context) => ({
      count: context.count + 1,
    }),
  },
});

const store = counterLogic.createStore({ initialCount: 2 });

store.selectors.count.get();
// => 2

store.selectors.doubled.get();
// => 4

Input

Store logic can define context as a function of input. This lets each created store instance start with different context.

import { createStoreLogic } from '@xstate/store';

const counterLogic = createStoreLogic({
  context: (input: { initialCount: number }) => ({
    count: input.initialCount,
  }),
  on: {
    inc: (context) => ({
      count: context.count + 1,
    }),
  },
});

const store = counterLogic.createStore({ initialCount: 0 });

When store logic requires input, framework hooks require the input argument too:

import { createStoreLogic, useStore } from '@xstate/store-react';

const counterLogic = createStoreLogic({
  context: (input: { initialCount: number }) => ({
    count: input.initialCount,
  }),
  on: {
    inc: (context) => ({
      count: context.count + 1,
    }),
  },
});

function Counter() {
  const store = useStore(counterLogic, { initialCount: 0 });
}

Inspection

Just like with XState, you can use the Inspect API to inspect events sent to the store and state transitions within the store by using the .inspect method:

import { createStore } from '@xstate/store';

const store = createStore({
  // ...
});

store.inspect((inspectionEvent) => {
  // type: '@xstate.transition'
  inspectionEvent.event;
  inspectionEvent.snapshot;
  console.log(inspectionEvent);
});

Since the store is automatically started, inspectors will immediately receive the initial state snapshot.

The .inspect(…) method returns a subscription object:

import { createStore } from '@xstate/store';

const sub = store.inspect((inspectionEvent) => {
  console.log(inspectionEvent);
});

// Stop listening for inspection events
sub.unsubscribe();

You can use the Stately Inspector to inspect and visualize the state of the store.

import { createBrowserInspector } from '@statelyai/inspect';
import { createStore } from '@xstate/store';

const store = createStore({
  // ...
});

const inspector = createBrowserInspector({
  // ...
});

store.inspect(inspector);

Using Immer

You can use the produce(…) function from Immer to update the context in transitions:

import { createStore } from '@xstate/store';
import { produce } from 'immer';

const store = createStore({
  context: { count: 0, todos: [] },
  on: {
    inc: (context, event: { by: number }) =>
      produce(context, (draft) => {
        draft.count += event.by;
      }),
    addTodo: (context, event: { todo: string }) =>
      produce(context, (draft) => {
        draft.todos.push(event.todo);
      }),
    // Not using a producer
    resetCount: (context) => ({
      ...context,
      count: 0,
    }),
  },
});

If a transition should be unavailable, return undefined from the transition before calling produce(...):

on: {
  eatDonut: (context) => {
    if (context.donuts === 0) {
      return;
    }

    return produce(context, (draft) => {
      draft.donuts--;
    });
  },
}

Immer treats a producer that returns undefined the same as a producer that does not explicitly return anything. If you need produce(...) itself to return undefined, return Immer's nothing token from the producer.

Atoms

An atom is a lightweight, reactive piece of state that can be read, written to, and subscribed to. Atoms can be used standalone or combined with other atoms and stores for more complex state management.

You can:

  • Create an atom with createAtom(initialValue)
  • Read the atom's value with atom.get()
  • Subscribe to changes with atom.subscribe(observer)
  • Update the atom with atom.set(value)

Atoms are best used for:

  • Simple, independent pieces of state
  • Derived/computed values
  • Bridging between stores and external state
  • When you need direct value updates without constraints

For state that needs to follow specific transition rules or complex update logic, consider using a store instead.

Creating Atoms

Create an atom using createAtom() with an initial value:

import { createAtom } from '@xstate/store';

// Create an atom with a primitive value
const countAtom = createAtom(0);

// Create an atom with an object
const userAtom = createAtom({ name: 'David', count: 100 });

Reading and Writing Atoms

You can read an atom's value using atom.get() and update it using atom.set():

const countAtom = createAtom(0);

// Read the current value
console.log(countAtom.get()); // 0

// Set a new value directly
countAtom.set(1); // 1

// Update value using a function
countAtom.set((prev) => prev + 1); // 2

const count = createAtom(0);
count.get(); // 0
count.set(1); // 1
count.set((prev) => prev + 1); // 2

// Recomputes when count changes
const laugh = createAtom(() => {
  return 'ha'.repeat(count.get());
});
laugh.subscribe((value) => {
  console.log(value);
});

Subscribing to Changes

Atoms support subscriptions to react to value changes:

const countAtom = createAtom(0);

// Subscribe to changes
const subscription = countAtom.subscribe((newValue) => {
  console.log('Count changed:', newValue);
});

countAtom.set(1); // Logs: "Count changed: 1"

// Unsubscribe when done
subscription.unsubscribe();

countAtom.set(2); // Does not log anything

Derived atoms

You can create derived atoms that combine values from other atoms, stores, or selectors:

const nameAtom = createAtom('David');
const ageAtom = createAtom(30);

// Derive from multiple atoms
const userAtom = createAtom(() => ({
  name: nameAtom.get(),
  age: ageAtom.get(),
}));

// Derived atoms are read-only and update automatically
console.log(userAtom.get()); // { name: 'David', age: 30 }
nameAtom.set('John');
console.log(userAtom.get()); // { name: 'John', age: 30 }
ageAtom.set(31);
console.log(userAtom.get()); // { name: 'John', age: 31 }

Accessing previous value in computed atoms

Computed atoms can access their previous computed value through the first parameter. This is useful for any derived state that depends on its own previous value:

const countAtom = createAtom(0);

// Running total that aggregates all count changes
// Note: Specify the type parameter for proper inference of prev
const totalAtom = createAtom<number>((prev) => countAtom.get() + (prev ?? 0));

console.log(totalAtom.get()); // 0
countAtom.set(5);
console.log(totalAtom.get()); // 5 (0 + 5)
countAtom.set(3);
console.log(totalAtom.get()); // 8 (5 + 3)
countAtom.set(2);
console.log(totalAtom.get()); // 10 (8 + 2)

The previous value is undefined on the first computation. Since TypeScript cannot infer the type of prev, you should provide a type parameter to createAtom<T>() for type safety.

Read other atoms directly with .get() when creating derived atoms:

const derivedAtom = createAtom(() => atomA.get() + atomB.get());

Reducer atoms

Use createReducerAtom(...) when updates should go through reducer events:

import { createReducerAtom } from '@xstate/store';

const countAtom = createReducerAtom(0, (state, event: { type: 'inc' }) => {
  if (event.type === 'inc') {
    return state + 1;
  }

  return state;
});

countAtom.send({ type: 'inc' });

Atom configs

Use createAtomConfig(...) to create an inert atom definition that can be instantiated later. This is useful for framework hooks that create component-scoped atoms from input.

import { createAtomConfig } from '@xstate/store';

const countConfig = createAtomConfig((input: { initialCount: number }) => {
  return input.initialCount;
});

const countAtom = countConfig.createAtom({ initialCount: 0 });

Async Atoms

Async atoms are a special type of atom that handle asynchronous values. They are created using createAsyncAtom(…) and take an async function that returns a promise. The atom's value represents the loading state of the async operation.

The value of an async atom will be an object with a status property that can be:

  • 'pending' - while the promise is resolving
  • 'done' with a data property containing the resolved value
  • 'error' with an error property containing the error that was thrown
import { createAsyncAtom } from '@xstate/store';

const userAtom = createAsyncAtom(async ({ signal }) => {
  const response = await fetch('/api/user', { signal });
  return response.json();
});

userAtom.subscribe((snapshot) => {
  if (snapshot.status === 'pending') {
    console.log(snapshot);
    // { status: 'pending' }
  } else if (snapshot.status === 'done') {
    console.log(snapshot);
    // { status: 'done', data: { name: 'David', ... } }
  } else if (snapshot.status === 'error') {
    console.log(snapshot);
    // { status: 'error', error: Error('Failed to fetch') }
  }
});

Working with Stores and Selectors

Atoms can seamlessly integrate with XState stores and selectors:

const store = createStore({
  context: { count: 0 },
  on: {
    increment: (context) => ({ ...context, count: context.count + 1 }),
  },
});

// Create an atom from a store selector
const countSelector = store.select((context) => context.count);
const doubleCountAtom = createAtom(() => 2 * countSelector.get());

console.log(doubleCountAtom.get()); // 0
store.trigger.increment();
console.log(doubleCountAtom.get()); // 2

Derived atoms are read-only by design. If you need to update multiple values atomically, consider using a store instead.

Framework integrations

For framework-specific usage, install the appropriate package:

In XState Store v4, importing framework bindings from @xstate/store/react, @xstate/store/solid, etc. is no longer supported. Use the dedicated packages above instead, such as @xstate/store-react.

Here is the same counter store used with each framework adapter:

import { createStore, useSelector } from '@xstate/store-react';

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context, event: { by?: number }) => ({
      count: context.count + (event.by ?? 1),
    }),
  },
});

function Counter() {
  const count = useSelector(store, (state) => state.context.count);

  return <button onClick={() => store.trigger.inc()}>Count: {count}</button>;
}
<script setup lang="ts">
import { createStore, useSelector } from '@xstate/store-vue';

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context, event: { by?: number }) => ({
      count: context.count + (event.by ?? 1),
    }),
  },
});

const count = useSelector(store, (state) => state.context.count);
</script>

<template>
  <button @click="store.trigger.inc()">Count: {{ count }}</button>
</template>
<script lang="ts">
  import { createStore, useSelector } from '@xstate/store-svelte';

  const store = createStore({
    context: { count: 0 },
    on: {
      inc: (context, event: { by?: number }) => ({
        count: context.count + (event.by ?? 1),
      }),
    },
  });

  const count = useSelector(store, (state) => state.context.count);
</script>

<button on:click={() => store.trigger.inc()}>Count: {$count}</button>
import { createStore, useSelector } from '@xstate/store-solid';

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context, event: { by?: number }) => ({
      count: context.count + (event.by ?? 1),
    }),
  },
});

function Counter() {
  const count = useSelector(store, (state) => state.context.count);

  return <button onClick={() => store.trigger.inc()}>Count: {count()}</button>;
}
import { Component } from '@angular/core';
import { createStore, injectStore } from '@xstate/store-angular';

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context, event: { by?: number }) => ({
      count: context.count + (event.by ?? 1),
    }),
  },
});

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="store.trigger.inc()">Count: {{ count() }}</button>
  `,
})
export class CounterComponent {
  store = store;
  count = injectStore(store, (state) => state.context.count);
}
import { createStore, useSelector } from '@xstate/store-preact';

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context, event: { by?: number }) => ({
      count: context.count + (event.by ?? 1),
    }),
  },
});

function Counter() {
  const count = useSelector(store, (state) => state.context.count);

  return <button onClick={() => store.trigger.inc()}>Count: {count}</button>;
}

Extensions

XState Store extensions add behavior to a store with .with(...).

  • Undo/redo adds store.trigger.undo() and store.trigger.redo().
  • Persist saves and restores store state from storage.
  • Reset adds store.trigger.reset().
  • Schema validation validates schema-declared events, emitted events, and context at runtime.

Using with XState

You may notice that stores are very similar to actors in XState. This is very much by design. XState's actors are very powerful, but may also be too complex for simple use cases, which is why @xstate/store exists.

However, if you have existing XState code, and you enjoy the simplicity of creating store logic with @xstate/store, you can use the fromStore(...) actor logic creator to create XState-compatible store logic that can be passed to the createActor(storeLogic) function:

import { fromStore } from '@xstate/store';
import { createActor } from 'xstate';

// Instead of:
// const store = createStore( ... };
const storeLogic = fromStore({
  context: { count: 0, incremented: false /* ... */ },
  on: {
    inc: {
      count: (context, event) => context.count + 1,
      // Static values do not need to be wrapped in a function
      incremented: true,
    },
  },
});

const store = createActor(storeLogic);
store.subscribe((snapshot) => {
  console.log(snapshot);
});
store.start();

store.send({
  type: 'inc',
});

In short, you can convert createStore(…) to fromStore(…) just by changing one line of code. Note that fromStore(…) returns store logic, and not a store actor instance. Store logic is passed to createActor(storeLogic) to create a store actor instance:

import { fromStore } from '@xstate/store';

// Instead of:
// const store = createStore({
const storeLogic = fromStore({
  context: {
    // ...
  },
  on: {
    // ...
  },
});

// Create the store (actor)
const storeActor = createActor(storeLogic);

Using fromStore(…) to create store actor logic also has the advantage of allowing you to provide input by using a context function that takes in the input and returns the initial context:

import { fromStore } from '@xstate/store';

const storeLogic = fromStore({
  context: (initialCount: number) => ({
    count: initialCount,
  }),
  on: {
    // ...
  },
});

const actor = createActor(storeLogic, {
  input: 42,
});

Converting stores to state machines

If you have a store that you want to convert to a state machine in XState, you can convert it in a straightforward way:

  1. Use createMachine(…) (imported from xstate) instead of createStore(…) (imported from @xstate/store) to create a state machine.
  2. Wrap the assignments in an assign(…) action creator (imported from xstate) and move that to the actions property of the transition.
  3. Destructure context and event from the first argument instead of them being separate arguments.

For example, here is our store before conversion:

import { createMachine } from 'xstate';

// 1. Use `createMachine(…)` instead of `createStore(…)`
const store = createStore({
  context: { count: 0, name: 'David' },
  on: {
    inc: {
      // 2. Wrap the assignments in `assign(…)`
      count: (context, event: { by: number }) => context.count + event.by,
    },
  },
});

const machine = createMachine({
  // ...
});

And here is the store as a state machine after conversion:

import { createMachine } from 'xstate';

// const store = createStore({
//   context: { count: 0, name: 'David' },
//   on: {
//     inc: {
//       count: (context, event: { by: number }) => context.count + event.by
//     }
//   }
// });

// 1. Use `createMachine(…)` instead of `createStore(…)`
const machine = createMachine({
  context: {
    count: 0,
    name: 'David',
  },
  on: {
    inc: {
      // 2. Wrap the assignments in `assign(…)`
      actions: assign({
        // 3. Destructure `context` and `event` from the first argument
        count: ({ context, event }) => context.count + event.by,
      }),
    },
  },
});

For stronger typing, use the setup(…) function to strongly type the context and events:

import { setup } from 'xstate';

const machine = setup({
  types: {
    context: {} as { count: number; name: string },
    events: {} as { type: 'inc'; by: number },
  },
}).createMachine({
  // Same as the previous example
});

Comparison

Coming soon

Migrating from v3

See the migration guide.

On this page