Skip to content

Commit

Permalink
Fixed an issue with exit actions being called twice when machine reac…
Browse files Browse the repository at this point in the history
…hed its final state (#4376)
  • Loading branch information
Andarist authored Oct 20, 2023
1 parent e890094 commit 078eaad
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-hornets-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': patch
---

Fixed an issue with exit actions being called twice when machine reached its final state.
43 changes: 28 additions & 15 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ export function macrostep(

// Handle stop event
if (event.type === XSTATE_STOP) {
nextState = stopStep(event, nextState, actorCtx);
nextState = stopStep(nextState, event, actorCtx);
states.push(nextState);

return {
Expand Down Expand Up @@ -1626,8 +1626,7 @@ export function macrostep(
}

if (nextState.status !== 'active') {
// Perform the stop step to ensure that child actors are stopped
stopStep(nextEvent, nextState, actorCtx);
stopChildren(nextState, nextEvent, actorCtx);
}

return {
Expand All @@ -1636,24 +1635,38 @@ export function macrostep(
};
}

function stopStep(
event: AnyEventObject,
function stopChildren(
nextState: AnyState,
event: AnyEventObject,
actorCtx: AnyActorContext
) {
const actions: UnknownAction[] = [];
return resolveActionsAndContext(
nextState,
event,
actorCtx,
Object.values(nextState.children).map((child) => stop(child)),
[]
);
}

for (const stateNode of nextState.configuration.sort(
(a, b) => b.order - a.order
)) {
actions.push(...stateNode.exit);
}
function stopStep(
nextState: AnyState,
event: AnyEventObject,
actorCtx: AnyActorContext
) {
const exitActions = nextState.configuration
.sort((a, b) => b.order - a.order)
.flatMap((stateNode) => stateNode.exit);

for (const child of Object.values(nextState.children)) {
actions.push(stop(child));
}
nextState = resolveActionsAndContext(
nextState,
event,
actorCtx,
exitActions,
[]
);

return resolveActionsAndContext(nextState, event, actorCtx, actions, []);
return stopChildren(nextState, event, actorCtx);
}

function selectTransitions(
Expand Down
8 changes: 3 additions & 5 deletions packages/core/test/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1803,15 +1803,13 @@ describe('entry/exit actions', () => {
});

it('sent events from exit handlers of a done child should be received by its children ', () => {
let eventReceived = false;
const spy = jest.fn();

const grandchild = createMachine({
id: 'grandchild',
on: {
STOPPED: {
actions: () => {
eventReceived = true;
}
actions: spy
}
}
});
Expand Down Expand Up @@ -1852,7 +1850,7 @@ describe('entry/exit actions', () => {
const interpreter = createActor(parent).start();
interpreter.send({ type: 'NEXT' });

expect(eventReceived).toBe(true);
expect(spy).toHaveBeenCalledTimes(1);
});

it('actors spawned in exit handlers of a stopped child should not be started', () => {
Expand Down

0 comments on commit 078eaad

Please sign in to comment.