From 88d540eb8e0b659c9621cc5c365bd626a000c1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 25 Jun 2022 10:00:44 +0200 Subject: [PATCH] Fixed an issue with targeted ancestor reentrancy (#3424) --- .changeset/unlucky-sloths-tell.md | 5 +++++ packages/core/src/StateNode.ts | 21 +++++++++++---------- packages/core/test/actions.test.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 .changeset/unlucky-sloths-tell.md diff --git a/.changeset/unlucky-sloths-tell.md b/.changeset/unlucky-sloths-tell.md new file mode 100644 index 0000000000..c2f71d04ee --- /dev/null +++ b/.changeset/unlucky-sloths-tell.md @@ -0,0 +1,5 @@ +--- +'xstate': patch +--- + +Fixed an issue with targeted ancestors not being correctly reentered during external transitions. diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 6cd9237b56..3377822d0c 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -956,8 +956,8 @@ class StateNode< const reentryNodes: StateNode[] = []; if (!isInternal) { - allNextStateNodes.forEach((n1) => { - reentryNodes.push(...this.getExternalReentryNodes(n1)); + nextStateNodes.forEach((targetNode) => { + reentryNodes.push(...this.getExternalReentryNodes(targetNode)); }); } @@ -972,23 +972,24 @@ class StateNode< } private getExternalReentryNodes( - childStateNode: StateNode + targetNode: StateNode ): Array> { const nodes: Array> = []; - let marker: - | StateNode - | undefined = childStateNode; + let [marker, possibleAncestor]: [ + StateNode | undefined, + StateNode + ] = targetNode.order > this.order ? [targetNode, this] : [this, targetNode]; - while (marker && marker !== this) { + while (marker && marker !== possibleAncestor) { nodes.push(marker); marker = marker.parent; } - if (marker !== this) { - // we never got to `this`, therefore the `childStateNode` "escapes" it + if (marker !== possibleAncestor) { + // we never got to `possibleAncestor`, therefore the initial `marker` "escapes" it // it's in a different part of the tree so no states will be reentered for such an external transition return []; } - nodes.push(this); + nodes.push(possibleAncestor); return nodes; } diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index ca28ba1ac3..c7528c6a5a 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -383,6 +383,34 @@ describe('entry/exit actions', () => { expect(stateA.actions.map((action) => action.type)).toEqual(['D2 Exit']); }); + it("should reenter targeted ancestor (as it's a descendant of the transition domain)", () => { + const actual: string[] = []; + const machine = createMachine({ + initial: 'loaded', + states: { + loaded: { + id: 'loaded', + entry: () => actual.push('loaded entry'), + initial: 'idle', + states: { + idle: { + on: { + UPDATE: '#loaded' + } + } + } + } + } + }); + + const service = interpret(machine).start(); + + actual.length = 0; + service.send('UPDATE'); + + expect(actual).toEqual(['loaded entry']); + }); + describe('should ignore same-parent state actions (sparse)', () => { const fooBar = { initial: 'foo',