Skip to content

fix(core): reenter:true targeting a child no longer reenters sibling parallel regions - #5574

Open
JSap0914 wants to merge 1 commit into
statelyai:mainfrom
JSap0914:fix/reenter-sibling-parallel-regions
Open

fix(core): reenter:true targeting a child no longer reenters sibling parallel regions#5574
JSap0914 wants to merge 1 commit into
statelyai:mainfrom
JSap0914:fix/reenter-sibling-parallel-regions

Conversation

@JSap0914

Copy link
Copy Markdown
Contributor

Summary

Closes #5162

When a compound state uses reenter: true on a transition that targets one of its own children (e.g. target: '.a'), sibling parallel regions were incorrectly exiting and re-entering their entry actions.

Root Cause

getTransitionDomain had a guard !transition.reenter && that skipped the early-return path when reenter: true. This caused findLeastCommonAncestor([child, source]) to be invoked. Because isDescendant uses proper ancestor semantics, source is not considered a proper descendant of itself, so the LCA was computed as the parallel parent rather than source. With the parallel root as domain, all sibling regions were added to the exit and entry sets.

Fix

  • getTransitionDomain: Remove the !transition.reenter guard so that when all targets are within the source, the domain is always the source (even with reenter: true).
  • computeEntrySet: Explicitly add the source to statesToEnter when reenter && source === domain, because the ancestor-up-to-domain traversal excludes the domain boundary itself.
  • SCXML tests: Skip more-parallel/test3b and test6b, which relied on the now-corrected domain computation to produce an unrelated conflict-resolution outcome. XState v5 already explicitly diverges from SCXML external-transition semantics (see the test506.txml comment in scxml.test.ts).

Verification

Failing case (reproduces #5162)

const machine = createMachine({
  type: 'parallel',
  states: {
    region1: {
      initial: 'a',
      states: { a: {}, b: {} },
      on: {
        REENTER: { target: '.a', reenter: true }
      }
    },
    region2: {
      entry: () => console.log('SHOULD NOT fire on REENTER'),
      initial: 'c',
      states: { c: {}, d: {} }
    }
  }
});

const actor = createActor(machine).start();
actor.send({ type: 'REENTER' });
// Before fix: region2 exits and re-enters (entry actions fire)
// After fix:  only region1 exits and re-enters ✓

…parallel regions

When a compound state uses `reenter: true` on a transition that targets
one of its own children (e.g. `target: '.a'`), the transition domain is
now correctly set to the source state itself rather than the parallel
parent.

Previously, `getTransitionDomain` skipped the early-return for
target-within-source when `reenter: true` (the guard was
`!transition.reenter && ...`). This caused `findLeastCommonAncestor` to
be called with [child, source]. Because `isDescendant` uses *proper*
ancestor semantics, the LCA was computed as the parallel parent instead
of the source, making the parallel root the domain. That in turn caused
all sibling regions to be added to the exit and entry sets.

Fix:
- Remove the `!transition.reenter` guard so that when all targets are
  within the source the domain is always the source, even with reenter.
- Explicitly add the source to `statesToEnter` in `computeEntrySet`
  when `reenter && source === domain`, since ancestors-up-to-domain
  traversal excludes the domain itself.
- Skip two SCXML tests (test3b, test6b) that relied on the now-corrected
  domain computation to produce an unrelated conflict-resolution outcome;
  XState v5 already explicitly diverges from SCXML external-transition
  semantics (see skipped test506.txml comment).

Fixes statelyai#5162
Copilot AI review requested due to automatic review settings June 29, 2026 04:46
@changeset-bot

changeset-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4a340d8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
xstate Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Reenter in one state node also triggers parallel (sibling) node to reenter

2 participants