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 today
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
Root cause: createInertActorScope used createActor(logic) internally, which eagerly ran getInitialSnapshot during construction and registered any syst
#5575 830db8b Thanks @JSap0914! - Fixed initialTransition (and transition) throwing "Actor with system ID '...' already exists" when the machine contains an invoke with a systemId.
Root cause: createInertActorScope used createActor(logic) internally, which eagerly ran getInitialSnapshot during construction and registered any systemId-carrying child actors in the system. When the caller then ran getInitialSnapshot (or transition) via the returned scope, the same system was reused, causing the duplicate-registration error.
Fix: After creating the internal actor, createInertActorScope now replaces the actor's system reference with a freshly-created system. Child actors spawned by the subsequent caller-driven getInitialSnapshot / transition invocation therefore register into a clean system with no pre-existing entries.
const machine = createMachine({
initial: 'idle',
states: {
idle: {
invoke: {
src: fromPromise(async () => 42),
systemId: 'myActor' // previously caused: "Actor with system ID 'myActor' already exists"
}
}
}
});
// Now works correctly — returns [snapshot, actions] without throwing
const [snapshot, actions] = initialTransition(machine);
#5585 a551a2b Thanks @RubenFricke! - Add missing https:// protocol to the Stately Studio link in the README
One column per quarter.
When a state has both an exact event descriptor (e.g. "foo.bar") and a matching wildcard descriptor (e.g. "foo.*"), transitions from the exact descrip
#5548 8a53531 Thanks @JSap0914! - fix(core): fall back to wildcard event descriptors when an exact descriptor's guard fails
When a state has both an exact event descriptor (e.g. "foo.bar") and a matching wildcard descriptor (e.g. "foo.*"), transitions from the exact descriptor are now tried first; if all their guards fail, matching wildcard descriptor transitions are tried as fallback. Previously, the presence of an exact match would prevent any wildcard fallback from being considered, leaving the machine in its current state when the exact descriptor's guard failed.
### Patch Changes - #5516 `41c0a5a` Thanks @joshuaellis! - fix(core): resolve children snapshot union pollution for typed invoke
41c0a5a Thanks @joshuaellis! - fix(core): resolve children snapshot union pollution for typed invoke### Minor Changes - #5512 `063416d` Thanks @davidkpiano! - Export InspectedTransitionEvent from xstate.
063416d Thanks @davidkpiano! - Export InspectedTransitionEvent from xstate.`ts const machine = setup({ guards: { isReady: ({ context }) => context.ready } }).createMachine({ states: { review: { id: 'review', route: { guard: '
#5525 f79ea13 Thanks @davidkpiano! - Fixed route transition guards so named guards registered with setup({ guards }) are resolved for route.guard.
const machine = setup({
guards: {
isReady: ({ context }) => context.ready
}
}).createMachine({
states: {
review: {
id: 'review',
route: {
guard: 'isReady'
}
}
}
});
```ts import { mapState } from 'xstate';
#5429 9d9c1fe Thanks @davidkpiano! - Add mapState(snapshot, mapper) to map a snapshot to values based on active state(s).
import { mapState } from 'xstate';
const results = mapState(snapshot, {
states: {
loading: { map: () => 'Loading...' },
success: { map: (snap) => snap.context.data },
error: { map: (snap) => snap.context.error.message }
}
});
console.log(results);
// E.g. if snapshot.value === 'loading', then:
// [
// { stateNode: { key: 'loading' }, result: 'Loading...' }
// ]
#5430 e543599 Thanks @davidkpiano! - Add maxIterations option to configure the maximum number of microsteps allowed before throwing an infinite loop error. The default is Infinity (no limit) to avoid breaking existing machines.
You can configure it when creating a machine:
const machine = createMachine({
// ... machine config
options: {
maxIterations: 1000 // set a limit to enable infinite loop detection
}
});
This makes it possible to opt into enabled-only traversal for machine snapshots, such as when you only want to explore events that currently pass guar
#5493 871857d Thanks @davidkpiano! - Add a filterEvents option to xstate/graph traversal helpers and
createTestModel(...) to control which events should be explored from each
state.
This makes it possible to opt into enabled-only traversal for machine snapshots, such as when you only want to explore events that currently pass guards:
import { createTestModel } from 'xstate/graph';
const model = createTestModel(machine);
const paths = model.getSimplePaths({
filterEvents: (state, event) => state.can(event)
});
```ts const actor = createActor(machine); actor.start();
#5299 ca8306f Thanks @Uniqen! - Add actor.select(selector, equalityFn?) method to derive a Readable<TSelected> from an actor's snapshot. The returned object has .subscribe() (only emits when the selected value changes, using Object.is by default) and .get() for synchronous access.
const actor = createActor(machine);
actor.start();
const count = actor.select((snap) => snap.context.count);
count.get(); // current value
count.subscribe((value) => {
console.log(value); // only fires when count changes
});
```ts const machine = setup({}).createMachine({ id: 'app', initial: 'home', states: { home: { id: 'home', route: {} }, dashboard: { initial: 'overview
#4184 a741fe7 Thanks @davidkpiano! - Added routable states. States with route: {} and an explicit id can be navigated to from anywhere via a single { type: 'xstate.route', to: '#id' } event.
const machine = setup({}).createMachine({
id: 'app',
initial: 'home',
states: {
home: { id: 'home', route: {} },
dashboard: {
initial: 'overview',
states: {
overview: { id: 'overview', route: {} },
settings: { id: 'settings', route: {} }
}
}
}
});
const actor = createActor(machine).start();
// Route directly to deeply nested state from anywhere
actor.send({ type: 'xstate.route', to: '#settings' });
Routes support guards for conditional navigation:
settings: {
id: 'settings',
route: {
guard: ({ context }) => context.role === 'admin'
}
}
ad809a0 Thanks @davidkpiano! - Fix: export types so setup() declaration emit works (fixes #5462)```ts import { createMachine, getInitialMicrosteps, getMicrosteps } from 'xstate';
#5457 287b51e Thanks @davidkpiano! - Add getInitialMicrosteps(…) and getMicrosteps(…) functions that return an array of [snapshot, actions] tuples for each microstep in a transition.
import { createMachine, getInitialMicrosteps, getMicrosteps } from 'xstate';
const machine = createMachine({
initial: 'a',
states: {
a: {
entry: () => console.log('enter a'),
on: {
NEXT: 'b'
}
},
b: {
entry: () => console.log('enter b'),
always: 'c'
},
c: {}
}
});
// Get microsteps from initial transition
const initialMicrosteps = getInitialMicrosteps(machine);
// Returns: [
// [snapshotA, [entryActionA]]
// ]
// Get microsteps from a transition
const microsteps = getMicrosteps(machine, initialMicrosteps[0][0], {
type: 'NEXT'
});
// Returns: [
// [snapshotB, [entryActionB]],
// [snapshotC, []]
// ]
// Each microstep is a tuple of [snapshot, actions]
for (const [snapshot, actions] of microsteps) {
console.log('State:', snapshot.value);
console.log('Actions:', actions.length);
}
```ts import { getNextTransitions } from 'xstate';
#5406 703c3a1 Thanks @davidkpiano! - Add getNextTransitions(state) utility to get all transitions available from current state.
import { getNextTransitions } from 'xstate';
// ...
const state = actor.getSnapshot();
const transitions = getNextTransitions(state);
transitions.forEach((t) => {
console.log(`Event: ${t.eventType}, Source: ${t.source.key}`);
});
### Patch Changes - #5440 `e36e299` Thanks @davidkpiano! - Fix systemId cleanup for nested children on stopChild
e36e299 Thanks @davidkpiano! - Fix systemId cleanup for nested children on stopChild``ts // Matches any event with a type that starts with FEEDBACK. assertEvent(event, 'FEEDBACK.*'); ``
#5422 329297b Thanks @davidkpiano! - Add partial descriptor support to assertEvent(…)
// Matches any event with a type that starts with `FEEDBACK.`
assertEvent(event, 'FEEDBACK.*');
2eb8274 Thanks @assertnotnull! - Fix a bug in Cordova when iterating an empty Map```ts import { setup, not, and } from 'xstate';
#5371 b8ec3b1 Thanks @davidkpiano! - Add setup.extend() method to incrementally extend machine setup configurations with additional actions, guards, and delays. This enables composable and reusable machine setups where extended actions, guards, and delays can reference base actions, guards, and delays and support chaining multiple extensions:
import { setup, not, and } from 'xstate';
const baseSetup = setup({
guards: {
isAuthenticated: () => true,
hasPermission: () => false
}
});
const extendedSetup = baseSetup.extend({
guards: {
// Type-safe guard references
isUnauthenticated: not('isAuthenticated'),
canAccess: and(['isAuthenticated', 'hasPermission'])
}
});
// Both base and extended guards are available
extendedSetup.createMachine({
on: {
LOGIN: {
guard: 'isAuthenticated',
target: 'authenticated'
},
LOGOUT: {
guard: 'isUnauthenticated',
target: 'unauthenticated'
}
}
});
```ts const childMachine = createMachine({}); const machine = createMachine({ // ... invoke: [ { src: childMachine, systemId: 'test' } ] }); const sys
#5387 53dd7f1 Thanks @farskid! - Adds system.getAll that returns a record of running actors within the system by their system id
const childMachine = createMachine({});
const machine = createMachine({
// ...
invoke: [
{
src: childMachine,
systemId: 'test'
}
]
});
const system = createActor(machine);
system.getAll(); // { test: ActorRefFrom<typeof childMachine> }
`ts const actor = createActor(machine, { systemId: 'test' }); actor.systemId; // 'test' `
#5379 98f9ddd Thanks @davidkpiano! - Make actor.systemId public:
const actor = createActor(machine, { systemId: 'test' });
actor.systemId; // 'test'
#5380 e7e5e44 Thanks @Nirajkashyap! - fix: remove 'eventType' from required fields in initialTransitionObject
createAction(fn) – create type-safe custom actions
#5367 76c857e Thanks @davidkpiano! - Add type-bound action helpers to setup():
createAction(fn) – create type-safe custom actionssetup().assign(...), setup().sendTo(...), setup().raise(...), setup().log(...), setup().cancel(...), setup().stopChild(...), setup().enqueueActions(...), setup().emit(...), setup().spawnChild(...) – setup-scoped helpers that are fully typed to the setup's context/events/actors/guards/delays/emitted.These helpers return actions that are bound to the specific setup() they were created from and can be used directly in the machine produced by that setup.
const machineSetup = setup({
types: {} as {
context: {
count: number;
};
events: { type: 'inc'; value: number } | { type: 'TEST' };
emitted: { type: 'PING' };
}
});
// Custom action
const action = machineSetup.createAction(({ context, event }) => {
console.log(context.count, event.value);
});
// Type-bound built-ins (no wrapper needed)
const increment = machineSetup.assign({
count: ({ context }) => context.count + 1
});
const raiseTest = machineSetup.raise({ type: 'TEST' });
const ping = machineSetup.emit({ type: 'PING' });
const batch = machineSetup.enqueueActions(({ enqueue, check }) => {
if (check(() => true)) {
enqueue(increment);
}
});
const machine = machineSetup.createMachine({
context: { count: 0 },
entry: [action, increment, raiseTest, ping, batch]
});
```ts const lightMachineSetup = setup({ // ... });
#5364 15e15b5 Thanks @davidkpiano! - Added .createStateConfig(…) to the setup API. This makes it possible to create state configs that are strongly typed and modular.
const lightMachineSetup = setup({
// ...
});
const green = lightMachineSetup.createStateConfig({
//...
});
const yellow = lightMachineSetup.createStateConfig({
//...
});
const red = lightMachineSetup.createStateConfig({
//...
});
const machine = lightMachineSetup.createMachine({
initial: 'green',
states: {
green,
yellow,
red
}
});
`ts actor.on('event', () => { // Will no longer crash the actor throw new Error('oops'); }); `
#5351 71387ff Thanks @davidkpiano! - Fix: Emit callback errors no longer crash the actor
actor.on('event', () => {
// Will no longer crash the actor
throw new Error('oops');
});
### Patch Changes - #5315 `9a0ae82` Thanks @sfc-gh-dperezalvarez! - Exported InspectedActionEvent type
9a0ae82 Thanks @sfc-gh-dperezalvarez! - Exported InspectedActionEvent type```ts import { createMachine } from 'xstate'; import { getShortestPaths } from 'xstate/graph';
#5287 e07a7cd8462473188a0fb646a965e61be1ce6ae3 Thanks @davidkpiano! - The graph and model-based testing utilities from @xstate/graph (and @xstate/test previously) were moved to the core xstate package.
import { createMachine } from 'xstate';
import { getShortestPaths } from 'xstate/graph';
const machine = createMachine({
// ...
});
const paths = getShortestPaths(machine, {
fromState: 'a',
toState: 'b'
});
### Patch Changes - #5289 `479c74b83fa77c57c48f54cf0e9dcfab5fe6cae5` Thanks @ebadyz! - Removed outdated context parameter reference from provide metho
479c74b83fa77c57c48f54cf0e9dcfab5fe6cae5 Thanks @ebadyz! - Removed outdated context parameter reference from provide method documentation.### Patch Changes - #5269 `b453b2d72ba12d0fe46a995f9ccced8000fd0cc9` Thanks @chladog! - Add proper history value persistence and restoration
b453b2d72ba12d0fe46a995f9ccced8000fd0cc9 Thanks @chladog! - Add proper history value persistence and restoration### Patch Changes - #5170 `d99df1d8f4fe49145c9974465b65028bf19b365f` Thanks @Andarist! - Improved compatibility of inferred types in projects with exa
d99df1d8f4fe49145c9974465b65028bf19b365f Thanks @Andarist! - Improved compatibility of inferred types in projects with exactOptionalPropertyTypes enabled```ts const childMachine = createMachine({ types: { input: {} as { value: number } } });
#5139 bf6119a7310a878afbf4f5b01f5e24288f9a0f16 Thanks @SandroMaglione! - Make spawn input required when defined inside referenced actor:
const childMachine = createMachine({
types: { input: {} as { value: number } }
});
const machine = createMachine({
types: {} as { context: { ref: ActorRefFrom<typeof childMachine> } },
context: ({ spawn }) => ({
ref: spawn(
childMachine,
// Input is now required!
{ input: { value: 42 } }
)
})
});
```ts import { transition } from 'xstate';
#4954 8c4b70652acaef2702f32435362e4755679a516d Thanks @davidkpiano! - Added a new transition function that takes an actor logic, a snapshot, and an event, and returns a tuple containing the next snapshot and the actions to execute. This function is a pure function and does not execute the actions itself. It can be used like this:
import { transition } from 'xstate';
const [nextState, actions] = transition(actorLogic, currentState, event);
// Execute actions as needed
Added a new initialTransition function that takes an actor logic and an optional input, and returns a tuple containing the initial snapshot and the actions to execute from the initial transition. This function is also a pure function and does not execute the actions itself. It can be used like this:
import { initialTransition } from 'xstate';
const [initialState, actions] = initialTransition(actorLogic, input);
// Execute actions as needed
These new functions provide a way to separate the calculation of the next snapshot and actions from the execution of those actions, allowing for more control and flexibility in the transition process.
### Patch Changes - #5079 `25963966c394fc904dc9b701a420b6e204ebe7f7` Thanks @davidkpiano! - The inspection event interfaces now expect ActorRefLike in
25963966c394fc904dc9b701a420b6e204ebe7f7 Thanks @davidkpiano! - The inspection event interfaces now expect ActorRefLike instead of AnyActorRef### Patch Changes - #5055 `ad38c35c37` Thanks @SandroMaglione! - Exported RequiredActorOptionsKeys type meant to be used by integration packages like
ad38c35c37 Thanks @SandroMaglione! - Exported RequiredActorOptionsKeys type meant to be used by integration packages like @xstate/react```ts const machine = setup({}).createMachine({ initial: 'green', states: { green: {}, yellow: {}, red: { initial: 'walk', states: { walk: {}, wait: {
#5042 54c9d9e6a4 Thanks @boneskull! - waitFor() now accepts a {signal: AbortSignal} in WaitForOptions
#5006 1ab974547f Thanks @davidkpiano! - The state value typings for setup state machine actors (setup({}).createMachine({ ... })) have been improved to represent the actual expected state values.
const machine = setup({}).createMachine({
initial: 'green',
states: {
green: {},
yellow: {},
red: {
initial: 'walk',
states: {
walk: {},
wait: {},
stop: {}
}
},
emergency: {
type: 'parallel',
states: {
main: {
initial: 'blinking',
states: {
blinking: {}
}
},
cross: {
initial: 'blinking',
states: {
blinking: {}
}
}
}
}
}
});
const actor = createActor(machine).start();
const stateValue = actor.getSnapshot().value;
if (stateValue === 'green') {
// ...
} else if (stateValue === 'yellow') {
// ...
} else if ('red' in stateValue) {
stateValue;
// {
// red: "walk" | "wait" | "stop";
// }
} else {
stateValue;
// {
// emergency: {
// main: "blinking";
// cross: "blinking";
// };
// }
}
#5054 853f6daa0b Thanks @davidkpiano! - The CallbackLogicFunction type (previously InvokeCallback) is now exported. This is the callback function that you pass into fromCallback(callbackLogicFn) to create an actor from a callback function.
import { type CallbackLogicFunction } from 'xstate';
// ...
### Patch Changes - #5039 `d6df8fb470` Thanks @Andarist! - Fixed an inference issue that prevented emit used directly in setup (or bare createMachine)
d6df8fb470 Thanks @Andarist! - Fixed an inference issue that prevented emit used directly in setup (or bare createMachine) to benefit from types.emitted types.### Patch Changes - #5034 `7bed484c38` Thanks @davidkpiano! - Fix EventFrom and ContextFrom types
7bed484c38 Thanks @davidkpiano! - Fix EventFrom and ContextFrom types### Patch Changes - #5029 `88bd87ab41` Thanks @davidkpiano! - Revert ActorRefFrom change - #5011 `a275d274de` Thanks @davidkpiano! - There is a new ty
#5029 88bd87ab41 Thanks @davidkpiano! - Revert ActorRefFrom change
#5011 a275d274de Thanks @davidkpiano! - There is a new type helper: ActorRefFromLogic<TLogic>. This type is a stricter form of ActorRefFrom<TLogic> that only accepts actor logic types. See https://github.com/statelyai/xstate/issues/4997 for more details.
### Patch Changes - #5009 `51d4c4fc5` Thanks @davidkpiano! - The internal types for StateMachine<...> have been improved so that all type params are r
51d4c4fc5 Thanks @davidkpiano! - The internal types for StateMachine<...> have been improved so that all type params are required, to prevent errors when using the types. This fixes weird issues like #5008.```ts const machine = setup({ // ... }).createMachine({ id: 'root', initial: 'parentState', states: { parentState: { meta: {}, initial: 'childState',
#4979 a0e9ebcef Thanks @davidkpiano! - State IDs are now strongly typed as keys of snapshot.getMeta() for state machine actor snapshots.
const machine = setup({
// ...
}).createMachine({
id: 'root',
initial: 'parentState',
states: {
parentState: {
meta: {},
initial: 'childState',
states: {
childState: {
meta: {}
},
stateWithId: {
id: 'state with id',
meta: {}
}
}
}
}
});
const actor = createActor(machine);
const metaValues = actor.getSnapshot().getMeta();
// Auto-completed keys:
metaValues.root;
metaValues['root.parentState'];
metaValues['root.parentState.childState'];
metaValues['state with id'];
// @ts-expect-error
metaValues['root.parentState.stateWithId'];
// @ts-expect-error
metaValues['unknown state'];
9877d548b Thanks @davidkpiano! - Fix an issue where clearTimeout(undefined) was sometimes being called, which can cause errors for some clock implementations. See https://github.com/statelyai/xstate/issues/5001 for details.```js import { createMachine, enqueueActions } from 'xstate';
#4996 5be796cd2 Thanks @ronvoluted! - The actor snapshot status type ('active' | 'done' | 'error' | 'stopped') is now exposed as SnapshotStatus
#4981 c4ae156b2 Thanks @davidkpiano! - Added sendParent to the enqueueActions feature. This allows users to enqueue actions that send events to the parent actor within the enqueueActions block.
import { createMachine, enqueueActions } from 'xstate';
const childMachine = createMachine({
entry: enqueueActions(({ enqueue }) => {
enqueue.sendParent({ type: 'CHILD_READY' });
})
});
Each type represents ActorRef narrowed to the corresponding type of logic (the type of self within the actor's logic):
#4976 452bce71e Thanks @with-heart! - Added exports for actor logic-specific ActorRef types: CallbackActorRef, ObservableActorRef, PromiseActorRef, and TransitionActorRef.
Each type represents ActorRef narrowed to the corresponding type of logic (the type of self within the actor's logic):
CallbackActorRef: actor created by fromCallback
import { fromCallback, createActor } from 'xstate';
/** The events the actor receives. */
type Event = { type: 'someEvent' };
/** The actor's input. */
type Input = { name: string };
/** Actor logic that logs whenever it receives an event of type `someEvent`. */
const logic = fromCallback<Event, Input>(({ self, input, receive }) => {
self;
// ^? CallbackActorRef<Event, Input>
receive((event) => {
if (event.type === 'someEvent') {
console.log(`${input.name}: received "someEvent" event`);
// logs 'myActor: received "someEvent" event'
}
});
});
const actor = createActor(logic, { input: { name: 'myActor' } });
// ^? CallbackActorRef<Event, Input>
ObservableActorRef: actor created by fromObservable and fromEventObservable
import { fromObservable, createActor } from 'xstate';
import { interval } from 'rxjs';
/** The type of the value observed by the actor's logic. */
type Context = number;
/** The actor's input. */
type Input = { period?: number };
/**
* Actor logic that observes a number incremented every `input.period`
* milliseconds (default: 1_000).
*/
const logic = fromObservable<Context, Input>(({ input, self }) => {
self;
// ^? ObservableActorRef<Event, Input>
return interval(input.period ?? 1_000);
});
const actor = createActor(logic, { input: { period: 2_000 } });
// ^? ObservableActorRef<Event, Input>
PromiseActorRef: actor created by fromPromise
import { fromPromise, createActor } from 'xstate';
/** The actor's resolved output. */
type Output = string;
/** The actor's input. */
type Input = { message: string };
/** Actor logic that fetches the url of an image of a cat saying `input.message`. */
const logic = fromPromise<Output, Input>(async ({ input, self }) => {
self;
// ^? PromiseActorRef<Output, Input>
const data = await fetch(`https://cataas.com/cat/says/${input.message}`);
const url = await data.json();
return url;
});
const actor = createActor(logic, { input: { message: 'hello world' } });
// ^? PromiseActorRef<Output, Input>
TransitionActorRef: actor created by fromTransition
import { fromTransition, createActor, type AnyActorSystem } from 'xstate';
/** The actor's stored context. */
type Context = {
/** The current count. */
count: number;
/** The amount to increase `count` by. */
step: number;
};
/** The events the actor receives. */
type Event = { type: 'increment' };
/** The actor's input. */
type Input = { step?: number };
/**
* Actor logic that increments `count` by `step` when it receives an event of
* type `increment`.
*/
const logic = fromTransition<Context, Event, AnyActorSystem, Input>(
(state, event, actorScope) => {
actorScope.self;
// ^? TransitionActorRef<Context, Event>
if (event.type === 'increment') {
return {
...state,
count: state.count + state.step
};
}
return state;
},
({ input, self }) => {
self;
// ^? TransitionActorRef<Context, Event>
return {
count: 0,
step: input.step ?? 1
};
}
);
const actor = createActor(logic, { input: { step: 10 } });
// ^? TransitionActorRef<Context, Event>
#4949 8aa4c2b90 Thanks @davidkpiano! - The TypeGen-related types have been removed from XState, simplifying the internal types without affecting normal XState usage.
```ts const actor = createActor(someMachine);
#4936 c58b36dc3 Thanks @davidkpiano! - Inspecting an actor system via actor.system.inspect(ev => …) now accepts a function or observer, and returns a subscription:
const actor = createActor(someMachine);
const sub = actor.system.inspect((inspectionEvent) => {
console.log(inspectionEvent);
});
// Inspection events will be logged
actor.start();
actor.send({ type: 'anEvent' });
// ...
sub.unsubscribe();
// Will no longer log inspection events
actor.send({ type: 'someEvent' });
#4942 9caaa1f70 Thanks @boneskull! - DoneActorEvent and ErrorActorEvent now contain property actorId, which refers to the ID of the actor the event refers to.
#4935 2ac08b700 Thanks @davidkpiano! - All actor logic creators now support emitting events:
Promise actors
const logic = fromPromise(async ({ emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
});
Transition actors
const logic = fromTransition((state, event, { emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
return state;
}, {});
Observable actors
const logic = fromObservable(({ emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
});
Callback actors
const logic = fromCallback(({ emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
});
417f35a11 Thanks @boneskull! - Expose type UnknownActorRef for use when calling getSnapshot() on an unknown ActorRef.### Patch Changes - #4932 `71a7f8692` Thanks @davidkpiano! - Actors with emitted events should no longer cause type issues:
71a7f8692 Thanks @davidkpiano! - Actors with emitted events should no longer cause type issues: https://github.com/statelyai/xstate/issues/4931`ts actor.on('*', (emitted) => { console.log(emitted); // Any emitted event }); `
#4905 dbeafeb25 Thanks @davidkpiano! - You can now use a wildcard to listen for any emitted event from an actor:
actor.on('*', (emitted) => {
console.log(emitted); // Any emitted event
});
`ts const logic = fromPromise(({ signal }) => fetch('https://api.example.com', { signal }) ); `
#4832 148d8fcef Thanks @cevr! - fromPromise now passes a signal into its creator function.
const logic = fromPromise(({ signal }) =>
fetch('https://api.example.com', { signal })
);
This will be called whenever the state transitions before the promise is resolved. This is useful for cancelling the promise if the state changes.
#4876 3f6a73b56 Thanks @davidkpiano! - XState will now warn when calling built-in actions like assign, sendTo, raise, emit, etc. directly inside of a custom action. See https://stately.ai/docs/actions#built-in-actions for more details.
const machine = createMachine({
entry: () => {
// Will warn:
// "Custom actions should not call \`assign()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details."
assign({
// ...
});
}
});
```ts const machine = setup({ types: { meta: {} as { layout: string; } } }).createMachine({ initial: 'home', states: { home: { meta: { layout: 'full'
#4863 0696adc21 Thanks @davidkpiano! - Meta objects for state nodes and transitions can now be specified in setup({ types: … }):
const machine = setup({
types: {
meta: {} as {
layout: string;
}
}
}).createMachine({
initial: 'home',
states: {
home: {
meta: {
layout: 'full'
}
}
}
});
const actor = createActor(machine).start();
actor.getSnapshot().getMeta().home;
// => { layout: 'full' }
// if in "home" state
`ts const machine = setup({ actors: { existingActor: fromPromise(async () => { // ... }) } }).createMachine({ invoke: { src: fromPromise(async () => {
#4806 f4e0ec48c Thanks @davidkpiano! - Inline actor logic is now permitted when named actors are present. Defining inline actors will no longer cause a TypeScript error:
const machine = setup({
actors: {
existingActor: fromPromise(async () => {
// ...
})
}
}).createMachine({
invoke: {
src: fromPromise(async () => {
// Inline actor
})
// ...
}
});
```ts import { setup, log, createActor } from 'xstate';
#4822 f7f1fbbf3 Thanks @davidkpiano! - The clock and logger specified in the options object of createActor(logic, options) will now propagate to all actors created within the same actor system.
import { setup, log, createActor } from 'xstate';
const childMachine = setup({
// ...
}).createMachine({
// ...
// Uses custom logger from root actor
entry: log('something')
});
const parentMachine = setup({
// ...
}).createMachine({
// ...
invoke: {
src: childMachine
}
});
const actor = createActor(parentMachine, {
logger: (...args) => {
// custom logger for args
}
});
actor.start();
### Patch Changes - #4780 `567267ed2` Thanks @Andarist! - Add missing emit export
```ts import { emit } from 'xstate';
#4746 b570ba20d Thanks @davidkpiano! - The new emit(…) action creator emits events that can be received by listeners. Actors are now event emitters.
import { emit } from 'xstate';
const machine = createMachine({
// ...
on: {
something: {
actions: emit({
type: 'emitted',
some: 'data'
})
}
}
// ...
});
const actor = createActor(machine).start();
actor.on('emitted', (event) => {
console.log(event);
});
actor.send({ type: 'something' });
// logs:
// {
// type: 'emitted',
// some: 'data'
// }
#4777 4abeed9df Thanks @Andarist! - Added support for params to enqueueActions
### Patch Changes - #4772 `9a0120901` Thanks @Andarist! - Fixed a type issue that prevent sendParent to be accepted by setup when delays stayed not co
### Patch Changes - #4768 `4a29f8aab` Thanks @Andarist! - Correctly use falsy outputs (instead of accidentally converting them to undefined).
### Minor Changes - #4750 `a9e3c086f` Thanks @Andarist! - Revamped setup and actions types to disallow usage of non-configured implementations
### Patch Changes - #4739 `15b7dd1f0` Thanks @devanfarrell! - Removed this from machine snapshot methods to fix issues with accessing those methods fr
15b7dd1f0 Thanks @devanfarrell! - Removed this from machine snapshot methods to fix issues with accessing those methods from union of actors and their snapshots.```ts const machine = createMachine({ initial: 'a', states: { a: { on: { event: 'b' } }, b: { entry: 'someAction', always: 'c' }, c: {} } });
#4290 7a8796f80 Thanks @davidkpiano! - An error will now be thrown if an incompatible state value is passed to machine.resolveState({ value }).
#4693 11b6a1ae1 Thanks @davidkpiano! - You can now inspect microsteps (@xstate.microstep) and actions (@xstate.action):
const machine = createMachine({
initial: 'a',
states: {
a: {
on: {
event: 'b'
}
},
b: {
entry: 'someAction',
always: 'c'
},
c: {}
}
});
const actor = createActor(machine, {
inspect: (inspEvent) => {
if (inspEvent.type === '@xstate.microstep') {
console.log(inspEvent.snapshot);
// logs:
// { value: 'a', … }
// { value: 'b', … }
// { value: 'c', … }
console.log(inspEvent.event);
// logs:
// { type: 'event', … }
} else if (inspEvent.type === '@xstate.action') {
console.log(inspEvent.action);
// logs:
// { type: 'someAction', … }
}
}
});
actor.start();
actor.send({ type: 'event' });
```ts import { getInitialSnapshot } from 'xstate'; import { someMachine } from './someMachine';
#4731 960cdcbcb Thanks @davidkpiano! - You can now import getInitialSnapshot(…) from xstate directly, which is useful for getting a mock of the initial snapshot when interacting with machines (or other actor logic) without createActor(…):
import { getInitialSnapshot } from 'xstate';
import { someMachine } from './someMachine';
// Returns the initial snapshot (state) of the machine
const initialSnapshot = getInitialSnapshot(
someMachine,
{ name: 'Mateusz' } // optional input
);
### Patch Changes - #4728 `659efd5c1` Thanks @Andarist! - Fixed compatibility issue of the internal type definitions with TypeScript 5.0 - #4712 `2f1d
#4728 659efd5c1 Thanks @Andarist! - Fixed compatibility issue of the internal type definitions with TypeScript 5.0
#4712 2f1d36a9d Thanks @davidkpiano! - Ensure that InteropObservable and InteropSubscribable are present in the type definition file.
#4694 0b6dff210 Thanks @davidkpiano! - Add UnknownMachineConfig type
### Minor Changes - #4704 `78699aef6` Thanks @Andarist! - createActor will now error if the required input is not given to it. - #4688 `14902e17a` Tha
#4704 78699aef6 Thanks @Andarist! - createActor will now error if the required input is not given to it.
#4688 14902e17a Thanks @Andarist! - The schemas property in setup(...) is now passed through to the resulting machine. This property is meant to be used with future developer tooling, and is typed as unknown for now.
The motivation is that external tools, such as Stately Studio, may allow users to enter any text into the state ID field. This change allows those too
#4685 e43eab144 Thanks @davidkpiano! - State IDs that have periods in them are now supported if those periods are escaped.
The motivation is that external tools, such as Stately Studio, may allow users to enter any text into the state ID field. This change allows those tools to escape periods in state IDs, so that they don't conflict with the internal path-based state IDs.
E.g. if a state ID of "Loading..." is entered into the state ID field, instead of crashing either the external tool and/or the XState state machine, it should be converted by the tool to "Loading\\.\\.\\.", and those periods will be ignored by XState.
### Patch Changes - #4667 `64ee3959c` Thanks @Andarist! - Fixed compatibility with older versions of integration packages.
If the snapshot is undefined, the initial snapshot of the actorLogic is used.
#4596 6113a590a Thanks @davidkpiano! - Introduce getNextSnapshot(...), which determines the next snapshot for the given actorLogic based on the given snapshot and event.
If the snapshot is undefined, the initial snapshot of the actorLogic is used.
import { getNextSnapshot } from 'xstate';
import { trafficLightMachine } from './trafficLightMachine.ts';
const nextSnapshot = getNextSnapshot(
trafficLightMachine, // actor logic
undefined, // snapshot (or initial state if undefined)
{ type: 'TIMER' }
); // event object
console.log(nextSnapshot.value);
// => 'yellow'
const nextSnapshot2 = getNextSnapshot(
trafficLightMachine, // actor logic
nextSnapshot, // snapshot
{ type: 'TIMER' }
); // event object
console.log(nextSnapshot2.value);
// =>'red'
### Patch Changes - #4600 `1f2ccb97c` Thanks @davidkpiano! - Typegen-based types for detecting missing implementations have been removed internally.
1f2ccb97c Thanks @davidkpiano! - Typegen-based types for detecting missing implementations have been removed internally.`ts setup({ /* ... */ }).createMachine({ context: ({ spawn, self }) => { return { childRef: spawn('child', { input: { parent: self } }) }; } }); `
`ts // ... entry: enqueueActions(({ self, system }) => { // ... }); `
#4597 ae0b05f11 Thanks @davidkpiano! - Update the argument object of enqueueActions(...) to include the self and system properties:
// ...
entry: enqueueActions(({ self, system }) => {
// ...
});
The assertEvent(event, 'someType') function will _throw_ if the event is not the expected type. This ensures that the event is guaranteed to have that
#4547 8e8d2ba38 Thanks @davidkpiano! - Add assertEvent(...) to help provide strong typings for events that can't be easily inferred, such as events in entry and exit actions, or in invoke.input.
The assertEvent(event, 'someType') function will throw if the event is not the expected type. This ensures that the event is guaranteed to have that type, and assumes that the event object has the expected payload (naturally enforced by TypeScript).
// ...
entry: ({ event }) => {
assertEvent(event, 'greet');
// event is { type: 'greet'; message: string }
assertEvent(event, ['greet', 'notify']);
// event is { type: 'greet'; message: string }
// or { type: 'notify'; message: string; level: 'info' | 'error' }
},
exit: ({ event }) => {
assertEvent(event, 'doNothing');
// event is { type: 'doNothing' }
}
Your coding agent can read these notes before it upgrades. Set up the MCP server →