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.

XState Store provides first-class framework integrations for React, Solid, and Vue, along with actor integration for XState.

React

The React integration uses useSyncExternalStore for optimal performance and automatic subscription management.

Installation

npm install @xstate/store
The React hooks are available from @xstate/store/react.

useSelector

Subscribe to store or atom values in React components:
import { useSelector } from '@xstate/store/react';
import { createStore } from '@xstate/store';

const counterStore = createStore({
  context: { count: 0, lastUpdate: Date.now() },
  on: {
    increment: (ctx) => ({
      ...ctx,
      count: ctx.count + 1,
      lastUpdate: Date.now()
    }),
    decrement: (ctx) => ({
      ...ctx,
      count: ctx.count - 1,
      lastUpdate: Date.now()
    })
  }
});

function Counter() {
  // Select a specific value
  const count = useSelector(counterStore, (s) => s.context.count);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => counterStore.trigger.increment()}>
        Increment
      </button>
      <button onClick={() => counterStore.trigger.decrement()}>
        Decrement
      </button>
    </div>
  );
}

Type Signature

// Select a value from store/atom
function useSelector<TStore extends Readable<any>, T>(
  store: TStore,
  selector: (snapshot: TStore extends Readable<infer T> ? T : never) => T,
  compare?: (a: T | undefined, b: T) => boolean
): T

// Use entire snapshot
function useSelector<TStore extends Readable<any>>(
  store: TStore,
  selector?: undefined,
  compare?: (a: T | undefined, b: T) => boolean
): TStore extends Readable<infer T> ? T : never
See [react:60-112] for the implementation.

Without Selector

Omit the selector to get the entire snapshot:
function FullState() {
  const snapshot = useSelector(counterStore);

  return (
    <div>
      <p>Count: {snapshot.context.count}</p>
      <p>Last update: {new Date(snapshot.context.lastUpdate).toLocaleString()}</p>
      <p>Status: {snapshot.status}</p>
    </div>
  );
}

Custom Comparison

Provide a custom comparison function to control re-renders:
import { shallowEqual } from '@xstate/store';

function UserProfile() {
  // Only re-render if user object properties change (shallow)
  const user = useSelector(
    userStore,
    (s) => s.context.user,
    shallowEqual
  );

  return <div>{user.name}</div>;
}

Using Atoms

useSelector works with atoms too:
import { createAtom } from '@xstate/store';
import { useSelector } from '@xstate/store/react';

const countAtom = createAtom(0);

function Counter() {
  const count = useSelector(countAtom);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => countAtom.set((c) => c + 1)}>
        Increment
      </button>
    </div>
  );
}

Multiple Selectors

Call useSelector multiple times for different values:
function Dashboard() {
  const user = useSelector(store, (s) => s.context.user);
  const settings = useSelector(store, (s) => s.context.settings);
  const notifications = useSelector(store, (s) => s.context.notifications);

  // Component only re-renders when its selected values change
  return (
    <div>
      <UserProfile user={user} />
      <Settings settings={settings} />
      <Notifications items={notifications} />
    </div>
  );
}
Each useSelector call creates an independent subscription. The component only re-renders when the selected value changes.

createStoreHook (Legacy)

The createStoreHook API creates a custom hook for a store:
import { createStoreHook } from '@xstate/store/react';

const useCountStore = createStoreHook({
  context: { count: 0 },
  on: {
    inc: (ctx, event: { by: number }) => ({
      count: ctx.count + event.by
    })
  }
});

function Component() {
  const [count, store] = useCountStore((s) => s.context.count);

  return (
    <div>
      {count}
      <button onClick={() => store.trigger.inc({ by: 1 })}>+</button>
    </div>
  );
}
createStoreHook is deprecated. Use useSelector with a module-level store instead.

Solid

Solid.js integration uses Solid’s reactive primitives for optimal fine-grained reactivity.

Installation

npm install @xstate/store solid-js
Import hooks from @xstate/store/solid.

useSelector

Create reactive signals from stores:
import { useSelector } from '@xstate/store/solid';
import { createStore } from '@xstate/store';

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

function Counter() {
  const count = useSelector(
    counterStore,
    (s) => s.context.count
  );

  return (
    <div>
      <p>Count: {count()}</p>
      <button onClick={() => counterStore.trigger.increment()}>
        Increment
      </button>
      <button onClick={() => counterStore.trigger.decrement()}>
        Decrement
      </button>
    </div>
  );
}

Type Signature

function useSelector<TStore extends AnyStore, T>(
  store: TStore,
  selector: (snapshot: SnapshotFromStore<TStore>) => T,
  compare?: (a: T | undefined, b: T) => boolean
): () => T // Returns a signal
See [solid:57-81] for the implementation.

Key Differences from React

  • Returns a signal accessor () => T, not the value directly
  • Use count() to access the value, not just count
  • Subscriptions are managed by Solid’s reactivity system
function Example() {
  const count = useSelector(store, (s) => s.context.count);

  // ✅ Correct - call the signal
  return <div>{count()}</div>;

  // ❌ Wrong - don't use it directly
  // return <div>{count}</div>;
}

With Atoms

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

const nameAtom = createAtom('Alice');

function Greeting() {
  const name = useSelector(nameAtom, (state) => state);

  return (
    <div>
      <p>Hello, {name()}!</p>
      <input
        type="text"
        value={name()}
        onInput={(e) => nameAtom.set(e.currentTarget.value)}
      />
    </div>
  );
}

Vue

Vue integration is available through manual subscriptions. Use Vue’s ref and computed for reactivity.

Basic Usage

<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { createStore } from '@xstate/store';

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

const snapshot = ref(store.getSnapshot());
const count = computed(() => snapshot.value.context.count);

let unsubscribe: (() => void) | undefined;

onMounted(() => {
  unsubscribe = store.subscribe((newSnapshot) => {
    snapshot.value = newSnapshot;
  }).unsubscribe;
});

onUnmounted(() => {
  unsubscribe?.();
});

function increment() {
  store.trigger.increment();
}

function decrement() {
  store.trigger.decrement();
}
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

Composable Pattern

Create a composable for reusable store subscriptions:
// useStore.ts
import { ref, onMounted, onUnmounted, Ref } from 'vue';
import { Store, StoreSnapshot } from '@xstate/store';

export function useStore<TContext>(
  store: Store<TContext, any, any>
): Ref<StoreSnapshot<TContext>> {
  const snapshot = ref(store.getSnapshot()) as Ref<StoreSnapshot<TContext>>;

  onMounted(() => {
    const subscription = store.subscribe((newSnapshot) => {
      snapshot.value = newSnapshot;
    });

    onUnmounted(() => {
      subscription.unsubscribe();
    });
  });

  return snapshot;
}
Use the composable:
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from './useStore';
import { counterStore } from './store';

const snapshot = useStore(counterStore);
const count = computed(() => snapshot.value.context.count);
</script>

<template>
  <div>Count: {{ count }}</div>
</template>

XState Integration

Use stores as actors in XState machines with fromStore().

Creating Store Actors

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

// Create store logic
const counterLogic = fromStore({
  context: (input: { initialCount: number }) => ({
    count: input.initialCount
  }),
  on: {
    increment: (ctx) => ({ count: ctx.count + 1 }),
    decrement: (ctx) => ({ count: ctx.count - 1 })
  }
});

// Use as an actor
const actor = createActor(counterLogic, {
  input: { initialCount: 10 }
});
actor.start();

console.log(actor.getSnapshot().context.count); // 10
actor.send({ type: 'increment' });
console.log(actor.getSnapshot().context.count); // 11

Type Signature

function fromStore<
  TContext extends StoreContext,
  TEventPayloadMap extends EventPayloadMap,
  TInput,
  TEmitted extends EventPayloadMap
>(config: {
  context: ((input: TInput) => TContext) | TContext;
  on: {
    [K in keyof TEventPayloadMap]: StoreAssigner<
      TContext,
      { type: K } & TEventPayloadMap[K],
      ExtractEvents<TEmitted>
    >;
  };
  emits?: { [K in keyof TEmitted]: (payload: ...) => void };
}): ActorLogic<StoreSnapshot<TContext>, ExtractEvents<TEventPayloadMap>, TInput, any, ExtractEvents<TEmitted>>
See [fromStore:31-93] for the implementation.

In State Machines

Invoke stores as child actors:
import { setup, createActor } from 'xstate';
import { fromStore } from '@xstate/store';

const cartStoreLogic = fromStore({
  context: { items: [] as Array<{ id: string; name: string }> },
  on: {
    addItem: (ctx, event: { id: string; name: string }) => ({
      items: [...ctx.items, { id: event.id, name: event.name }]
    }),
    removeItem: (ctx, event: { id: string }) => ({
      items: ctx.items.filter((item) => item.id !== event.id)
    })
  },
  emits: {
    itemAdded: (payload) => {
      console.log('Item added:', payload);
    }
  }
});

const checkoutMachine = setup({
  actors: {
    cartStore: cartStoreLogic
  }
}).createMachine({
  initial: 'shopping',
  context: {
    cartRef: null
  },
  states: {
    shopping: {
      invoke: {
        id: 'cart',
        src: 'cartStore',
        input: { items: [] }
      },
      on: {
        ADD_TO_CART: {
          actions: ({ event, system }) => {
            const cartRef = system.get('cart');
            cartRef.send({
              type: 'addItem',
              id: event.id,
              name: event.name
            });
          }
        },
        CHECKOUT: 'payment'
      }
    },
    payment: {
      // ...
    }
  }
});

const actor = createActor(checkoutMachine);
actor.start();

actor.send({ type: 'ADD_TO_CART', id: '1', name: 'Widget' });

Emitted Events

Stores can emit events that the parent machine can listen to:
const storeLogic = fromStore({
  context: { count: 0 },
  on: {
    increment: (ctx, event, enqueue) => {
      const newCount = ctx.count + 1;

      if (newCount >= 10) {
        enqueue.emit.limitReached({ count: newCount });
      }

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

const machine = setup({
  actors: { storeLogic }
}).createMachine({
  invoke: {
    id: 'counter',
    src: 'storeLogic'
  },
  on: {
    limitReached: {
      actions: ({ event }) => {
        console.log('Parent received:', event);
      }
    }
  }
});

Patterns and Best Practices

Module-Level Stores

Create stores at the module level and import them:
// store.ts
import { createStore } from '@xstate/store';

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

// Counter.tsx
import { useSelector } from '@xstate/store/react';
import { counterStore } from './store';

export function Counter() {
  const count = useSelector(counterStore, (s) => s.context.count);
  return <div>{count}</div>;
}
Module-level stores provide global state with automatic subscription management in components.

Optimizing Re-renders

Select only the data you need:
// ❌ Bad - re-renders on any context change
const snapshot = useSelector(store);
return <div>{snapshot.context.user.name}</div>;

// ✅ Good - only re-renders when name changes
const name = useSelector(store, (s) => s.context.user.name);
return <div>{name}</div>;

Store Selections for Derived State

Use store.select() for derived values:
const store = createStore({
  context: { items: [] as Array<{ price: number }> },
  on: { /* ... */ }
});

const totalSelection = store.select((s) =>
  s.context.items.reduce((sum, item) => sum + item.price, 0)
);

function Total() {
  const total = useSelector(totalSelection);
  return <div>Total: ${total}</div>;
}

Lazy Atoms for Expensive Computations

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

const expensiveAtom = createAtom((read) => {
  const data = read(dataAtom);
  // Expensive computation only runs when dataAtom changes
  return processLargeDataset(data);
});

function Results() {
  const results = useSelector(expensiveAtom);
  return <div>{results}</div>;
}

Testing

Testing Stores

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

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

  expect(store.get().context.count).toBe(0);

  store.trigger.increment();
  expect(store.get().context.count).toBe(1);
});

Testing React Components

import { render, screen, fireEvent } from '@testing-library/react';
import { createStore } from '@xstate/store';
import { Counter } from './Counter';

test('counter displays and increments', () => {
  const store = createStore({
    context: { count: 0 },
    on: {
      increment: (ctx) => ({ count: ctx.count + 1 })
    }
  });

  render(<Counter store={store} />);

  expect(screen.getByText('Count: 0')).toBeInTheDocument();

  fireEvent.click(screen.getByText('Increment'));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Next Steps

Build docs developers (and LLMs) love