Minor Changes
-
3e024f9:
createAsyncLogicacceptsschemas.error. It types the actor'serrorsnapshot field andevent.errorin the invoking machine'sonError. Without it, the error staysunknown, so reading properties from it is a type error.const fetchUser = createAsyncLogic({ schemas: { output: z.object({ name: z.string() }), error: z.object({ code: z.string() }), }, run: async () => ({ name: "David" }), }); setup({ actors: { fetchUser } }).createMachine({ invoke: { src: "fetchUser", onError: ({ event }) => { event.error.code; // string }, }, });
Without
schemas.error, narrowevent.errorbefore reading from it.With a
timeout, the error type also includesTimeoutError, so narrow before reading schema fields:onError: ({ event }) => { if (event.error instanceof TimeoutError) return; event.error.code; // string };
-
3e024f9: Children declared in
schemas.childrennow contribute their completion events to the event union seen byentry,exit, guards and transition functions.assertEvent(event, 'xstate.done.actor')narrowsevent.outputto the child's output type, andevent.actorIdto the declared ids.setup({ actors: { fetchUser }, schemas: { children: { fetch: z.custom<ActorRefFromLogic<typeof fetchUser>>() } } }).createMachine({ invoke: { id: 'fetch', src: 'fetchUser' }, entry: ({ event }) => { assertEvent(event, 'xstate.done.actor'); event.output.name; // string } });
Code that assumed every event in these positions is a declared public event may need a narrowing check first.
-
73fa80b: Entry and exit functions now receive
stateNode, the state node being entered or exited.createMachine({ initial: 'a', states: { a: { entry: ({ stateNode }, enq) => { enq(() => console.log('Entered', stateNode.id)); } } } });
-
11c6f52:
createFSMfromxstate/fsmnow follows the(snapshot, event) => [snapshot, effects]protocol used by all actor logic.fsm.transition(...)returns a[nextSnapshot, effects]tuple, whereeffectsis always empty, and snapshots includestatus: 'active'. An FSM can now run increateActorand be passed totransition()andinitialTransition().Before:
let state = fsm.initialState; state = fsm.transition(state, { type: 'toggle' });
After:
let state = fsm.initialState; [state] = fsm.transition(state, { type: 'toggle' }); // Run it as an actor import { createActor } from 'xstate'; const actor = createActor(fsm).start(); actor.send({ type: 'toggle' }); actor.getSnapshot().value; // 'active'
-
97e9166:
getMicrosteps()andgetInitialMicrosteps()now return the transitions taken in each microstep as a third tuple element, including eventless transitions and transitions for raised events.import { getMicrosteps } from 'xstate'; for (const [snapshot, actions, transitions] of getMicrosteps( machine, snapshot, event )) { console.log(transitions.map((t) => `${t.source.id} -> ${t.eventType}`)); }
-
d62cdc7: One event now has a single microstep bound:
options.maxIterations, which defaults to1000. Exceeding it throws the new exportedInfiniteTransitionError, whose message names the actor id, the event and the last five states visited. Previously a hard-coded limit of 1000 applied regardless ofmaxIterations, so raising the limit had no effect.import { createMachine, InfiniteTransitionError } from 'xstate'; const machine = createMachine({ options: { maxIterations: 5000 }, // ... });
-
aa49aee: ### Removed
createFSMand theFSM*types are no longer exported from the rootxstateentry. Import them fromxstate/fsm.- The empty
xstate/actions,xstate/guards,xstate/invoke, andxstate/devfolders are no longer published.
Changed
xstate/graph:getStateNodes(stateNode)is renamed togetDescendantStateNodes(stateNode)so it no longer shares a name with the rootgetStateNodes(stateNode, stateValue).
import { createFSM } from 'xstate/fsm'; import { getDescendantStateNodes } from 'xstate/graph';
-
8576291: Remove the
@xstate.deadletterinspection event; observe undelivered events with theonRejectedEventoption. The inspection protocol is now exactly@xstate.actorand@xstate.transition. In@xstate/effect,deadLetters(actor)now streamsEventRejectionobjects.createActor(machine, { onRejectedEvent: (rejection) => { console.log(rejection.event.type, rejection.reason, rejection.issues); } });
Undelivered events are also available through
system.onRejectedEvent(listener), which accepts any number of listeners added at any time and returns a subscription. TheonRejectedEventoption registers a listener the same way.const subscription = actor.system.onRejectedEvent((rejection) => { console.log(rejection.event.type, rejection.reason); }); subscription.unsubscribe();
-
aa49aee: ### Removed
getInitialSnapshot(logic, input?)andgetNextSnapshot(logic, snapshot, event). UseinitialTransition(...)andtransition(...), which return[snapshot, effects].- The deprecated type aliases
NoInfer(use the built-inNoInfer),AnyInterpreter(useAnyActor), andResolvedStateMachineTypes.
import { initialTransition, transition } from 'xstate'; const [initial] = initialTransition(machine, input); const [next] = transition(machine, initial, { type: 'NEXT' });
-
97e9166: Removed
createTestModelandTestModelfromxstate/graph; use@xstate/test. The types used only by them (TestModelOptions,TestParam,TestPath,TestPathResult,TestStepResult,TestMeta,EventExecutor) andcreateShortestPathsGen/createSimplePathsGenare removed too.xstate/graphkeeps its path traversal functions.// Before const model = createTestModel(machine, { events: [ { type: 'SUBMIT', zip: '12345' }, { type: 'SUBMIT', zip: 'abc' }, { type: 'CANCEL' } ] }); for (const path of model.getShortestPaths()) await path.test(params); // After: `events` is keyed by event type; each payload becomes a named case import * as fc from 'fast-check'; import { testPaths } from '@xstate/test'; await testPaths(machine, { events: { SUBMIT: [ { case: 'valid', generate: fc.constant({ zip: '12345' }) }, { case: 'invalid', generate: fc.constant({ zip: 'abc' }) } ] }, samples: 1, sut });
Event types without a payload, such as
CANCEL, need no entry. -
73fa80b:
createActor(machine)now requiresinputwhen the machine declares an input schema whose type does not acceptundefined. Restoring from a persistedsnapshotdoes not requireinput.const machine = setup({ schemas: { input: z.object({ id: z.string() }) } }).createMachine({}); createActor(machine); // type error createActor(machine, { input: { id: 'a' } }); // ok createActor(machine, { snapshot: persisted }); // ok
-
aa49aee: ### Removed
- The
xstate/scxmlentry point moved to the new@xstate/scxmlpackage.xstateno longer depends onsaxes.
// Before import { createMachineFromSCXML } from 'xstate/scxml'; // After (npm i @xstate/scxml) import { createMachineFromSCXML } from '@xstate/scxml';
- The
-
d62cdc7:
enq.sendTo(...)to a missing target no longer errors the sending actor. Sending to anundefinedref, to a child id with no running child, or toparentfrom a root actor now produces a dead letter with reason'missingTarget': the actor staysactive,onRejectedEventreceives the event (withtargetIdandsourceRef), and development builds log a warning naming the sender and the target. StateonErrorhandlers no longer receivexstate.error.communicationfor these sends.const actor = createActor(machine, { onRejectedEvent: (rejection) => { if (rejection.reason === 'missingTarget') { console.log(rejection.event, rejection.targetId); } } });
-
d62cdc7: Actors are single-use. Calling
start()on an actor afterstop()now throwsActor <id> was stopped and cannot be restarted. Create a new actor with createActor().in all builds, instead of silently doing nothing. Callingstart()on a running actor, or on an actor that already completed or errored, is still a no-op.actor.stop(); actor.start(); // throws const next = createActor(machine).start();
-
3e024f9: When delays are declared (
setup({ delays })orcreateMachine({ delays })), eachafterkey must be a declared delay name, a number of milliseconds or a duration string such as'5s'. The error now names the offending key. Duration strings are no longer rejected when named delays are declared. Duration keys are checked against the forms the runtime parses: integer milliseconds ('250ms'), decimal seconds ('1.5s') and ISO 8601 durations ('PT1M30S'). Malformed keys such as'Pfoo'or'1.5ms'are type errors.setup({ delays: { retryDelay: 1_000 } }).createMachine({ initial: 'waiting', states: { waiting: { after: { // Type error: Delay 'retryDelya' is not declared in delays. retryDelya: { target: 'retrying' } } }, retrying: {} } });
Fix the name, or declare the delay in
delays.At runtime, a delay that is neither a configured delay name nor a valid duration string now errors the actor with
Invalid delay "…"instead of firing immediately. -
3e024f9: When
schemas.eventsis declared, every key in a state'sonmap must match a declared event type. Wildcards ('*','user.*') and reservedxstate.*event types remain allowed. Machines withoutschemas.eventsare unchanged.setup({ schemas: { events: { toggle: z.object({}) } } }).createMachine({ on: { // Type error: Event type 'toggel' is not declared in schemas.events. toggel: { target: '.active' } } });
Fix the typo, or declare the event in
schemas.events. -
d62cdc7: Unhandled events are now observable.
transition(logic, snapshot, event)returns the same snapshot object and no effects when no transition handles the event. A handled event always returns a new snapshot object, including a transition function that returns{}.- New
isUnhandled(previousSnapshot, result)helper. - New
onUnhandledEvent(event, snapshot)option forcreateActor(...). - Development builds warn once per event type per actor. Internal
xstate.*events are not reported.
import { createActor, isUnhandled, transition } from 'xstate'; const result = transition(machine, snapshot, { type: 'unknown' }); isUnhandled(snapshot, result); // true createActor(machine, { onUnhandledEvent: (event, snapshot) => { console.log(`${event.type} not handled in`, snapshot.value); } });
-
ef251fa: Development builds now report leftover v5 configuration (
cond,types, string actions, …) with the v6 replacement instead of ignoring it.// Before: `cond` was silently ignored, so the transition was always taken createMachine({ initial: 'idle', states: { idle: { on: { submit: { target: 'sending', cond: ({ context }) => context.valid } } }, sending: {} } }); // Now throws: Transition "submit" in state "(machine).idle" uses "cond", // which was removed. Use an inline transition function instead: ... // After createMachine({ initial: 'idle', states: { idle: { on: { submit: ({ context }) => { if (!context.valid) return; return { target: 'sending' }; } } }, sending: {} } });
cond, object-formguard, transitionactions, non-functionentry/exit,types,tsTypesandschemathrow.services,activities,predictableActionArguments,preserveActionOrder,strictanddevToolslog a warning. Machines built withcreateMachineFromConfigorcreateMachineFromSCXMLare not checked.
Patch Changes
-
0fe9afe: React Fast Refresh keeps the running actor and its state when you edit a machine. In development builds,
useActorRef(),useActor()anduseMachine()switch the running actor to the edited machine, so the current state and context are kept, including context that holds DOM elements or cyclic objects. If the edited machine cannot represent the current state, the actor restarts from the edited machine. Production builds are unaffected. -
73fa80b: Final states are now inert everywhere, including final regions of a parallel state, matching SCXML: they take no transitions and their invoked actors are not created or started. In development,
createMachinewarns when any final state declaresinvoke,onorafter.Move transitions off a final region onto a non-final state:
region: { initial: 'active', states: { active: { on: { NEXT: { target: 'done' } } }, done: { type: 'final' } } }
-
8576291: Persisting a snapshot whose
contextcontains a circular reference now throws a descriptive error instead of aRangeError(maximum call stack size exceeded). Shared references that are not circular still persist.const node: Record<string, unknown> = {}; node.self = node; const machine = createMachine({ id: 'tree', context: { node } }); createActor(machine).getPersistedSnapshot(); // Error: Cannot persist actor "tree": circular reference at context.node.self
-
8576291: In development builds,
getPersistedSnapshot()warns whencontext,output,erroror state inputs contain a value that does not survive a JSON round-trip: a function, symbol,BigInt,NaNorInfinity,Map,Setor circular reference. The warning names the path of the first such value. -
d62cdc7: Restoring a snapshot whose state has
alwaystransitions or is a choice state now logs a development warning: restored snapshots are not re-evaluated, so those eventless transitions do not run until the next event. -
d62cdc7: A persisted snapshot that fails to restore (for example, an unknown state in
valueor a machine id mismatch) now produces a full machine snapshot withstatus: 'error'and the failure aserror, instead of a bare{ status, output, error }object.snapshot.matches(...),snapshot.can(...)and the other snapshot methods keep working. -
d62cdc7: Restoring a persisted snapshot with
status: 'stopped'now yields a stopped actor. Previously the restored actor kept processing events, running transitions and actions while reportingstatus: 'stopped'. -
d62cdc7: A transition function that returns a promise now throws a descriptive execution error (recoverable with a state
onError) instead of leaving the actor unchanged with an internal error. The returned promise's rejection is observed, so no unhandled rejection is reported. Callingenq.*after the transition function returned now throws in development and does nothing in production.