Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/statelyai/xstate/llms.txt

Use this file to discover all available pages before exploring further.

Create stores using createStore() with context and event handlers.

Basic Store Creation

Type Signature

function createStore<
  TContext extends StoreContext,
  TEventPayloadMap extends EventPayloadMap,
  TEmittedPayloadMap extends EventPayloadMap
>(
  definition: StoreConfig<TContext, TEventPayloadMap, TEmittedPayloadMap>
): Store<TContext, TEventPayloadMap, ExtractEvents<TEmittedPayloadMap>>

Simple Example

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

const counterStore = createStore({
  context: { count: 0 },
  on: {
    increment: (context) => ({
      ...context,
      count: context.count + 1
    }),
    decrement: (context) => ({
      ...context,
      count: context.count - 1
    }),
    incrementBy: (context, event: { value: number }) => ({
      ...context,
      count: context.count + event.value
    })
  }
});

Event Handlers

Event handlers are functions that receive the current context and event, returning the next context.

Event Handler Signature

type StoreAssigner<TContext, TEvent, TEmitted> = (
  context: TContext,
  event: TEvent,
  enqueue: EnqueueObject<TEmitted>
) => TContext | void

Returning New Context

Always return a new context object (or void when using Immer):
const todoStore = createStore({
  context: {
    todos: [] as Array<{ id: string; text: string; completed: boolean }>
  },
  on: {
    addTodo: (context, event: { id: string; text: string }) => ({
      ...context,
      todos: [
        ...context.todos,
        { id: event.id, text: event.text, completed: false }
      ]
    }),
    toggleTodo: (context, event: { id: string }) => ({
      ...context,
      todos: context.todos.map((todo) =>
        todo.id === event.id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    }),
    removeTodo: (context, event: { id: string }) => ({
      ...context,
      todos: context.todos.filter((todo) => todo.id !== event.id)
    })
  }
});
Event handlers must return a new context object for the store to detect changes. Mutating the context directly won’t trigger updates.

Sending Events

Using send()

store.send({ type: 'incrementBy', value: 5 });
The trigger proxy provides type-safe, convenient event dispatching:
// Type-safe and autocomplete-friendly
store.trigger.incrementBy({ value: 5 });

// Events without payload
store.trigger.increment();

Side Effects

Use the enqueue parameter to schedule effects:

Enqueue Effects

const store = createStore({
  context: { user: null as { id: string; name: string } | null },
  on: {
    login: (context, event: { userId: string }, enqueue) => {
      // Schedule side effect
      enqueue.effect(() => {
        console.log('User logged in:', event.userId);
        // Call analytics, save to localStorage, etc.
        localStorage.setItem('userId', event.userId);
      });

      return {
        ...context,
        user: { id: event.userId, name: 'Loading...' }
      };
    }
  }
});
Effects run after the state update completes. Use them for side effects like logging, API calls, or localStorage updates.

Emitted Events

Stores can emit events that other parts of your application can listen to:
const store = createStore({
  context: { count: 0 },
  on: {
    increment: (context, event, enqueue) => {
      const newCount = context.count + 1;

      // Emit event when threshold reached
      if (newCount >= 10) {
        enqueue.emit.thresholdReached({ count: newCount });
      }

      return { ...context, count: newCount };
    }
  },
  emits: {
    thresholdReached: (payload) => {
      console.log('Threshold reached!', payload);
    }
  }
});

// Subscribe to emitted events
store.on('thresholdReached', (event) => {
  console.log('Count is now:', event.count);
});

// Wildcard listener for all emitted events
store.on('*', (event) => {
  console.log('Event emitted:', event);
});

Using Immer for Immutability

Use createStoreWithProducer() with Immer for mutable-style updates:

Type Signature

function createStoreWithProducer<
  TContext extends StoreContext,
  TEventPayloadMap extends EventPayloadMap,
  TEmittedPayloadMap extends EventPayloadMap
>(
  producer: (context: TContext, recipe: (context: TContext) => void) => TContext,
  config: {
    context: TContext;
    on: { [K in keyof TEventPayloadMap]: (context: TContext, event: ...) => void };
    emits?: { ... };
  }
): Store<TContext, TEventPayloadMap, ExtractEvents<TEmittedPayloadMap>>

Example with Immer

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

const store = createStoreWithProducer(produce, {
  context: {
    todos: [] as Array<{ id: string; text: string; completed: boolean }>
  },
  on: {
    addTodo: (context, event: { id: string; text: string }) => {
      // Mutate draft directly - Immer handles immutability
      context.todos.push({
        id: event.id,
        text: event.text,
        completed: false
      });
    },
    toggleTodo: (context, event: { id: string }) => {
      const todo = context.todos.find((t) => t.id === event.id);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo: (context, event: { id: string }) => {
      const index = context.todos.findIndex((t) => t.id === event.id);
      if (index !== -1) {
        context.todos.splice(index, 1);
      }
    }
  }
});
When using createStoreWithProducer(), event handlers don’t return a value. They mutate the draft context, and Immer produces the next immutable state.

Store Configuration Helper

Use createStoreConfig() to define reusable store configurations:
import { createStoreConfig, createStore } from '@xstate/store';

const counterConfig = createStoreConfig({
  context: { count: 0 },
  on: {
    increment: (context) => ({ ...context, count: context.count + 1 }),
    decrement: (context) => ({ ...context, count: context.count - 1 })
  }
});

// Use the config to create multiple stores
const store1 = createStore(counterConfig);
const store2 = createStore(counterConfig);

Subscribing to Changes

Basic Subscription

const subscription = store.subscribe((snapshot) => {
  console.log('State changed:', snapshot.context);
});

// Unsubscribe when done
subscription.unsubscribe();

Selecting Specific Values

Use select() to subscribe to derived values:
const countSelection = store.select((snapshot) => snapshot.context.count);

countSelection.subscribe((count) => {
  console.log('Count changed:', count);
});

// Or get the current value
const currentCount = countSelection.get();
See [store:187-193] for the select() type signature.

Getting State

Get Snapshot

const snapshot = store.getSnapshot();
// or
const snapshot = store.get();

console.log(snapshot);
// {
//   status: 'active',
//   context: { count: 5 },
//   output: undefined,
//   error: undefined
// }

Access Context Directly

const count = store.get().context.count;

Store Extensions

Extend stores with additional functionality using the .with() method:
import { undoRedo } from '@xstate/store/undo';

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

// Extension adds new events
store.trigger.increment();
store.trigger.undo(); // Reverts to previous state
store.trigger.redo(); // Reapplies the increment
Store extensions transform the store logic and can add new events. They’re composable via the .with() method.

Inspection

Inspect store events and state changes:
store.inspect((inspectionEvent) => {
  if (inspectionEvent.type === '@xstate.snapshot') {
    console.log('State:', inspectionEvent.snapshot);
    console.log('Event:', inspectionEvent.event);
  }
});

Inspection Event Types

  • @xstate.actor - Store initialized
  • @xstate.snapshot - State changed
  • @xstate.event - Event sent to store

TypeScript Tips

Infer Event Types

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

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

type StoreEvent = EventFromStore<typeof store>;
// { type: 'increment' } | { type: 'setValue', value: number }

Infer Snapshot Type

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

type StoreSnapshot = SnapshotFromStore<typeof store>;
// StoreSnapshot<{ count: number }>

Next Steps

Build docs developers (and LLMs) love