Migrating to v4
XState Store v4 removes deprecated APIs, introduces schemas, and tightens the public API around framework packages and extensions.
Removed APIs
createStore(context, transitions) - Use the config object form:
- const store = createStore(
- { count: 0 },
- {
- inc: (context) => ({ count: context.count + 1 })
- }
- );
+ const store = createStore({
+ context: { count: 0 },
+ on: {
+ inc: (context) => ({ count: context.count + 1 })
+ }
+ });createStoreWithProducer - Call your producer inside transition handlers:
- import { createStoreWithProducer } from '@xstate/store';
+ import { createStore } from '@xstate/store';
import { produce } from 'immer';
- const store = createStoreWithProducer(produce, {
+ const store = createStore({
context: { count: 0 },
on: {
- inc: (context) => {
- context.count++;
- }
+ inc: (context) =>
+ produce(context, (draft) => {
+ draft.count++;
+ })
}
});When using Immer, return undefined from the transition before calling produce(...) when the event should not result in a transition (store.can.someEvent() returns false). If you need produce(...) itself to return undefined, return Immer's nothing token from the producer.
@xstate/store/react and @xstate/store/solid - Use dedicated framework packages:
- import { useSelector } from '@xstate/store/react';
+ import { useSelector } from '@xstate/store-react';undoRedo(config) - Use the extension form with .with(...):
- const store = createStore(
- undoRedo({
- context: { count: 0 },
- on: {
- inc: (context) => ({ count: context.count + 1 })
- }
- })
- );
+ const store = createStore({
+ context: { count: 0 },
+ on: {
+ inc: (context) => ({ count: context.count + 1 })
+ }
+ }).with(undoRedo());store._snapshot - Use store.getSnapshot() or store.get().
Changed APIs
emits moved to schemas.emitted - The type declaration moved; emitting with enq.emit is unchanged:
const store = createStore({
context: { count: 0 },
- emits: {
- increased: (_payload: { by: number }) => {}
- },
+ schemas: {
+ emitted: {
+ increased: z.object({ by: z.number() })
+ }
+ },
on: {
inc: (context, event: { by: number }, enq) => {
enq.emit.increased({ by: event.by });
return { count: context.count + event.by };
}
}
});Computed atom getters no longer receive read. The first argument is now the previous value of the atom. Call other atoms with .get() directly:
- const doubled = createAtom((read) => read(countAtom) * 2);
+ const doubled = createAtom(() => countAtom.get() * 2);
- const withPrev = createAtom((read, prev) => read(countAtom) + (prev ?? 0));
+ const withPrev = createAtom((prev) => countAtom.get() + (prev ?? 0));store.inspect(...) emits one event - Now emits a single @xstate.transition event for simplicity instead of separate @xstate.actor, @xstate.event, and @xstate.snapshot events:
store.inspect((inspectionEvent) => {
- // '@xstate.actor' | '@xstate.event' | '@xstate.snapshot'
+ // '@xstate.transition'
+ inspectionEvent.event;
+ inspectionEvent.snapshot;
});persist(...) writes snapshots - persist(...) now derives the persisted value from the full store snapshot. clearStorage(store) and flushStorage(store) may return a Promise when the storage adapter is async.