Major Changes
-
d2c518b: Public names are consistent before the beta. The old names are not kept as aliases.
Before After serializeMachine(machine)machineToJSON(machine)(returnsMachineJSON)createMachineFromConfig(json, sources)machineFromJSON(json, sources)createLogiccreateStepLogicLogicActorLogic,LogicActorRef,LogicSnapshotStepActorLogic,StepActorRef,StepSnapshotLogicConfig,LogicArgs,LogicEnqueue,LogicFunctionStepLogicConfig,StepLogicArgs,StepLogicEnqueue,StepLogicFunctionLogicPatch,LogicEffectStateStepPatch,StepEffectStateLogicEffectExecutableEffectEffectDescriptor,getEffectDescriptorExecutableEffectDescriptor,describeExecutableEffectcheckStateIn(snapshot, value)snapshot.matches(value)state node contextSchemastate node schemas: { context }StrictSetupStateTargetsFlag,StrictSetupStateTargetsMarkerStrictSetupStateTargetsMarker,StrictSetupStateMarkersmachineConfigToJSON(config)is unchanged and now returnsMachineJSON.Removed exports:
listenerLogic,subscriptionLogic(usecreateListenerLogic()/createSubscriptionLogic()),__unsafe_getAllOwnEventDescriptors(usegetAllOwnEventsfromxstate/graph),toObserver,pathToStateValue,SpecialTargets, and thedevToolsactor option type.The
report-removed-apiscodemod reportstoObserver,pathToStateValueandSpecialTargets, and itsstateInhint suggestssnapshot.matches(...).- import { createLogic, serializeMachine } from 'xstate'; + import { createStepLogic, machineToJSON } from 'xstate';
-
d2c518b: Built-in event and effect type strings follow one rule: events delivered to logic use the
xstate.prefix, and effect types use the@xstate.prefix. Inspection records (@xstate.actor,@xstate.transition) are unchanged.Before After Kind @xstate.initxstate.initevent @xstate.stop(sent to a stopping actor)xstate.stopevent xstate.timeoutxstate.timeout.stateevent xstate.system.cancelEffect@xstate.cancelEffecteffect xstate.logic.cleanup@xstate.cleanupeffect xstate.logic.effect@xstate.effecteffect xstate.log(system effect)@xstate.logeffect Built-in guard types in
MachineJSONare source keys, not events, so they use the@xstate.prefix:Before After xstate.not@xstate.notxstate.stateIn@xstate.stateInIn development, action, guard, actor and delay source names that start with
xstate.or@xstate.throw.machineFromJSONalso rejects JSON-definedactions,guardsanddelayswith those names.The stop-child effect stays
@xstate.stop. Helper actors created byenq.listen()andenq.subscribeTo()use the@xstate.listenerand@xstate.subscriptionsource keys, so their ids look like@xstate.listener:0. In development, spawned sources whose id starts withxstate.or@xstate.throw.Suffixed lifecycle event types are no longer matched. Key transitions by the category type and select the payload with
matches:on: { - 'xstate.done.actor.job': { target: 'complete' } + 'xstate.done.actor': { matches: { actorId: 'job' }, target: 'complete' } }This applies to
xstate.done.actor.<id>,xstate.error.actor.<id>,xstate.snapshot.<id>(nowxstate.snapshot.actor),xstate.done.state.<id>,xstate.after.<delay>.<id>,xstate.timeout.<id>andxstate.timeout.actor.<id>, both as transition keys and as sent event types. -
d2c518b: Serialized actor identity uses one shape everywhere:
{ $actor: address }, with an optional stringincarnation.Where Before After actor.toJSON(),SerializedActorRef{ xstate$type: 'actorRef', id, address, src }{ $actor: address }Actor refs in persisted context{ xstate$type: 'actorRef', id, address? }{ $actor: address }Effect descriptor source/actor/targetaddress string (+ incarnationonsendTo){ $actor: address, incarnation? }System snapshot incarnationnumberstringSystemActorState.logiclogicsrcSystem dead-letter effect kind'deadLetter''builtin'(type@xstate.deadLetter)Persisted timer start startedAtscheduledAtPersisted snapshot top-level versionmirror of machine.versionremoved; use machine.versionThe old shapes are not read on restore. Convert or discard snapshots persisted with
xstate$typeactor refs orstartedAttimers before restoring them. -
d2c518b: Pending timers are a list, and no generated timer ids exist.
Where Before After snapshot.timers(in memory and persisted)record keyed by id list in scheduling order after, statetimeout, invoketimeouttimersgenerated id(xstate.after.<delay>.<stateId>,xstate.timeout.state.<stateId>, …)no id; identified by their eventdelayed raise/send without an idgenerated xstate.timer.auto.<n>seq(the actor's sequence number)ActorSystemRuntime.scheduleTimer/cancelTimer(source, id, delay)/(source, id)(source, timer, delay)/(source, timer)with aTimerKeytimer firing input { type: 'xstate.timer', id }{ type: 'xstate.timer', ...timer }@xstate.canceleffect and descriptoridthe cancelled TimerKeyfields (id,seqorevent)system snapshot timersrecord; SystemTimer.idlist; SystemTimer.eventis the firing input; select byoccurrenceA
TimerKeyis{ id },{ seq }or, for a built-in timer,{ event }with the event identity ({ type, stateId, delay }forxstate.after,{ type, stateId }forxstate.timeout.state,{ type, actorId }forxstate.timeout.actor). Durable hosts key timers bysource.addressplus the key.enq.cancel(...)also accepts aTimerKey, which cancels a built-in timer:enq.cancel({ event: { type: 'xstate.after', stateId: 'machine.waiting', delay: 1000 } });
Snapshots persisted with the old
timersrecord are not read on restore.
Minor Changes
-
d2c518b:
snapshot.matches('#id')is true while the state node with thatidis active. This replaces the removedcheckStateIn(snapshot, '#id').const machine = createMachine({ initial: 'editing', states: { editing: { initial: 'draft', states: { draft: { id: 'draft' }, review: {} } }, done: {} } }); const snapshot = createActor(machine).start().getSnapshot(); snapshot.matches('#draft'); // true snapshot.matches('#nope'); // type error: not a state id of this machine
When the machine's state ids are statically known, only those ids are accepted. Unlike state-value checks, an
#idcheck does not narrowcontext. The@xstate.stateInJSON guard uses the same check.
Patch Changes
-
37b3c73: The persisted snapshot JSON schema now accepts unknown fields on the envelope, on
childrenentries and ontimersentries. Readers should ignore fields they don't recognize; future 6.x releases may add optional fields. -
b8edbab:
enq.sendTo(...)accepts a serialized actor ref — the{ $actor: address }value an actor ref becomes afterJSON.stringify, e.g. areplyToin an event payload after persistence or a remote hop. The event is delivered to whichever actor is live at that address when the send executes (for a delayed send, when its timer fires), or reported as a'missingTarget'dead letter (viaonRejectedEvent) when no live actor has it. Previously this threw aTypeErrorand the event was lost. Serialized refs held incontextalso keep their address across persistence and restore. Use the newSerializedActorReftype to type such fields.const server = createMachine({ schemas: { events: { ping: types<{ replyTo: SerializedActorRef; requestId: string }>() } }, on: { ping: ({ event }, enq) => { enq.sendTo(event.replyTo, { type: 'pong', requestId: event.requestId }); } } });
Actor refs stored in context now persist with their address, so a ref to an actor that isn't a child (such as a stored
replyTo) still routes after restore.A pending delayed send to a serialized ref persists with that ref as its timer target and resolves it by address after restore.
A serialized ref that carries an
incarnationreaches only that incarnation: if a different actor is live at the address when the send executes, the event is reported as a'missingTarget'dead letter. -
5d7dfb3:
snapshot.matches(...)accepts a state value whose leaves are not literals, such as{ checkout: string }, as it acceptsstringandStateValue. Values are checked per position: a non-literal leaf is accepted at its own position, and every literal key and child beside it must still name a state, so{ nope: 'payment', checkout: step }is rejected.