@xstate/store
Version 3.x (Version 2.x docs)
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
- pnpm
- yarn
npm install @xstate/store
pnpm install @xstate/store
yarn add @xstate/store
Quick start​
import { createStore } from '@xstate/store';
const store = createStore({
// Initial context
context: { count: 0, name: 'David' },
// Transitions
on: {
inc: (context) => ({
...context,
count: context.count + 1,
}),
add: (context, event: { num: number }) => ({
...context,
count: context.count + event.num,
}),
changeName: (context, event: { newName: string }) => ({
...context,
name: 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 (traditional way)
store.send({ type: 'inc' });
// logs { count: 1, name: 'David' }
// Send an event using the fluent trigger API
store.trigger.add({ num: 10 });
// logs { count: 11, name: 'David' }
store.trigger.changeName({ newName: 'Jenny' });
// logs { count: 11, name: 'Jenny' }
Creating a store​
To create a store, pass a configuration object to the createStore(…)
function with:
- The initial
context
- 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,
}),
},
});
Effects and Side Effects​
You can enqueue effects in state transitions using the enqueue
argument:
import { createStore } from '@xstate/store';
const store = createStore({
context: { count: 0 },
on: {
incrementDelayed: (context, event, enqueue) => {
enqueue.effect(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
store.send({ type: 'increment' });
});
return context;
},
increment: (context) => ({
...context,
count: context.count + 1,
}),
},
});
Emitting Events​
You can emit events from transitions by defining them in the emits
property and using enqueue.emit
:
import { createStore } from '@xstate/store';
const store = createStore({
context: { count: 0 },
emits: {
increased: (payload: { by: number }) => {
// Optional side effects can go here
},
},
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}`);
});
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.snapshot' or
// type: '@xstate.event'
console.log(inspectionEvent);
});
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​
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(produce, {
context: { count: 0, todos: [] },
on: {
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({
context: { count: 0, name: 'David' },
on: {
inc: (context) => ({
...context,
count: 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>
<button onClick={() => store.trigger.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.
Usage with Solid​
Documentation coming soon!
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({
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:
- Use
createMachine(…)
(imported fromxstate
) instead ofcreateStore(…)
(imported from@xstate/store
) to create a state machine. - Wrap the assignments in an
assign(…)
action creator (imported fromxstate
) and move that to theactions
property of the transition. - Destructure
context
andevent
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(
// { count: 0, name: 'David' },
// {
// 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​
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({
context: {
count: 0,
},
on: {
increment: (context, { qty }: { qty: number }) => ({
...context,
count: context.count + qty,
}),
decrement: (context, { qty }: { qty: number }) => ({
...context,
count: context.count - qty,
}),
},
});
const Component = () => {
const count = useSelector(store, (state) => state.context.count);
const { increment, decrement } = store.trigger;
// ...
};