Skip to content
7 minute read

David Khourshid

Does the world need another state management library? Probably not, but if you've been interested in XState, you're going to want to check this one out.

XState Store is a simple & tiny state management library largely inspired by XState. If you just need a way to update data in a store and subscribe to changes in the store, XState Store is for you. It is:

  • Extremely simple. Provide initial context and transition functions to the createStore(…) function, and you're good to go.
  • Extremely small. Less than 1kb minified and gzipped.
  • XState compatible. Shares actor APIs with XState, making integration/migration easy if (when) you need to handle more complexity.
  • Extra type-safe. Written in TypeScript, it provides strong event and snapshot types, automatically inferred from your context and transitions.
  • Event-based. Works just like XState; send events to trigger transitions.
  • Immer ready. Easily add Immer for "mutable" context updates with createStoreWithProducer(producer, …).

Install via npm:

npm install @xstate/store

Create your store and use it anywhere:

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

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

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

store.send({ type: 'inc', by: 1 });
// logs { count: 1 }
store.send({ type: 'inc', by: 2 });
// logs { count: 3 }

Even in React:

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

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

return <button onClick={() => store.send({ type: 'inc', by: 1 })}>
{count}
</button>;
}

Motivation

There are many state management libraries out there, such as XState, Redux, MobX, Zustand, Pinia, and more. They generally fall under two categories: direct and indirect state manipulation.

  • Direct state manipulation is easiest, since you can directly update the state anywhere in your application, at any time. However, this can lead to bugs and unpredictable behavior, since logic is not centralized, and a lot of defensive programming is required.
  • Indirect state manipulation is simplest, since you can centralize all state manipulation in one place. This can be a little more verbose since you are sending/dispatching events (or "actions" in Redux lingo) to a centralized location, but it means that you have a single source of truth for your app logic. This central source makes testing, inspecting, debugging, and reusability much easier.

XState has taken the road less traveled, and has strongly pushed for indirect state manipulation, as it can scale better for more complex application logic. However, XState has a pretty big learning curve, since it also implements state machines, statecharts, and the actor model – all of which are new (and important!) concepts for many developers. Additionally, we have seen teams use XState not only for complex state management, but also for simple data updates, where using full state machines may be overkill.

Instead of directing developers to go outside of the XState ecosystem for simple state management, we've created @xstate/store, which shares the same principles as XState, has identical APIs, but is much simpler and easier to use. If you need to scale up to more complex state management, you can easily migrate to XState.

In summary, if you just need a way to update data in a store and subscribe to changes in a store and share that data with other parts of your application, use @xstate/store. If you need more complex state management, including finite states, effects (actions, invoked/spawned actors), use XState.

Feature@xstate/storexstate
Finite states
Context
Events
Transitions
Guards
Effects
Actor model

Super simple example

This is a contrived example to demonstrate the API.

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

// 1. Create a store
export const donutStore = createStore(
// Initial context data
{ donuts: 0, favoriteFlavor: 'chocolate' },

// Transitions
{
addDonut: {
donuts: (context) => context.donuts + 1
},
changeFlavor: {
favoriteFlavor: (context, event: { flavor: string }) => event.flavor
},
eatAllDonuts: {
donuts: 0
}
}
);

console.log(store.getSnapshot());
// {
// status: 'active',
// context: {
// donuts: 0,
// favoriteFlavor: 'chocolate'
// }
// }

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

// 3. Send events
store.send({ type: 'addDonut' });
// logs { donuts: 1, favoriteFlavor: 'chocolate' }

store.send({
type: 'changeFlavor',
flavor: 'strawberry' // Strongly-typed!
});
// logs { donuts; 1, favoriteFlavor: 'strawberry' }

Overall, the API is:

  1. Create a store using createStore(initialContext, transitions).
  2. Subscribe to updates from that store using store.subscribe(callback).
  3. Send events to trigger transitions using store.send(event).
  4. (Optional) Use store.getSnapshot() to get the current snapshot of the store.

Superpowers

We've packed a few nice features into @xstate/store to make state management as smooth sailing as possible. ⛵️

First and foremost, you get strong types out of the box for both the state context and events, without having to write any awkward generic type parameters. Of course, intellisense works well for events in store.send({ … }). Note that to make this magic work, TypeScript version 5.4 or higher is required.

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

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

store.send({
type: 'inc', // Strongly-typed!
by: 1 // Also strongly-typed!
});

// @ts-expect-error
store.send({ type: 'unknownEvent' });

Secondly, there are convenient ways to update context in a transition, similar to how you would do it with assign(…) in XState. You can:

  • Use an object to update specific context properties:
    const store = createStore({
    count: 0
    }, {
    inc: {
    count: (context, event: { by: number }) => context.count + event.by
    }
    });
  • Use an object to update context properties to static values:
    const store = createStore({
    count: 0
    }, {
    reset: {
    count: 0 // No function needed
    }
    });
  • Use a function to update the entire context (can be a partial or full update):
    const store = createStore({
    count: 0,
    greeting: 'Hello'
    }, {
    adios: (context) => ({ greeting: 'Goodbye' }) // Merged with { count }
    });

But if you want to make complex context updates even easier, you can easily use Immer by plugging in its producer function to createStoreWithProducer(producer, …):

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

const store = createStoreWithProducer(
produce,
{
todos: []
}, {
addTodo: (context, event: { todo: string }) => {
context.todos.push(event.todo);
}
});

What's next

New features are not planned for @xstate/store since it aims to remain small, simple, and focused. However, we would like to add integrations with other frameworks (such as Vue, Angular, Svelte, Solid, etc.) and would greatly appreciate community contributions for those. And we can't forget about examples; keep an eye out for those in the /examples directory of the XState repo, such as this small React counter example.

Other than that, the next thing for you to do is to try it out! If you've used Zustand, Redux, Pinia, or XState, you'll find @xstate/store very familiar. Please keep in mind that you should choose the state management library that best suits your requirements and your team's preferences. However, it is straightforward to migrate to (and from) @xstate/store to Redux, Zustand, Pinia, XState, or other state management libraries if needed.

Our goal with @xstate/store is to provide a simple yet powerful event-based state management solution that is type-safe. We believe that indirect (event-based) state management leads to better organization of application logic, especially as it grows in complexity, and @xstate/store is a great starting point for that approach.

Give it a try, and feel free to ask any questions in our Discord or report bugs in the XState GitHub repo. We're always looking for feedback on how we can improve the experience!

2 minute read

Laura Kalbag

Watch our latest office hours live stream where we cover single file GitHub pull requests, sorting machines, Stately Agent, updates to XState and more.

5 minute read

David Khourshid

XState is a versatile state management & orchestration library that works with any framework, including React with the @xstate/react package. For many apps, managing global state is a requirement, and there are many options for sharing global state in React, like using React Context or libraries like Redux, MobX, and Zustand.

The @xstate/react package makes it simple to manage component-level state with hooks like useMachine() and useActor(), but it works equally well for managing global state 🌎

17 minute read

Kevin Maes

We recently announced the release of XState v5! During its beta phase, we created a migration guide, specifically to call out breaking changes and to give developers onging updates regarding API changes. This post is a walkthrough of migrating existing XState machines from v4 to v5 and is intended to be more of a step-by-step companion to the migration guide. It also focuses on migrating XState machines that are using TypeScript.

10 minute read

Kevin Maes

State machines and visual diagrams are such a powerful way to organize, and convey information. All of those lovely “boxes and arrows” convey meaningful relationships, indicate sequential order, and direct flows in a way that’s easier to understand since it's visual. Add to that the ability to attach assets to your diagrams and you’re well on your way towards creating truly expressive, executable software diagrams. But there’s still one thing that state machines have that should make them easy to understand. Text.