# `@xstate/store`

XState Store is a library for **simple event-based state management**. If you want a state management library that allows you to update a store's state via events, `@xstate/store` is a great option. If you need more complex application logic needs, like state machines/statecharts, effects, communicating actors, and more, consider [using XState instead](https://github.com/statelyai/xstate).

<!-- store capabilities from src/index.ts and runtime dependency requirements from package.json -->

- **Extremely simple**: transitions update state via events, just like Redux, Zustand, Pinia, etc.
- **Small**: no required runtime dependencies
- **XState compatible**: use it with (or without) XState, or convert to XState machines when you need to handle more complex logic & effects.
- **Extra type-safe**: great typing out of the box, with strong inference and no awkwardness.

> [!NOTE]
> This readme is written for [TypeScript](#typescript) users. If you are a JavaScript user, just remove the types.

## Installation

```bash
# yarn add @xstate/store
# pnpm add @xstate/store
npm install @xstate/store
```

## Quick start

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

export const donutStore = createStore({
  context: {
    donuts: 0,
    favoriteFlavor: 'chocolate'
  },
  on: {
    addDonut: (context) => ({
      ...context,
      donuts: context.donuts + 1
    }),
    changeFlavor: (context, event: { flavor: string }) => ({
      ...context,
      favoriteFlavor: event.flavor
    }),
    eatAllDonuts: (context) => ({
      ...context,
      donuts: 0
    })
  }
});

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

// Equivalent to
// donutStore.send({ type: 'addDonut' });
donutStore.trigger.addDonut();
// => { donuts: 1, favoriteFlavor: 'chocolate' }

// donutStore.send({
//   type: 'changeFlavor',
//   flavor: 'strawberry' // Strongly-typed!
// });
donutStore.trigger.changeFlavor({ flavor: 'strawberry' });
// => { donuts: 1, favoriteFlavor: 'strawberry' }
```

## Checking events

<!-- store.can API from packages/xstate-store/src/types.ts -->

Use `store.can` to check whether an event is allowed without updating the store:

```ts
const store = createStore({
  context: { count: 0 },
  on: {
    increment: (context, event: { by: number }) => {
      if (context.count + event.by > 10) {
        return;
      }

      return {
        count: context.count + event.by
      };
    }
  }
});

store.can.increment({ by: 4 });
// => true
```

Returning `undefined` marks the event as not allowed. Returning the same context
object is still allowed, and transitions that enqueue effects are allowed.

## Persistence

<!-- persist and flushStorage behavior from src/persist.ts -->

Use the `persist` extension to save state after events are committed. Calling
`store.can` or the pure `store.transition` method does not schedule writes.
Writes preserve commit order, including events triggered synchronously by effects
or subscriptions. A newer eligible event can supersede an older pending write;
`onDone` runs for writes actually performed. Asynchronous writes execute in event
order for each store. Call
`await flushStorage(store)` to write buffered changes and wait for queued writes
to finish. Synchronous storage adapters continue to write synchronously.

With throttling, `pick` runs once per write on the latest buffered context.
`clearStorage(store)` cancels buffered writes and removes saved data after any
already queued writes finish; await its result when using asynchronous storage.
New events sent after clearing can persist new state. For asynchronous storage,
call `await rehydrateStore(store)` to load saved state; initial read failures are
reported through `onError`.

```ts
import { persist, flushStorage } from '@xstate/store/persist';

const savedDonutStore = donutStore.with(persist({ name: 'donuts' }));
savedDonutStore.trigger.addDonut();
await flushStorage(savedDonutStore);
```

### Undo and persistence

<!-- snapshot undo restoration behavior from src/undo.ts -->

Snapshot-based `undoRedo` restores historical state while preserving live
extension metadata, including persistence hydration. A custom `restore` callback
can enqueue events; their updated extension metadata is preserved too. Apply
`persist` after `undoRedo` when undo and redo themselves should write to storage:

```ts
import { undoRedo } from '@xstate/store/undo';

const undoableDonutStore = donutStore
  .with(undoRedo({ strategy: 'snapshot' }))
  .with(persist({ name: 'undoable-donuts' }));
```

## Atom updates and subscriptions

<!-- writable updater tracking and subscriber errors from src/atom.ts -->

Reads inside a writable atom's `set(previous => next)` updater do not create
reactive dependencies. Use a computed atom getter to track other atoms instead.
If a synchronous subscriber throws, other queued subscribers still receive their
notifications before the first error is rethrown to the caller. The atom's value
has already changed; later updates continue to notify subscribers normally.

## Source atoms

<!-- createSourceAtom snapshot and observed dependency lifecycle from src/atom.ts and src/types.ts -->

Use `createSourceAtom` for a read-only external value. Direct and derived
subscribers share one listener; cleanup runs synchronously when the last consumer
leaves. Plain reads do not start listeners. Resubscribing reconnects and refreshes
the snapshot.

```ts
import { createAtom, createSourceAtom } from '@xstate/store';

const media = window.matchMedia('(prefers-color-scheme: dark)');
const isDark = createSourceAtom({
  getSnapshot: () => media.matches,
  subscribe: (notify) => {
    media.addEventListener('change', notify);
    return () => media.removeEventListener('change', notify);
  }
});
const theme = createAtom(() => (isDark.get() ? 'dark' : 'light'));
const subscription = theme.subscribe(console.log);
subscription.unsubscribe(); // Removes the media query listener.
```

See [source atoms](./docs/source-atoms.md) for snapshot comparison, conditional
dependencies, and lifecycle timing. Use XState actors for commands, retries, or
reconnection logic.

## Async atoms

<!-- createAsyncAtom dependency and cancellation behavior from src/atom.ts -->

`createAsyncAtom` loads a value lazily and exposes a `pending`, `done`, or
`error` state. Atoms read synchronously by its getter remain dependencies after
the request succeeds or fails. When a dependency changes, subscribed async atoms
reload; otherwise, they reload on the next read. Read dependencies before the
first `await` to track them. Errors while delivering a resolved value (including
source activation) also become the async atom’s `error` state. If notifying that
error state fails, the state remains available through `.get()`.

```ts
import { createAtom, createAsyncAtom } from '@xstate/store';

const userId = createAtom('ada');
const user = createAsyncAtom(async ({ signal }) => {
  const id = userId.get();
  const response = await fetch(`/users/${id}`, { signal });
  return response.json();
});

user.subscribe((state) => {
  if (state.status === 'done') {
    console.log(state.data);
  }
});

userId.set('grace'); // Reloads even after the previous request settled.
```

Recomputing aborts the previous getter's signal and ignores its stale result.

## Usage with React

Import `useSelector` from `@xstate/store-react`. Select the data you want via `useSelector(…)` and send events using `store.send(eventObject)`:

```tsx
import { donutStore } from './donutStore.ts';
import { useSelector } from '@xstate/store-react';

function DonutCounter() {
  const donutCount = useSelector(donutStore, (state) => state.context.donuts);

  return (
    <div>
      <button onClick={() => donutStore.send({ type: 'addDonut' })}>
        Add donut ({donutCount})
      </button>
    </div>
  );
}
```

## Usage with SolidJS

Import `useSelector` from `@xstate/store-solid`. Select the data you want via `useSelector(…)` and send events using `store.send(eventObject)`:

```tsx
import { donutStore } from './donutStore.ts';
import { useSelector } from '@xstate/store-solid';

function DonutCounter() {
  const donutCount = useSelector(donutStore, (state) => state.context.donuts);

  return (
    <div>
      <button onClick={() => donutStore.send({ type: 'addDonut' })}>
        Add donut ({donutCount()})
      </button>
    </div>
  );
}
```

## Usage with Immer

XState Store works well with immutable update libraries like [Immer](https://github.com/immerjs/immer) or [Mutative](https://github.com/unadlib/mutative). Use `produce(...)` inside your transition functions:

```ts
import { createStore } from '@xstate/store';
import { produce } from 'immer'; // or { create } from 'mutative'

const donutStore = createStore({
  context: {
    donuts: 0,
    favoriteFlavor: 'chocolate'
  },
  on: {
    addDonut: (context) =>
      produce(context, (draft) => {
        draft.donuts++;
      }),
    changeFlavor: (context, event: { flavor: string }) =>
      produce(context, (draft) => {
        draft.favoriteFlavor = event.flavor;
      }),
    eatAllDonuts: (context) =>
      produce(context, (draft) => {
        draft.donuts = 0;
      })
  }
});
```

If a transition should be unavailable, return `undefined` from the transition
before calling `produce(...)`:

```ts
on: {
  eatDonut: (context) => {
    if (context.donuts === 0) {
      return;
    }

    return produce(context, (draft) => {
      draft.donuts--;
    });
  };
}
```

Immer treats a producer that returns `undefined` the same as a producer that
does not explicitly return anything. If you need `produce(...)` itself to return
`undefined`, return Immer's `nothing` token from the producer:

```ts
import { nothing, produce } from 'immer';

on: {
  eatDonut: (context) =>
    produce(context, (draft) => {
      if (draft.donuts === 0) {
        return nothing;
      }

      draft.donuts--;
    });
}
```

## TypeScript

XState Store is written in TypeScript and provides full type safety, _without_ you having to specify generic type parameters. The `context` type is inferred from the initial context object, and the event types are inferred from the event object payloads you provide in the transition functions.

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

const donutStore = createStore({
  // Context inferred as:
  // {
  //   donuts: number;
  //   favoriteFlavor: string;
  // }
  context: {
    donuts: 0,
    favoriteFlavor: 'chocolate'
  },
  on: {
    // Event inferred as:
    // {
    //   type: 'changeFlavor';
    //   flavor: string;
    // }
    changeFlavor: (context, event: { flavor: string }) => {
      context.favoriteFlavor = event.flavor;
    }
  }
});

donutStore.getSnapshot().context.favoriteFlavor; // string
donutStore.get().context.favoriteFlavor; // same snapshot, readable/tracked read

donutStore.send({
  type: 'changeFlavor', // Strongly-typed from transition key
  flavor: 'strawberry' // Strongly-typed from { flavor: string }
});
```

If you want to provide event or emitted-event types explicitly, you can use `schemas` with any library that implements the [Standard Schema](https://standardschema.dev/) interface. Schemas define the store's runtime-readable contract: the shape of its context, accepted events, and emitted events. Store uses schemas for type inference and metadata by default; it does not validate schema-declared values unless you opt in with `validateSchemas()`.

```ts
import { createStore } from '@xstate/store';
import { z } from 'zod';

const store = createStore({
  schemas: {
    context: z.object({
      donuts: z.number(),
      favoriteFlavor: z.string()
    }),
    events: {
      changeFlavor: z.object({
        flavor: z.string()
      })
    },
    emitted: {
      flavorChanged: z.object({
        flavor: z.string()
      })
    }
  },
  context: {
    donuts: 0,
    favoriteFlavor: 'chocolate'
  },
  on: {
    changeFlavor: (context, event, enqueue) => {
      enqueue.emit.flavorChanged({ flavor: event.flavor });
      return {
        ...context,
        favoriteFlavor: event.flavor
      };
    }
  }
});
```

Event and emitted-event schemas describe payload objects. Use an empty object
schema for events without payload:

```ts
schemas: {
  events: {
    reset: z.object({})
  },
  emitted: {
    reset: z.object({})
  }
}
```

### Validating schemas

Use the `validateSchemas()` extension when the store should validate its schema
contract at runtime:

```ts
import { createStore } from '@xstate/store';
import { validateSchemas } from '@xstate/store/validate';
import { z } from 'zod';

const store = createStore({
  schemas: {
    context: z.object({
      count: z.number()
    }),
    events: {
      increment: z.object({
        by: z.number()
      })
    },
    emitted: {
      increased: z.object({
        by: z.number()
      })
    }
  },
  context: { count: 0 },
  on: {
    increment: (context, event, enqueue) => {
      enqueue.emit.increased({ by: event.by });
      return { count: context.count + event.by };
    }
  }
}).with(validateSchemas());
```

`validateSchemas()` validates store macrosteps. It validates the event sent to
the store, the final context after the transition completes, and emitted events
before any effects execute. Events queued internally with `enqueue.trigger` are
processed as part of the same macrostep; their payloads are not separately
validated in this version.

Invalid `send(...)`, `trigger.*(...)`, or `transition(...)` calls throw a
`StoreValidationError`. `store.can.*(...)` always returns a boolean; validation
errors make it return `false`.

By default, unknown events and emitted events throw when the corresponding
schema map exists. Extension-added event types are treated as known, even when
they do not have payload schemas. You can opt out:

```ts
const store = createStore({
  // ...
}).with(
  validateSchemas({
    unknownEvents: 'ignore',
    unknownEmitted: 'ignore'
  })
);
```

Use `store.getSnapshot()` when you want an explicit snapshot read from the store itself. Use `store.get()` when consuming the store as a `Readable` value in tracked or reactive code.

If you want to make the `context` type more specific, you can strongly type the `context` outside of `createStore(…)` and pass it in:

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

interface DonutContext {
  donuts: number;
  favoriteFlavor: 'chocolate' | 'strawberry' | 'blueberry';
}

const donutContext: DonutContext = {
  donuts: 0,
  favoriteFlavor: 'chocolate'
};

const donutStore = createStore({
  context: donutContext,
  on: {
    // ... (transitions go here)
  }
});
```

## Effects and Side Effects

You can enqueue effects in state transitions using the `enqueue` argument:

```ts
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 declaring them in `schemas.emitted` and using `enqueue.emit`:

```ts
import { createStore } from '@xstate/store';
import { z } from 'zod';

const store = createStore({
  schemas: {
    emitted: {
      increased: z.object({
        by: z.number()
      })
    }
  },
  context: { count: 0 },
  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}`);
});
```
