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/store | xstate |
---|---|---|
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:
- Create a store using
createStore(initialContext, transitions)
. - Subscribe to updates from that store using
store.subscribe(callback)
. - Send events to trigger transitions using
store.send(event)
. - (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!