Skip to content

Commit e88aa18

Browse files
committed
Fixed an issue with data expressions of root's final nodes being called twice
1 parent ed61b16 commit e88aa18

File tree

3 files changed

+79
-55
lines changed

3 files changed

+79
-55
lines changed

.changeset/modern-pumas-applaud.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'xstate': patch
3+
---
4+
5+
Fixed an issue with data expressions of root's final nodes being called twice.

packages/core/src/StateNode.ts

+52-55
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,9 @@ class StateNode<
373373
this.strict = !!this.config.strict;
374374

375375
// TODO: deprecate (entry)
376-
this.onEntry = toArray(this.config.entry || this.config.onEntry).map(
377-
action => toActionObject(action)
378-
);
376+
this.onEntry = toArray(
377+
this.config.entry || this.config.onEntry
378+
).map(action => toActionObject(action));
379379
// TODO: deprecate (exit)
380380
this.onExit = toArray(this.config.exit || this.config.onExit).map(action =>
381381
toActionObject(action)
@@ -510,14 +510,11 @@ class StateNode<
510510

511511
const transitions = this.transitions;
512512

513-
return (this.__cache.on = transitions.reduce(
514-
(map, transition) => {
515-
map[transition.eventType] = map[transition.eventType] || [];
516-
map[transition.eventType].push(transition as any);
517-
return map;
518-
},
519-
{} as TransitionDefinitionMap<TContext, TEvent>
520-
));
513+
return (this.__cache.on = transitions.reduce((map, transition) => {
514+
map[transition.eventType] = map[transition.eventType] || [];
515+
map[transition.eventType].push(transition as any);
516+
return map;
517+
}, {} as TransitionDefinitionMap<TContext, TEvent>));
521518
}
522519

523520
public get after(): Array<DelayedTransitionDefinition<TContext, TEvent>> {
@@ -642,21 +639,20 @@ class StateNode<
642639
}
643640

644641
const subStateKeys = keys(stateValue);
645-
const subStateNodes: Array<
646-
StateNode<TContext, any, TEvent>
647-
> = subStateKeys.map(subStateKey => this.getStateNode(subStateKey));
642+
const subStateNodes: Array<StateNode<
643+
TContext,
644+
any,
645+
TEvent
646+
>> = subStateKeys.map(subStateKey => this.getStateNode(subStateKey));
648647

649648
return subStateNodes.concat(
650-
subStateKeys.reduce(
651-
(allSubStateNodes, subStateKey) => {
652-
const subStateNode = this.getStateNode(subStateKey).getStateNodes(
653-
stateValue[subStateKey]
654-
);
649+
subStateKeys.reduce((allSubStateNodes, subStateKey) => {
650+
const subStateNode = this.getStateNode(subStateKey).getStateNodes(
651+
stateValue[subStateKey]
652+
);
655653

656-
return allSubStateNodes.concat(subStateNode);
657-
},
658-
[] as Array<StateNode<TContext, any, TEvent>>
659-
)
654+
return allSubStateNodes.concat(subStateNode);
655+
}, [] as Array<StateNode<TContext, any, TEvent>>)
660656
);
661657
}
662658

@@ -995,6 +991,10 @@ class StateNode<
995991

996992
const parent = sn.parent!;
997993

994+
if (!parent.parent) {
995+
return events;
996+
}
997+
998998
events.push(
999999
done(sn.id, sn.data), // TODO: deprecate - final states should not emit done events for their own state.
10001000
done(
@@ -1003,17 +1003,15 @@ class StateNode<
10031003
)
10041004
);
10051005

1006-
if (parent.parent) {
1007-
const grandparent = parent.parent;
1006+
const grandparent = parent.parent!;
10081007

1009-
if (grandparent.type === 'parallel') {
1010-
if (
1011-
getChildren(grandparent).every(parentNode =>
1012-
isInFinalState(transition.configuration, parentNode)
1013-
)
1014-
) {
1015-
events.push(done(grandparent.id, grandparent.data));
1016-
}
1008+
if (grandparent.type === 'parallel') {
1009+
if (
1010+
getChildren(grandparent).every(parentNode =>
1011+
isInFinalState(transition.configuration, parentNode)
1012+
)
1013+
) {
1014+
events.push(done(grandparent.id, grandparent.data));
10171015
}
10181016
}
10191017

@@ -1268,15 +1266,12 @@ class StateNode<
12681266
? currentState.configuration
12691267
: [];
12701268

1271-
const meta = resolvedConfiguration.reduce(
1272-
(acc, stateNode) => {
1273-
if (stateNode.meta !== undefined) {
1274-
acc[stateNode.id] = stateNode.meta;
1275-
}
1276-
return acc;
1277-
},
1278-
{} as Record<string, string>
1279-
);
1269+
const meta = resolvedConfiguration.reduce((acc, stateNode) => {
1270+
if (stateNode.meta !== undefined) {
1271+
acc[stateNode.id] = stateNode.meta;
1272+
}
1273+
return acc;
1274+
}, {} as Record<string, string>);
12801275

12811276
const isDone = isInFinalState(resolvedConfiguration, this);
12821277

@@ -1749,9 +1744,10 @@ class StateNode<
17491744
: parent.initialStateNodes;
17501745
}
17511746

1752-
const subHistoryValue = nestedPath<HistoryValue>(parent.path, 'states')(
1753-
historyValue
1754-
).current;
1747+
const subHistoryValue = nestedPath<HistoryValue>(
1748+
parent.path,
1749+
'states'
1750+
)(historyValue).current;
17551751

17561752
if (isString(subHistoryValue)) {
17571753
return [parent.getStateNode(subHistoryValue)];
@@ -1903,11 +1899,9 @@ class StateNode<
19031899
return transition;
19041900
}
19051901
private formatTransitions(): Array<TransitionDefinition<TContext, TEvent>> {
1906-
let onConfig: Array<
1907-
TransitionConfig<TContext, EventObject> & {
1908-
event: string;
1909-
}
1910-
>;
1902+
let onConfig: Array<TransitionConfig<TContext, EventObject> & {
1903+
event: string;
1904+
}>;
19111905

19121906
if (!this.config.on) {
19131907
onConfig = [];
@@ -1932,11 +1926,14 @@ class StateNode<
19321926
return arrayified;
19331927
})
19341928
.concat(
1935-
toTransitionConfigArray(WILDCARD, wildcardConfigs as SingleOrArray<
1936-
TransitionConfig<TContext, EventObject> & {
1937-
event: '*';
1938-
}
1939-
>)
1929+
toTransitionConfigArray(
1930+
WILDCARD,
1931+
wildcardConfigs as SingleOrArray<
1932+
TransitionConfig<TContext, EventObject> & {
1933+
event: '*';
1934+
}
1935+
>
1936+
)
19401937
)
19411938
);
19421939
}

packages/core/test/final.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,26 @@ describe('final states', () => {
195195

196196
service.send('REQUEST_SECRET');
197197
});
198+
199+
it("should only call data expression once when entering root's final state", () => {
200+
const spy = jest.fn();
201+
const machine = Machine({
202+
initial: 'start',
203+
states: {
204+
start: {
205+
on: {
206+
FINISH: 'end'
207+
}
208+
},
209+
end: {
210+
type: 'final',
211+
data: spy
212+
}
213+
}
214+
});
215+
216+
const service = interpret(machine).start();
217+
service.send({ type: 'FINISH', value: 1 });
218+
expect(spy).toBeCalledTimes(1);
219+
});
198220
});

0 commit comments

Comments
 (0)