diff --git a/.changeset/sendparent-noop-when-no-parent.md b/.changeset/sendparent-noop-when-no-parent.md new file mode 100644 index 0000000000..0712887b01 --- /dev/null +++ b/.changeset/sendparent-noop-when-no-parent.md @@ -0,0 +1,19 @@ +--- +"xstate": patch +--- + +`sendParent(...)` is now a no-op when the actor has no parent, instead of throwing an error. The docs already stated it sends "if it exists"; this change makes the runtime match that promise. + +```ts +const machine = createMachine({ + initial: 'init', + states: { + init: { + // Previously threw "Unable to send event to actor '#_parent' from machine '(machine)'." + // Now silently does nothing when there is no parent. + entry: sendParent({ type: 'CHILD_INIT' }) + } + } +}); +createActor(machine).start(); // no longer errors +``` diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 35b2aa4f02..d5818a42a5 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -112,6 +112,12 @@ function resolveSendTo( : snapshot.children[resolvedTarget]; } if (!targetActorRef) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (resolvedTarget === SpecialTargets.Parent) { + // sendParent is documented as sending to the parent "if it exists". + // When there is no parent actor, silently skip rather than error. + return [snapshot, undefined, undefined]; + } throw new Error( `Unable to send event to actor '${resolvedTarget}' from machine '${snapshot.machine.id}'.` ); @@ -136,13 +142,18 @@ function resolveSendTo( function retryResolveSendTo( _: AnyActorScope, snapshot: AnyMachineSnapshot, - params: { - to: AnyActorRef; - event: EventObject; - id: string | undefined; - delay: number | undefined; - } + params: + | { + to: AnyActorRef; + event: EventObject; + id: string | undefined; + delay: number | undefined; + } + | undefined ) { + if (!params) { + return; + } if (typeof params.to === 'string') { params.to = snapshot.children[params.to]; } @@ -150,13 +161,18 @@ function retryResolveSendTo( function executeSendTo( actorScope: AnyActorScope, - params: { - to: AnyActorRef; - event: EventObject; - id: string | undefined; - delay: number | undefined; - } + params: + | { + to: AnyActorRef; + event: EventObject; + id: string | undefined; + delay: number | undefined; + } + | undefined ) { + if (!params) { + return; + } // this forms an outgoing events queue // thanks to that the recipient actors are able to read the *updated* snapshot value of the sender actorScope.defer(() => { diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index edc268742d..c662bad747 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -4357,3 +4357,21 @@ describe('actions', () => { expect(actions).toEqual({}); }); }); + +describe('sendParent', () => { + it('should be a no-op when there is no parent actor', () => { + const machine = createMachine({ + initial: 'init', + states: { + init: { + entry: sendParent({ type: 'CHILD_INIT' }) + } + } + }); + + // sendParent should be a no-op (not error) when the actor has no parent + const actor = createActor(machine).start(); + // The actor should remain active (not in error status) + expect(actor.getSnapshot().status).toBe('active'); + }); +});