NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #4208 most downloaded on npm
Finite State Machines and Statecharts for the Modern Web.
Last release 4 days ago
17 Sep 2026
Ships on a steady schedule
a new release about every 9 days
Nearly every release is documented
notes for 60 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
9 years old
334 releases · first in 2017
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
One column per quarter.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
```ts const store = createStore({ context: { count: 0 }, on: { inc: (ctx) => ({ count: ctx.count + 1 }) }, }).with(undoRedo({ strategy: "snapshot" }))
#5624 d146eaa Thanks @Jaybhade! - Fixed snapshot-based undo/redo so that undoing after a redo steps back through history instead of staying put.
const store = createStore({
context: { count: 0 },
on: { inc: (ctx) => ({ count: ctx.count + 1 }) },
}).with(undoRedo({ strategy: "snapshot" }));
store.trigger.inc(); // 1
store.trigger.inc(); // 2
store.trigger.inc(); // 3
store.trigger.undo(); // 2
store.trigger.undo(); // 1
store.trigger.redo(); // 2
store.trigger.undo(); // 1 (previously stayed at 2)
`ts const undoableStore = store.with( undoRedo({ strategy: 'snapshot', restore: ({ current, next }) => ({ ...next, viewport: current.viewport }) }) );
#5608 fbcbd5a Thanks @davidkpiano! - Allow snapshot-based undo and redo to customize restored context while preserving complete history snapshots. Restoration can enqueue emitted events, effects, and store triggers.
const undoableStore = store.with(
undoRedo({
strategy: 'snapshot',
restore: ({ current, next }) => ({
...next,
viewport: current.viewport
})
})
);
`ts const counterLogic = createStoreLogic({ context: (input: { initialCount: number }) => ({ count: input.initialCount }), selectors: { count: (contex
#5563 2911278 Thanks @davidkpiano! - Fixed inference for selector context parameters when using createStoreLogic(...) with an input function.
const counterLogic = createStoreLogic({
context: (input: { initialCount: number }) => ({
count: input.initialCount
}),
selectors: {
count: (context) => context.count
},
on: {
inc: (context) => ({ count: context.count + 1 })
}
});
`ts const storeLogic = createStoreLogic({ context: () => ({ foo: null, loading: false }), on: { fetchFoo: (context, event, enq) => { enq.effect(({ tri
#5537 28c9961 Thanks @davidkpiano! - Effects enqueued via enq.effect(...) now receive an enqueue object with trigger, send, and getSnapshot, so you can dispatch events back into the store after async work and read the latest state — without needing a reference to the store. This is especially useful with createStoreLogic(...), where the store is created per-instance and there's no store to close over.
const storeLogic = createStoreLogic({
context: () => ({ foo: null, loading: false }),
on: {
fetchFoo: (context, event, enq) => {
enq.effect(({ trigger }) => {
myApi.requestFoo().then((response) => trigger.gotFoo({ response }));
});
return { ...context, loading: true };
},
gotFoo: (context, event) => ({
...context,
foo: event.response,
loading: false
})
}
});
Use trigger for fully-typed dispatch; send is a loosely-typed escape hatch for dynamically-constructed events. After an await, the context argument is stale — use getSnapshot() to read the current state:
enq.effect(async ({ getSnapshot, trigger }) => {
await someAsyncWork();
if (getSnapshot().context.loading) {
trigger.done();
}
});
Previously even when input was defined inside types, useActor, useMachine, and useActorRef would not make the input required:
#5055 ad38c35c37 Thanks @SandroMaglione! - Updated types of useActor, useMachine, and useActorRef to require input when defined inside types/input.
Previously even when input was defined inside types, useActor, useMachine, and useActorRef would not make the input required:
const machine = setup({
types: {
input: {} as { value: number }
}
}).createMachine({});
function App() {
// Event if `input` is not defined, `useMachine` works at compile time, but risks crashing at runtime
const _ = useMachine(machine);
return <></>;
}
With this change the above code will show a type error, since input is now required:
const machine = setup({
types: {
input: {} as { value: number }
}
}).createMachine({});
function App() {
const _ = useMachine(machine, {
input: { value: 1 } // Now input is required at compile time!
});
return <></>;
}
This avoids runtime errors when forgetting to pass input when defined inside types.
```tsx import { createStore } from '@xstate/store'; import { useSelector } from '@xstate/react';
#4844 5aa6eb05c Thanks @davidkpiano! - The useSelector(…) hook from @xstate/react is now compatible with stores from @xstate/store.
import { createStore } from '@xstate/store';
import { useSelector } from '@xstate/react';
const store = createStore(
{
count: 0
},
{
inc: {
count: (context) => context.count + 1
}
}
);
function Counter() {
// Note that this `useSelector` is from `@xstate/react`,
// not `@xstate/store/react`
const count = useSelector(store, (state) => state.context.count);
return (
<div>
<button onClick={() => store.send({ type: 'inc' })}>{count}</button>
</div>
);
}
```ts const store = createStore({ schemas: { context: z.object({ count: z.number() }), events: { inc: z.object({ by: z.number() }) } }, context: { cou
#5530 0502c04 Thanks @davidkpiano! - Expose store.schemas so integrations can read the store's context, event, and emitted event schemas at runtime.
const store = createStore({
schemas: {
context: z.object({ count: z.number() }),
events: {
inc: z.object({ by: z.number() })
}
},
context: { count: 0 },
on: {
inc: (context, event) => ({ count: context.count + event.by })
}
});
store.schemas?.events?.inc;
#4231 c2402e7bc Thanks @davidkpiano! - The actor passed to useSelector(actor, selector) is now allowed to be undefined for an actor that may not exist yet. For actors that may be undefined, the snapshot provided to the selector function can also be undefined:
const count = useSelector(maybeActor, (snapshot) => {
// `snapshot` may be undefined
return snapshot?.context.count;
});
count; // number | undefined
Updated dependencies \[`bf6119a7310a878afbf4f5b01f5e24288f9a0f16`]:
bf6119a7310a878afbf4f5b01f5e24288f9a0f16]:
55ffd698419ea7259506bb0fa4bba9c7f592823e Thanks @lendle! - Add Svelte 5 to the allowed peer dependency ranged7f220225 Thanks @davidkpiano! - Fix an issue where after transitions do not work in React strict mode. Delayed events (including from after transitions) should now work as expected in all React modes.The deprecated config-wrapping form of undoRedo(...) was removed. Use the extension form instead:
#5512 063416d Thanks @davidkpiano! - Remove createStoreWithProducer. Use (ctx, ev) => produce(ctx, draft => …) in createStore event handlers instead.
#5512 063416d Thanks @davidkpiano! - Added enq.trigger for enqueueing store events from transitions.
const store = createStore({
schemas: {
events: {
inc: z.object({}),
incTwice: z.object({})
}
},
context: { count: 0 },
on: {
inc: (context) => ({ count: context.count + 1 }),
incTwice: (context, _event, enq) => {
enq.trigger.inc();
enq.trigger.inc();
return context;
}
}
});
#5512 063416d Thanks @davidkpiano! - Modernize Store v4 package entrypoints.
Use framework-specific packages such as @xstate/store-react and @xstate/store-solid instead of @xstate/store/react or @xstate/store/solid. The Store packages now publish ESM package entrypoints.
#5512 063416d Thanks @davidkpiano! - Add createStoreLogic(...) for reusable store definitions, and support creating stores from logic in framework hooks.
const counterLogic = createStoreLogic({
context: (input: { initialCount: number }) => ({
count: input.initialCount
}),
on: {
inc: (context) => ({ count: context.count + 1 })
}
});
const store = useStore(counterLogic, { initialCount: 0 });
If a store logic requires input, the input argument is also required:
useStore(counterLogic, { initialCount: 0 });
Framework hooks also preserve schema-derived context, event, and emitted event types when creating stores from config objects.
#5512 063416d Thanks @davidkpiano! - Update persist(...) helpers to support async storage results.
clearStorage(...) and flushStorage(...) may return a promise when the configured storage is async.
#5512 063416d Thanks @davidkpiano! - Remove deprecated Store APIs.
The deprecated config-wrapping form of undoRedo(...) was removed. Use the extension form instead:
const store = createStore({
context: { count: 0 },
on: {
inc: (context) => ({ count: context.count + 1 })
}
}).with(undoRedo());
Computed atoms now receive only the previous value. Read other atoms directly with .get():
-const doubled = createAtom((read) => read(countAtom) * 2);
+const doubled = createAtom(() => countAtom.get() * 2);
-const accumulated = createAtom((read, prev) => read(countAtom) + (prev ?? 0));
+const accumulated = createAtom((prev) => countAtom.get() + (prev ?? 0));
#5512 063416d Thanks @davidkpiano! - Add Standard Schema support to store configs and fromStore(...).
Schemas can type context, accepted events, and emitted events without enabling runtime validation by default. To validate schema-declared values at runtime, use the new validateSchemas() extension from @xstate/store/validate.
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() })
}
},
context: { count: 0 },
on: {
increment: (context, event) => ({
count: context.count + event.by
})
}
}).with(validateSchemas());
#5512 063416d Thanks @davidkpiano! - Pass an AbortSignal to createAsyncAtom(...) getters and ignore stale async results after recomputation.
const user = createAsyncAtom(async ({ signal }) => {
const response = await fetch('/user', { signal });
return response.json();
});
#5512 063416d Thanks @davidkpiano! - Add reusable atom configs and framework atom-state helpers.
createAtomConfig(...) creates an inert atom definition that can be instantiated with its createAtom(...) method or React/Preact/Vue/Solid's useAtomState(...). These helpers return the current framework-native value and live atom instance, and also work with existing atom instances.
const countConfig = createAtomConfig((input: { initialCount: number }) => {
return input.initialCount;
});
function Counter() {
const [count, countAtom] = useAtomState(countConfig, { initialCount: 0 });
return (
<button onClick={() => countAtom.set((count) => count + 1)}>
{count}
</button>
);
}
#5512 063416d Thanks @davidkpiano! - Add broadcast-aware storage helpers for persisted stores.
Use createBroadcastStorage(...) with persist(...) and subscribeToBroadcastStorage(...) to rehydrate a persisted store when another tab or window writes to the same storage key.
#5512 063416d Thanks @davidkpiano! - Add store.can for checking whether an event is allowed without updating the store.
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
store.can.increment({ by: 11 }); // false
#5512 063416d Thanks @davidkpiano! - Add createReducerAtom(...) for reducer-driven atoms.
const count = createReducerAtom(0, (state, event: { type: 'inc' }) => {
if (event.type === 'inc') {
return state + 1;
}
return state;
});
count.send({ type: 'inc' });
8c4b70652acaef2702f32435362e4755679a516d]:
#3947 5fa3a0c74 Thanks @davidkpiano! - Removed the ability to pass a factory function as argument to useMachine.
#4006 42df9a536 Thanks @davidkpiano! - useActorRef is introduced, which returns an ActorRef from actor logic:
const actorRef = useActorRef(machine, { ... });
const anotherActorRef = useActorRef(fromPromise(...));
is deprecated in favor of useMachineuseActor, which works with machines and any other kind of logic
-const [state, send] = useMachine(machine);
+const [state, send] = useActor(machine);
const [state, send] = useActor(fromTransition(...));
is removed in favor of useSpawnuseActorRef
-const actorRef = useSpawn(machine);
+const actorRef = useActorRef(machine);
The previous use of `useActor(actorRef)` is now replaced with just using the `actorRef` directly, and with `useSelector`:
```diff
-const [state, send] = useActor(actorRef);
+const state = useSelector(actorRef, s => s);
// actorRef.send(...)
#4050 fc88dc8e6 Thanks @davidkpiano! - The options prop has been added (back) to the Context.Provider component returned from createActorContext:
const SomeContext = createActorContext(someMachine);
// ...
<SomeContext.Provider options={{ input: 42 }}>
{/* ... */}
</SomeContext.Provider>;
#4006 42df9a536 Thanks @davidkpiano! - useActor has been removed from the created actor context, you should be able to replace its usage with MyCtx.useSelector and MyCtx.useActorRef.
#4265 1153b3f9a Thanks @davidkpiano! - FSM-related functions have been removed.
#3947 5fa3a0c74 Thanks @davidkpiano! - Implementations for machines on useMachine hooks should go directly on the machine via machine.provide(...), and are no longer allowed to be passed in as options.
-const [state, send] = useMachine(machine, {
- actions: {
- // ...
- }
-});
+const [state, send] = useMachine(machine.provide({
+ actions: {
+ // ...
+ }
+}));
#3148 7a68cbb61 Thanks @davidkpiano! - Removed getSnapshot parameter from hooks. It is expected that the received actorRef has to have a getSnapshot method on it that can be used internally.
5fb3c683d Thanks @Andarist! - exports field has been added to the package.json manifest. It limits what files can be imported from a package - it's no longer possible to import from files that are not considered to be a part of the public API.409552cf8 Thanks @davidkpiano! - The useMachine function is an alias of useActor.340aee643 Thanks @Andarist! - Fast refresh now works as expected for most use-cases.fc88dc8e6 Thanks @davidkpiano! - The observerOrListener argument has been removed from the 3rd argument of createActorContext(logic, options).Your coding agent can read these notes before it upgrades. Set up the MCP server →