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 2 days ago
22 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
335 releases · first in 2017
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' });
One column per quarter.
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).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 [nextState, effects] = store.transition(store.getSnapshot(), { type: 'increment', by: 1 }); `
#5215 13279166ed9fa3d3626a2129bd257f6cd663fd0e Thanks @davidkpiano! - Added store.transition(state, event) method that returns the next state and effects for a given state and event as a tuple, without actually updating the store. This is useful for computing state changes before committing them, or controlling the execution of effects.
Example:
const [nextState, effects] = store.transition(store.getSnapshot(), {
type: 'increment',
by: 1
});
### Patch Changes - #5223 `9e1de554c4ebf49997b717fada540951d01f511c` Thanks @davidkpiano! - Added React 19 as a peer dependency.
9e1de554c4ebf49997b717fada540951d01f511c Thanks @davidkpiano! - Added React 19 as a peer dependency.store.select(selector) function to create a "selector" entity where you can:
#5200 0332a16a42fb372eb614df74ff4cb7f003c31fc8 Thanks @davidkpiano! - Added selectors to @xstate/store that enable efficient state selection and subscription:
store.select(selector) function to create a "selector" entity where you can:
.get().subscribe(callback)store.select(selector, equalityFn)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
})
}
});
const position = store.select((state) => state.context.position);
position.get(); // { x: 0, y: 0 }
position.subscribe((position) => {
console.log(position);
});
store.trigger.positionUpdated({ x: 100, y: 200 });
// Logs: { x: 100, y: 200 }
### Patch Changes - #4759 `f112081b9` Thanks @davidkpiano! - Fix send(…) type for useActor(…)
f112081b9 Thanks @davidkpiano! - Fix send(…) type for useActor(…)```tsx const storeConfig = createStoreConfig({ context: { count: 0 }, on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) } });
#5205 65784aef746b6249a9c3d71d9e4a7c9b454698c8 Thanks @davidkpiano! - Added createStoreConfig to create a store config from an object. This is an identity function that returns the config unchanged, but is useful for type inference.
const storeConfig = createStoreConfig({
context: { count: 0 },
on: { inc: (ctx) => ({ ...ctx, count: ctx.count + 1 }) }
});
// Reusable store config:
const store = createStore(storeConfig);
// ...
function Comp1() {
const store = useStore(storeConfig);
// ...
}
function Comp2() {
const store = useStore(storeConfig);
// ...
}
#5205 65784aef746b6249a9c3d71d9e4a7c9b454698c8 Thanks @davidkpiano! - There is now a useStore() hook that allows you to create a local component store from a config object.
import { useStore, useSelector } from '@xstate/store/react';
function Counter() {
const store = useStore({
context: {
name: 'David',
count: 0
},
on: {
inc: (ctx, { by }: { by: number }) => ({
...ctx,
count: ctx.count + by
})
}
});
const count = useSelector(store, (state) => state.count);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => store.trigger.inc({ by: 1 })}>
Increment by 1
</button>
<button onClick={() => store.trigger.inc({ by: 5 })}>
Increment by 5
</button>
</div>
);
}
65784aef746b6249a9c3d71d9e4a7c9b454698c8 Thanks @davidkpiano! - The createStoreWithProducer(config) function now accepts an emits object.#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 \[`479c74b83fa77c57c48f54cf0e9dcfab5fe6cae5`]:
479c74b83fa77c57c48f54cf0e9dcfab5fe6cae5]:
#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.
Updated dependencies \[`b453b2d72ba12d0fe46a995f9ccced8000fd0cc9`]:
b453b2d72ba12d0fe46a995f9ccced8000fd0cc9]:
#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>
);
}
1f2ccb97c Thanks @davidkpiano! - Typegen-based types for detecting missing implementations have been removed internally.Updated dependencies \[`d99df1d8f4fe49145c9974465b65028bf19b365f`]:
d99df1d8f4fe49145c9974465b65028bf19b365f]:
1f2ccb97c Thanks @davidkpiano! - Typegen-based types for detecting missing implementations have been removed internally.### Patch Changes - #5197 `5e05d5908093bfd3435dc2243e066e4e91b3ebc5` Thanks @davidkpiano! - The emitted event type can no longer be accidentally overr
5e05d5908093bfd3435dc2243e066e4e91b3ebc5 Thanks @davidkpiano! - The emitted event type can no longer be accidentally overridden in the emitted event payload. See #5196 for the issue.bf6119a7310a878afbf4f5b01f5e24288f9a0f16]:
f8cc116d3 Thanks @davidkpiano! - Actor options are now optional in useActor.f8cc116d3 Thanks @davidkpiano! - Marked useMachine as aliased, not deprecated.```ts // Before // createStore( // { // count: 0 // }, // { // increment: (context) => ({ count: context.count + 1 }) // } // );
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - The createStore function now only accepts a single configuration object argument. This is a breaking change that simplifies the API and aligns with the configuration pattern used throughout XState.
// Before
// createStore(
// {
// count: 0
// },
// {
// increment: (context) => ({ count: context.count + 1 })
// }
// );
// After
createStore({
context: {
count: 0
},
on: {
increment: (context) => ({ count: context.count + 1 })
}
});
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - You can now enqueue effects in state transitions.
const store = createStore({
context: {
count: 0
},
on: {
incrementDelayed: (context, event, enq) => {
enq.effect(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
store.send({ type: 'increment' });
});
return context;
},
increment: (context) => ({ count: context.count + 1 })
}
});
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - The fromStore(config) function now only supports a single config object argument.
const storeLogic = fromStore({
context: (input: { initialCount: number }) => ({
count: input.initialCount
}),
on: {
inc: (ctx, ev: { by: number }) => ({
...ctx,
count: ctx.count + ev.by
})
}
});
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - The createStoreWithProducer(…) function now only accepts two arguments: a producer and a config ({ context, on }) object.
// Before
// createStoreWithProducer(
// producer,
// {
// count: 0
// },
// {
// increment: (context) => {
// context.count++;
// }
// }
// );
// After
createStoreWithProducer(producer, {
context: {
count: 0
},
on: {
increment: (context) => {
context.count++;
}
}
});
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - Only complete assigner functions that replace the context fully are supported. This is a breaking change that simplifies the API and provides more type safety.
const store = createStore({
context: {
items: [],
count: 0
},
on: {
- increment: { count: (context) => context.count + 1 }
- increment: (context) => ({ count: context.count + 1 })
+ increment: (context) => ({ ...context, count: context.count + 1 })
}
})
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - Emitted event types are now specified in functions on the emits property of the store definition:
const store = createStore({
// …
emits: {
increased: (payload: { upBy: number }) => {
// You can execute a side-effect here
// or leave it empty
}
},
on: {
inc: (ctx, ev: { by: number }, enq) => {
enq.emit.increased({ upBy: ev.by });
// …
}
}
});
#5175 38aa9f518ee2f9a5f481306a1dc68c0ad47d28d5 Thanks @davidkpiano! - Added store.trigger API for sending events with a fluent interface:
const store = createStore({
context: { count: 0 },
on: {
increment: (ctx, event: { by: number }) => ({
count: ctx.count + event.by
})
}
});
// Instead of manually constructing event objects:
store.send({ type: 'increment', by: 5 });
// You can now use the fluent trigger API:
store.trigger.increment({ by: 5 });
The trigger API provides full type safety for event names and payloads, making it easier and safer to send events to the store.
8c4b70652acaef2702f32435362e4755679a516d]:
#4288 cfdf754f8 Thanks @davidkpiano! - The useMachine(machine) hook now returns { snapshot, send, actorRef } instead of { state, send, service }:
const {
- state,
+ snapshot,
send,
- service
+ actorRef
} = useMachine(machine);
#3148 7a68cbb61 Thanks @davidkpiano! - Removed getSnapshot parameter from composables. It is expected that the received actorRef has to have a getSnapshot method on it that can be used internally.
#4265 1153b3f9a Thanks @davidkpiano! - FSM-related functions have been removed.
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.cfdf754f8 Thanks @davidkpiano! - The useInterpret(machine) and useSpawn(machine) hooks have been removed; use the useActorRef(machine) hook instead.#4507 9ea542c34 Thanks @Andarist! - The useMachine(machine) hook now returns { snapshot, send, actorRef } instead of { state, send, service }:
const {
- state,
+ snapshot,
send,
- service
+ actorRef
} = useMachine(machine);
#4265 1153b3f9a Thanks @davidkpiano! - FSM-related functions have been removed.
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.9ea542c34 Thanks @Andarist! - The useActorRef(logic) and useActor(logic) hooks have been added.```ts import { createStore, type EventFromStore } from '@xstate/store';
#5020 e974797b0 Thanks @with-heart! - Added the EventFromStore utility type which extracts the type of events from a store:
import { createStore, type EventFromStore } from '@xstate/store';
const store = createStore(
{ count: 0 },
{
add: (context, event: { addend: number }) => ({
count: context.count + event.addend
}),
multiply: (context, event: { multiplier: number }) => ({
count: context.count * event.multiplier
})
}
);
type StoreEvent = EventFromStore<typeof store>;
// ^? { type: 'add'; addend: number } | { type: 'multiply'; multiplier: number }
EventFromStore allows us to create our own utility types which operate on a store's event types.
For example, we could create a type EventByType which extracts the specific type of store event where Type matches the event's type property:
import { type EventFromStore, type Store } from '@xstate/store';
/**
* Extract the event where `Type` matches the event's `type` from the given
* `Store`.
*/
type EventByType<
TStore extends Store<any, any>,
// creates a type-safe relationship between `Type` and the `type` keys of the
// store's events
Type extends EventFromStore<TStore>['type']
> = Extract<EventFromStore<TStore>, { type: Type }>;
Here's how the type works with the store we defined in the first example:
// we get autocomplete listing the store's event `type` values on the second
// type parameter
type AddEvent = EventByType<typeof store, 'add'>;
// ^? { type: 'add'; addend: number }
type MultiplyEvent = EventByType<typeof store, 'multiply'>;
// ^? { type: 'multiply'; multiplier: number }
// the second type parameter is type-safe, meaning we get a type error if the
// value isn't a valid event `type`
type DivideEvent = EventByType<typeof store, 'divide'>;
// Type '"divide"' does not satisfy the constraint '"add" | "multiply"'.ts(2344)
Building on that, we could create a type EventInputByType to extract a specific event's "input" type (the event type without the type property):
import { type EventFromStore, type Store } from '@xstate/store';
/**
* Extract a specific store event's "input" type (the event type without the
* `type` property).
*/
type EventInputByType<
TStore extends Store<any, any>,
Type extends EventFromStore<TStore>['type']
> = Omit<EventByType<TStore, Type>, 'type'>;
And here's how EventInputByType works with our example store:
type AddInput = EventInputByType<typeof store, 'add'>;
// ^? { addend: number }
type MultiplyInput = EventInputByType<typeof store, 'multiply'>;
// ^? { multiplier: number }
type DivideInput = EventInputByType<typeof store, 'divide'>;
// Type '"divide"' does not satisfy the constraint '"add" | "multiply"'.ts(2344)
Putting it all together, we can use EventInputByType to create a type-safe transition function for each of our store's defined events:
import { createStore, type EventFromStore, type Store } from '@xstate/store';
/**
* Extract the event where `Type` matches the event's `type` from the given
* `Store`.
*/
type EventByType<
TStore extends Store<any, any>,
Type extends EventFromStore<TStore>['type']
> = Extract<EventFromStore<TStore>, { type: Type }>;
/**
* Extract a specific store event's "input" type (the event type without the
* `type` property).
*/
type EventInputByType<
TStore extends Store<any, any>,
Type extends EventFromStore<TStore>['type']
> = Omit<EventByType<TStore, Type>, 'type'>;
const store = createStore(
{ count: 0 },
{
add: (context, event: { addend: number }) => ({
count: context.count + event.addend
}),
multiply: (context, event: { multiplier: number }) => ({
count: context.count * event.multiplier
})
}
);
const add = (input: EventInputByType<typeof store, 'add'>) =>
store.send({ type: 'add', addend: input.addend });
add({ addend: 1 }); // sends { type: 'add', addend: 1 }
const multiply = (input: EventInputByType<typeof store, 'multiply'>) =>
store.send({ type: 'multiply', multiplier: input.multiplier });
multiply({ multiplier: 2 }); // sends { type: 'multiply', multiplier: 2 }
Happy typing!
Nothing published for this version
Updated dependencies \[`25963966c394fc904dc9b701a420b6e204ebe7f7`]:
25963966c394fc904dc9b701a420b6e204ebe7f7]:
Updated dependencies \[`e07a7cd8462473188a0fb646a965e61be1ce6ae3`]:
e07a7cd8462473188a0fb646a965e61be1ce6ae3]:
#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! - 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>
);
}
eeadb7121 Thanks @TkDodo! - - Replace use-sync-external-store/shim with useSyncExternalStore from React.
getSnapshot in useSyncExternalStore.getServerSnapshot in useSyncExternalStore.store to always be defined in useSelector#4896 7c6e2ea Thanks @davidkpiano! - Test model path generation now has the option to allow duplicate paths by setting allowDuplicatePaths: true:
const paths = model.getSimplePaths({
allowDuplicatePaths: true
});
// a
// a -> b
// a -> b -> c
// a -> d
// a -> d -> e
By default, allowDuplicatePaths is set to false:
const paths = model.getSimplePaths();
// a -> b -> c
// a -> d -> e
#4896 7c6e2ea Thanks @davidkpiano! - The adjacencyMapToArray(…) helper function has been introduced, which converts an adjacency map to an array of { state, event, nextState } objects.
import { getAdjacencyMap, adjacencyMapToArray } from '@xstate/graph';
const machine = createMachine({
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
});
const arr = adjacencyMapToArray(getAdjacencyMap(machine));
// [
// {
// "state": {value: "green", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "yellow", ... }
// },
// {
// "state": {value: "yellow", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "red", ... }
// },
// {
// "state": {value: "red", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "green", ... }
// },
// {
// "state": {value: "green", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "yellow", ... }
// },
// ]
#4896 7c6e2ea Thanks @davidkpiano! - The traversalLimit option has been renamed to limit:
model.getShortestPaths({
- traversalLimit: 100
+ limit: 100
});
#4233 3d96d0f95 Thanks @davidkpiano! - Remove getMachineShortestPaths and getMachineSimplePaths
import {
- getMachineShortestPaths,
+ getShortestPaths,
- getMachineSimplePaths,
+ getSimplePaths
} from '@xstate/graph';
-const paths = getMachineShortestPaths(machine);
+const paths = getShortestPaths(machine);
-const paths = getMachineSimplePaths(machine);
+const paths = getSimplePaths(machine);
#4238 b4f12a517 Thanks @davidkpiano! - The steps in the paths returned from functions like getShortestPaths(...) and getSimplePaths(...) have the following changes:
step.event property now represents the event object that resulted in the transition to the step.state, not the event that comes before the next step.path.steps array now includes the target path.state as the last step.
path.steps always has at least one step.step now has the { type: 'xstate.init' } event#4896 7c6e2ea Thanks @davidkpiano! - The createTestMachine(…) function has been removed. Use a normal createMachine(…) or setup(…).createMachine(…) function instead to create machines for path generation.
#4896 7c6e2ea Thanks @davidkpiano! - The filter and stopCondition option for path generation has been renamed to stopWhen, which is used to stop path generation when a condition is met. This is a breaking change, but it is a more accurate name for the option.
const shortestPaths = getShortestPaths(machine, {
events: [{ type: 'INC' }],
- filter: (state) => state.context.count < 5
- stopCondition: (state) => state.context.count < 5
+ stopWhen: (state) => state.context.count === 5
});
#4896 7c6e2ea Thanks @davidkpiano! - Path generation now supports input for actor logic:
const model = createTestModel(
setup({
types: {
input: {} as {
name: string;
},
context: {} as {
name: string;
}
}
}).createMachine({
context: (x) => ({
name: x.input.name
}),
initial: 'checking',
states: {
checking: {
always: [
{ guard: (x) => x.context.name.length > 3, target: 'longName' },
{ target: 'shortName' }
]
},
longName: {},
shortName: {}
}
})
);
const path1 = model.getShortestPaths({
input: { name: 'ed' }
});
expect(path1[0].steps.map((s) => s.state.value)).toEqual(['shortName']);
const path2 = model.getShortestPaths({
input: { name: 'edward' }
});
expect(path2[0].steps.map((s) => s.state.value)).toEqual(['longName']);
#4896 7c6e2ea Thanks @davidkpiano! - The test model "sync" methods have been removed, including:
testModel.testPathSync(…)testModel.testStateSync(…)testPath.testSync(…)The async methods should always be used instead.
model.getShortestPaths().forEach(async (path) => {
- model.testPathSync(path, {
+ await model.testPath(path, {
states: { /* ... */ },
events: { /* ... */ },
});
})
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.#4896 7c6e2ea Thanks @davidkpiano! - The @xstate/graph package now includes everything from @xstate/test.
#4308 af032db12 Thanks @davidkpiano! - Traversing state machines that have delayed transitions will now work as expected:
const machine = createMachine({
initial: 'a',
states: {
a: {
after: {
1000: 'b'
}
},
b: {}
}
});
const paths = getShortestPaths(machine); // works
Nothing published for this version
### Minor Changes - #5452 `9992398` Thanks @davidkpiano! - Ensured that compare argument is a direct comparison function.
9992398 Thanks @davidkpiano! - Ensured that compare argument is a direct comparison function.Updated dependencies \[`d99df1d8f4fe49145c9974465b65028bf19b365f`]:
d99df1d8f4fe49145c9974465b65028bf19b365f]:
Updated dependencies \[`6ba9538`]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-vue.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-svelte.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-solid.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-react.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-preact.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-angular.6ba9538]:
8c4b70652acaef2702f32435362e4755679a516d]:
f73366504 Thanks @davidkpiano! - Release @xstate/store version 1.0Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →