Skip to content
Version: XState v5

@xstate/store

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.

Installation

npm install @xstate/store

Quick start

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

const store = createStore(
// Initial context
{ count: 0, name: 'David' },
// Transitions
{
inc: {
count: (context) => context.count + 1
},
add: {
count: (context, event: { num: number }) => context.count + event.num
},
changeName: {
name: (context, event: { newName: string }) => event.newName
}
}
);

// Get the current state (snapshot)
console.log(store.getSnapshot());
// => {
// status: 'active',
// context: { count: 0, name: 'David' }
// }

// Subscribe to snapshot changes
store.subscribe((snapshot) => {
console.log(snapshot.context);
});

// Send an event
store.send({ type: 'inc' });
// logs { count: 1, name: 'David' }

store.send({ type: 'add', num: 10 });
// logs { count: 11, name: 'David' }

store.send({ type: 'changeName', newName: 'Jenny' });
// logs { count: 11, name: 'Jenny' }

Creating a store

To create a store, you need to pass in two arguments to the createStore(…) function:

  1. The initial context
  2. An object for transitions (event handlers) where:
  • The keys are the event types (e.g. "inc", "add", "changeName")
  • The values are the state updates to apply when the event is sent to the store, as either an object or a function.

Updating context in transitions is similar to using the assign action in XState. You can update specific context properties by using an object:

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

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

Or you can update the entire context by using a function:

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

const store = createStore(
{ count: 0, incremented: false, /* ... */ },
{
inc: (context, event) => {
// ...

return {
count: context.count + 1,
incremented: true
}
}
}
);

You do not need to spread the ...context when updating the entire context with a function. The return value of the function will be merged into the current context.

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

const store = createStore(
{ count: 0, incremented: false, /* ... */ },
{
reset: (context, event) => {
// No need to use `...context`
// `incremented` and other properties will be preserved
return {
count: 0,
}
}
}
);

Transition functions

A transition function is a function that takes the current context and an event object, and returns:

  • The partial or entire context object to update (if using a function assigner)
  • The context property value to update (if using an object assigner).

For strong typing, you should specify the payload type of the event object in the transition function.

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

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

store.send({
type: 'updateName',
name: 'Jenny' // Strongly-typed as `string`
});

store.send({
type: 'inc',
by: 10 // Strongly-typed as `number`
});

Using Immer

If you want to use Immer to update the context, you can do so by passing in the produce function as the first argument to createStoreWithProducer(producer, …).

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

const store = createStoreWithProducer(
// Producer
produce,
// Initial context
{ count: 0, todos: [] },
// Transitions
{
inc: (context, event: { by: number }) => {
// No return; handled by Immer
context.count += event.by;
},
addTodo: (context, event: { todo: string }) => {
// No return; handled by Immer
context.todos.push(event.todo);
}
});

// ...

Note that you cannot use the object assigner syntax when using createStoreFromProducer(…), nor is it even necessary.

Usage with React

If you are using React, you can use the useSelector(store, selector) hook to subscribe to the store and get the current state.

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

// Create a store
const store = createStore(
{ count: 0, name: 'David' },
{
inc: {
count: (context) => context.count + 1
}
});

// Use the `useSelector` hook to subscribe to the store
function Component(props) {
const count = useSelector(store, (state) => state.context.count);

// This component displays the count and has a button to increment it
return (
<div>
Count: {count}
<button onClick={() => store.send({ type: 'inc' })}>Increment</button>
</div>
);
}

A store can be shared with multiple components, which will all receive the same snapshot from the store instance. Stores are useful for global state management.

Using XState Store 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(context, transitions) 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(
{ count: 0, incremented: false, /* ... */ },
{
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:

// Instead of:
// const store = createStore({
const storeLogic = fromStore({
// (same arguments as `createStore`)
});

// 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((initialCount: number) => ({
count: initialCount,
}), {
// ...
});

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. Move the first argument (the initial context object) to the context property of the machine configuration.
  3. Move the second argument (the transitions object) to the on property of the machine configuration.
  4. Wrap the assignments in an assign(…) action creator (imported from xstate) and move that to the actions property of the transition.
  5. 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(
// 2. Move the first argument (the initial `context` object)
// to the `context` property of the machine configuration
{ count: 0, name: 'David' },
// 3. Move the second argument (the `transitions` object)
// to the `on` property of the machine configuration
{
inc: {
// 4. Wrap the assignments in `assign(…)`
// 5. Destructure `context` and `event` from the first argument
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(
// { count: 0, name: 'David' },
// {
// inc: {
// count: (context, event: { by: number }) => context.count + event.by
// }
// });

// 1. Use `createMachine(…)` instead of `createStore(…)`
const machine = createMachine({
// 2. Move the first argument (the initial `context` object)
// to the `context` property of the machine configuration
context: {
count: 0,
name: 'David'
},
// 3. Move the second argument (the `transitions` object)
// to the `on` property of the machine configuration
on: {
inc: {
// 4. Wrap the assignments in `assign(…)`
actions: assign({
// 5. 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

This section compares XState Store to other popular state management libraries in TypeScript. It is meant for reference purposes only, and not intended to favor one approach over the other. The examples are copied from Zustand's comparison docs.

Compare to Zustand

Zustand

import { create } from 'zustand'

type State = {
count: number
}

type Actions = {
increment: (qty: number) => void
decrement: (qty: number) => void
}

const useCountStore = create<State & Actions>((set) => ({
count: 0,
increment: (qty: number) => set((state) => ({
count: state.count + qty
})),
decrement: (qty: number) => set((state) => ({
count: state.count - qty
})),
}));

const Component = () => {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
// ...
}

XState Store

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

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

const Component = () => {
const count = useSelector(store, (state) => state.context.count);
const increment = (qty) => store.send({ type: 'increment', qty });
const decrement = (qty) => store.send({ type: 'decrement', qty });
// ...
}