W↓
All docs
🔑
Sign Up/Sign In
stately.ai/docs/xstate-store#atoms
Public Link
Jun 17, 2025, 2:59:02 AM - complete - 22 kB
Created by:
****ad@vlad.studio
Starting URLs:
https://stately.ai/docs/xstate-store#atoms
Crawl Prefixes:
https://stately.ai/docs/xstate-store#atoms
## Page: https://stately.ai/docs/xstate-store#atoms **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 ## 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 changesstore.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 APIstore.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: 1. The initial `context` 2. 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 eventsstore.on('increased', (event) => { console.log(`Count increased by ${event.by}`);}); ## Pure transitions​ You can use `store.transition(state, event)` to compute a tuple of the next state and any effects from a given state and event. This is useful for debugging and testing, or for having full control over the state transitions in your application. const store = createStore({ context: { count: 0 }, emits: { incremented: (payload: { by: number }) => {}, }, on: { inc: (context, event: { by: number }, enqueue) => { enqueue.emit.incremented({ by: event.by }); enqueue.effect(async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); store.send({ type: 'increment' }); }); return { ...context, count: context.count + event.by, }; }, },});const snapshot = store.getSnapshot();const [nextState, effects] = store.transition(snapshot, { type: 'inc', by: 1,});console.log(nextState.context);// => { count: 1 }console.log(effects);// => [// { type: 'incremented', by: 1 },// Function// ]// The store's state is unchangedconsole.log(store.getSnapshot().context);// => { count: 0 } If you need to determine the next state from the store's initial state, you can get the initial snapshot using `store.getInitialSnapshot()`: const initialSnapshot = store.getInitialSnapshot();const [nextState, effects] = store.transition(initialSnapshot, { type: 'inc', by: 1,}); ## Selectors​ Store selectors provide an efficient way to select and subscribe to specific parts of your store's state. With store selectors, you can: * Get the current value of a specific part of state via `selector.get()` * Subscribe to changes of only that specific part via `selector.subscribe(observer)` * Optimize performance by only notifying subscribers when the selected value actually changes via `selector.subscribe(observer, equalityFn)` You can create a selector using `store.select(selector)`: import { createStore } from '@xstate/store';const store = createStore({ context: { position: { x: 0, y: 0 }, name: 'John', age: 30, }, on: { positionUpdated: ( context, event: { position: { x: number; y: number } }, ) => ({ ...context, position: event.position, }), },});// Create a selector for the positionconst position = store.select((context) => context.position);// Get current valueconsole.log(position.get()); // { x: 0, y: 0 }// Subscribe to changesposition.subscribe((position) => { console.log('Position updated:', position);});// When position updates, only position subscribers are notifiedstore.trigger.positionUpdated({ position: { x: 100, y: 200 } });// Logs: Position updated: { x: 100, y: 200 } ### Custom Equality Functions​ You can provide a custom equality function as the second argument to `store.select(selector, equalityFn)` to control when subscribers should be notified: const position = store.select( (state) => state.context.position, // Only notify if x coordinate changes (prev, next) => prev.x === next.x,); XState Store also provides a `shallowEqual` function that can be used as a default equality function: import { shallowEqual } from '@xstate/store';const position = store.select((state) => state.context.position, shallowEqual); ## Atoms​ An atom is a lightweight, reactive piece of state that can be read, written to, and subscribed to. Atoms can be used standalone or combined with other atoms and stores for more complex state management. You can: * Create an atom with `createAtom(initialValue)` * Read the atom's value with `atom.get()` * Subscribe to changes with `atom.subscribe(observer)` * Update the atom with `atom.set(value)` ### Creating Atoms​ Create an atom using `createAtom()` with an initial value: import { createAtom } from '@xstate/store';// Create an atom with a primitive valueconst countAtom = createAtom(0);// Create an atom with an objectconst userAtom = createAtom({ name: 'David', count: 100 }); ### Reading and Writing Atoms​ You can read an atom's value using `atom.get()` and update it using `atom.set()`: const countAtom = createAtom(0);// Read the current valueconsole.log(countAtom.get()); // 0// Set a new value directlycountAtom.set(1); // 1// Update value using a functioncountAtom.set((prev) => prev + 1); // 2const count = createAtom(0);count.get(); // 0count.set(1); // 1count.set((prev) => prev + 1); // 2// Recomputes when count changesconst laugh = createAtom(() => { return 'ha'.repeat(count.get());});laugh.subscribe((value) => { console.log(value);}); ### Subscribing to Changes​ Atoms support subscriptions to react to value changes: const countAtom = createAtom(0);// Subscribe to changesconst subscription = countAtom.subscribe((newValue) => { console.log('Count changed:', newValue);});countAtom.set(1); // Logs: "Count changed: 1"// Unsubscribe when donesubscription.unsubscribe();countAtom.set(2); // Does not log anything ### Combined Atoms​ You can create derived atoms that combine values from other atoms, stores, or selectors: const nameAtom = createAtom('David');const ageAtom = createAtom(30);// Combine multiple atomsconst userAtom = createAtom(() => ({ name: nameAtom.get(), age: ageAtom.get(),}));// Combined atoms are read-only and update automaticallyconsole.log(userAtom.get()); // { name: 'David', age: 30 }nameAtom.set('John');console.log(userAtom.get()); // { name: 'John', age: 30 }ageAtom.set(31);console.log(userAtom.get()); // { name: 'John', age: 31 } ### Async Atoms​ _Since v3.6.0_ Async atoms are a special type of atom that handle asynchronous values. They are created using `createAsyncAtom(…)` and take an async function that returns a promise. The atom's value represents the loading state of the async operation. The value of an async atom will be an object with a `status` property that can be: * `'pending'` - while the promise is resolving * `'done'` with a `data` property containing the resolved value * `'error'` with an `error` property containing the error that was thrown import { createAsyncAtom } from '@xstate/store';const userAtom = createAsyncAtom(async () => { const response = await fetch('/api/user'); return response.json();});userAtom.subscribe((snapshot) => { if (snapshot.status === 'pending') { console.log(snapshot); // { status: 'pending' } } else if (snapshot.status === 'done') { console.log(snapshot); // { status: 'done', data: { name: 'David', ... } } } else if (snapshot.status === 'error') { console.log(snapshot); // { status: 'error', error: Error('Failed to fetch') } }}); ### Working with Stores and Selectors​ Atoms can seamlessly integrate with XState stores and selectors: const store = createStore({ context: { count: 0 }, on: { increment: (context) => ({ ...context, count: context.count + 1 }), },});// Create an atom from a store selectorconst countSelector = store.select((state) => state.context.count);const doubleCountAtom = createAtom(() => 2 * countSelector.get());console.log(doubleCountAtom.get()); // 0store.trigger.increment();console.log(doubleCountAtom.get()); // 2 ### Using Atoms with React​ Atoms can be used with React using the `useSelector` hook: import { createAtom } from '@xstate/store';import { useSelector } from '@xstate/store/react';const countAtom = createAtom(0);function Counter() { const currentCount = useSelector(countAtom, (s) => s); return ( <div> <button onClick={() => countAtom.set((count) => count + 1)}> Increment </button> <div>Count: {currentCount}</div> </div> );} ## 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 eventssub.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​ You can use the `produce(…)` function from Immer to update the `context` in transitions: import { createStore } from '@xstate/store';import { produce } from 'immer';const store = createStore({ context: { count: 0, todos: [] }, on: { inc: (context, event: { by: number }) => produce(context, (draft) => { draft.count += event.by; }), addTodo: (context, event: { todo: string }) => produce(context, (draft) => { draft.todos.push(event.todo); }), // Not using a producer resetCount: (context) => ({ ...context, count: 0, }), },}); Deprecated: `createStoreWithProducer(…)` In previous versions of `@xstate/store`, you could use the `createStoreWithProducer(…)` function to pass in a producer function to update the `context` for every transition. This will not be supported in future versions of `@xstate/store`. Instead, you can use the `produce(…)` function from Immer or similar libraries directly with `createStore(…)`. import { createStore } from '@xstate/store';import { produce } from 'immer';// Deprecated APIconst 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); }, },});// ... ## 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 storeconst store = createStore({ context: { count: 0, name: 'David' }, on: { inc: (context) => ({ ...context, count: context.count + 1, }), },});// Use the `useSelector` hook to subscribe to the storefunction 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. ### Local Stores with `useStore()`​ The `useStore()` hook allows you to create local stores within React components, similar to `useReducer()`. This is useful when you want to manage component-specific state that doesn't need to be shared globally: import { useStore, useSelector } from '@xstate/store/react';function Counter({ initialCount = 0 }) { const store = useStore({ context: { count: initialCount, }, emits: { increased: (payload: { by: number }) => {}, }, on: { increment: (context, event: { by: number }, enqueue) => { enqueue.emit.increased({ by: event.by }); return { ...context, count: context.count + event.by }; }, }, }); const count = useSelector(store, (state) => state.context.count); return ( <div> <div>Count: {count}</div> <button onClick={() => store.trigger.increment({ by: 1 })}> Increment by 1 </button> <button onClick={() => store.trigger.increment({ by: 5 })}> Increment by 5 </button> </div> );} The store created by `useStore()` has all the same capabilities as a global store: * Send events using `store.trigger` or `store.send()` * Select state using `useSelector()` * Listen to emitted events * Use effects and side effects * Use selectors ### Listening to Emitted Events​ You can listen to events emitted by the local store via `useEffect(…)`: function Counter({ initialCount = 0, onIncreased,}: { initialCount?: number; onIncreased?: (by: number) => void;}) { const store = useStore({ // ... store config }); // Listen to emitted events useEffect(() => { const subscription = store.on('increased', ({ by }) => { onIncreased?.(by); }); return subscription.unsubscribe; }, [store, onIncreased]); // ... rest of component} ### Initializing with Props​ Local stores can be initialized using component props, making them more reusable: function Counter({ initialCount = 0, step = 1 }) { const store = useStore({ context: { count: initialCount, step, }, on: { increment: (context) => ({ ...context, count: context.count + context.step, }), }, }); // ... rest of component} ### Props and Store State​ Similar to `useState` and `useReducer`, changes to props after the initial render will not automatically update the store's state. The store's state can only be updated by sending events: function Counter({ currentCount = 0 }) { const store = useStore({ context: { // currentCount is only used once during initialization count: currentCount, }, on: { countUpdated: (context, event: { value: number }) => ({ ...context, count: event.value, }), }, }); // If you need to update the store when props change, // you'll need to send an event explicitly: useEffect(() => { store.trigger.countUpdated({ value: currentCount }); }, [store, currentCount]); // ... rest of component} This behavior ensures that state updates are always explicit and traceable through events, maintaining a predictable data flow in your application. ## 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: 1. Use `createMachine(…)` (imported from `xstate`) instead of `createStore(…)` (imported from `@xstate/store`) to create a state machine. 2. Wrap the assignments in an `assign(…)` action creator (imported from `xstate`) and move that to the `actions` property of the transition. 3. 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({ 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({// context: { count: 0, name: 'David' },// on: {// 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; // ...};