Skip to content

Commit 668fbd6

Browse files
authored
Fix serial passive effects (#15650)
* Failing test for false positive warning * Flush passive effects before discrete events Currently, we check for pending passive effects inside the `setState` method before we add additional updates to the queue, in case those pending effects also add things to the queue. However, the `setState` method is too late, because the event that caused the update might not have ever fired had the passive effects flushed before we got there. This is the same as the discrete/serial events problem. When a serial update comes in, and there's already a pending serial update, we have to do it before we call the user-provided event handlers. Because the event handlers themselves might change as a result of the pending update. This commit moves the `flushPassiveEffects` call to before the discrete event handlers are called, and removes it from the `setState` method. Non-discrete events will not cause passive effects to flush, which is fine, since by definition they are not order dependent.
1 parent b0657fd commit 668fbd6

File tree

7 files changed

+76
-61
lines changed

7 files changed

+76
-61
lines changed

packages/react-dom/src/__tests__/ReactDOMHooks-test.js

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -72,42 +72,6 @@ describe('ReactDOMHooks', () => {
7272
expect(container3.textContent).toBe('6');
7373
});
7474

75-
it('can batch synchronous work inside effects with other work', () => {
76-
let otherContainer = document.createElement('div');
77-
78-
let calledA = false;
79-
function A() {
80-
calledA = true;
81-
return 'A';
82-
}
83-
84-
let calledB = false;
85-
function B() {
86-
calledB = true;
87-
return 'B';
88-
}
89-
90-
let _set;
91-
function Foo() {
92-
_set = React.useState(0)[1];
93-
React.useEffect(() => {
94-
ReactDOM.render(<A />, otherContainer);
95-
});
96-
return null;
97-
}
98-
99-
ReactDOM.render(<Foo />, container);
100-
ReactDOM.unstable_batchedUpdates(() => {
101-
_set(0); // Forces the effect to be flushed
102-
expect(otherContainer.textContent).toBe('A');
103-
ReactDOM.render(<B />, otherContainer);
104-
expect(otherContainer.textContent).toBe('A');
105-
});
106-
expect(otherContainer.textContent).toBe('B');
107-
expect(calledA).toBe(true);
108-
expect(calledB).toBe(true);
109-
});
110-
11175
it('should not bail out when an update is scheduled from within an event handler', () => {
11276
const {createRef, useCallback, useState} = React;
11377

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import {
5252
requestCurrentTime,
5353
computeExpirationForFiber,
5454
scheduleWork,
55-
flushPassiveEffects,
5655
} from './ReactFiberScheduler';
5756

5857
const fakeInternalInstance = {};
@@ -194,7 +193,6 @@ const classComponentUpdater = {
194193
update.callback = callback;
195194
}
196195

197-
flushPassiveEffects();
198196
enqueueUpdate(fiber, update);
199197
scheduleWork(fiber, expirationTime);
200198
},
@@ -214,7 +212,6 @@ const classComponentUpdater = {
214212
update.callback = callback;
215213
}
216214

217-
flushPassiveEffects();
218215
enqueueUpdate(fiber, update);
219216
scheduleWork(fiber, expirationTime);
220217
},
@@ -233,7 +230,6 @@ const classComponentUpdater = {
233230
update.callback = callback;
234231
}
235232

236-
flushPassiveEffects();
237233
enqueueUpdate(fiber, update);
238234
scheduleWork(fiber, expirationTime);
239235
},

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
import {
3232
scheduleWork,
3333
computeExpirationForFiber,
34-
flushPassiveEffects,
3534
requestCurrentTime,
3635
warnIfNotCurrentlyActingUpdatesInDev,
3736
markRenderEventTime,
@@ -1108,8 +1107,6 @@ function dispatchAction<S, A>(
11081107
lastRenderPhaseUpdate.next = update;
11091108
}
11101109
} else {
1111-
flushPassiveEffects();
1112-
11131110
const currentTime = requestCurrentTime();
11141111
const expirationTime = computeExpirationForFiber(currentTime, fiber);
11151112

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ function scheduleRootUpdate(
152152
update.callback = callback;
153153
}
154154

155-
flushPassiveEffects();
156155
enqueueUpdate(current, update);
157156
scheduleWork(current, expirationTime);
158157

@@ -392,8 +391,6 @@ if (__DEV__) {
392391
id--;
393392
}
394393
if (currentHook !== null) {
395-
flushPassiveEffects();
396-
397394
const newState = copyWithSet(currentHook.memoizedState, path, value);
398395
currentHook.memoizedState = newState;
399396
currentHook.baseState = newState;
@@ -411,7 +408,6 @@ if (__DEV__) {
411408

412409
// Support DevTools props for function components, forwardRef, memo, host components, etc.
413410
overrideProps = (fiber: Fiber, path: Array<string | number>, value: any) => {
414-
flushPassiveEffects();
415411
fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value);
416412
if (fiber.alternate) {
417413
fiber.alternate.pendingProps = fiber.pendingProps;
@@ -420,7 +416,6 @@ if (__DEV__) {
420416
};
421417

422418
scheduleUpdate = (fiber: Fiber) => {
423-
flushPassiveEffects();
424419
scheduleWork(fiber, Sync);
425420
};
426421

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ export function flushInteractiveUpdates() {
560560
return;
561561
}
562562
flushPendingDiscreteUpdates();
563+
// If the discrete updates scheduled passive effects, flush them now so that
564+
// they fire before the next serial event.
565+
flushPassiveEffects();
563566
}
564567

565568
function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -595,6 +598,8 @@ export function interactiveUpdates<A, B, C, R>(
595598
// should explicitly call flushInteractiveUpdates.
596599
flushPendingDiscreteUpdates();
597600
}
601+
// TODO: Remove this call for the same reason as above.
602+
flushPassiveEffects();
598603
return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
599604
}
600605

packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,58 @@ describe('ReactHooks', () => {
17191719
).toThrow('Hello');
17201720
});
17211721

1722+
// Regression test for https://github.com/facebook/react/issues/15057
1723+
it('does not fire a false positive warning when previous effect unmounts the component', () => {
1724+
let {useState, useEffect} = React;
1725+
let globalListener;
1726+
1727+
function A() {
1728+
const [show, setShow] = useState(true);
1729+
function hideMe() {
1730+
setShow(false);
1731+
}
1732+
return show ? <B hideMe={hideMe} /> : null;
1733+
}
1734+
1735+
function B(props) {
1736+
return <C {...props} />;
1737+
}
1738+
1739+
function C({hideMe}) {
1740+
const [, setState] = useState();
1741+
1742+
useEffect(() => {
1743+
let isStale = false;
1744+
1745+
globalListener = () => {
1746+
if (!isStale) {
1747+
setState('hello');
1748+
}
1749+
};
1750+
1751+
return () => {
1752+
isStale = true;
1753+
hideMe();
1754+
};
1755+
});
1756+
return null;
1757+
}
1758+
1759+
ReactTestRenderer.act(() => {
1760+
ReactTestRenderer.create(<A />);
1761+
});
1762+
1763+
expect(() => {
1764+
globalListener();
1765+
globalListener();
1766+
}).toWarnDev([
1767+
'An update to C inside a test was not wrapped in act',
1768+
'An update to C inside a test was not wrapped in act',
1769+
// Note: should *not* warn about updates on unmounted component.
1770+
// Because there's no way for component to know it got unmounted.
1771+
]);
1772+
});
1773+
17221774
// Regression test for https://github.com/facebook/react/issues/14790
17231775
it('does not fire a false positive warning when suspending memo', async () => {
17241776
const {Suspense, useState} = React;

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,7 @@ describe('ReactHooksWithNoopRenderer', () => {
670670
// Destroying the first child shouldn't prevent the passive effect from
671671
// being executed
672672
ReactNoop.render([passive]);
673-
expect(Scheduler).toHaveYielded(['Passive effect']);
674-
expect(Scheduler).toFlushAndYield([]);
673+
expect(Scheduler).toFlushAndYield(['Passive effect']);
675674
expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
676675

677676
// (No effects are left to flush.)
@@ -776,11 +775,12 @@ describe('ReactHooksWithNoopRenderer', () => {
776775
ReactNoop.render(<Counter count={1} />, () =>
777776
Scheduler.yieldValue('Sync effect'),
778777
);
779-
expect(Scheduler).toHaveYielded([
778+
expect(Scheduler).toFlushAndYieldThrough([
780779
// The previous effect flushes before the reconciliation
781780
'Committed state when effect was fired: 0',
781+
1,
782+
'Sync effect',
782783
]);
783-
expect(Scheduler).toFlushAndYieldThrough([1, 'Sync effect']);
784784
expect(ReactNoop.getChildren()).toEqual([span(1)]);
785785

786786
ReactNoop.flushPassiveEffects();
@@ -849,8 +849,10 @@ describe('ReactHooksWithNoopRenderer', () => {
849849
ReactNoop.render(<Counter count={1} />, () =>
850850
Scheduler.yieldValue('Sync effect'),
851851
);
852-
expect(Scheduler).toHaveYielded(['Schedule update [0]']);
853-
expect(Scheduler).toFlushAndYieldThrough(['Count: 0']);
852+
expect(Scheduler).toFlushAndYieldThrough([
853+
'Schedule update [0]',
854+
'Count: 0',
855+
]);
854856
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
855857

856858
expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
@@ -862,7 +864,7 @@ describe('ReactHooksWithNoopRenderer', () => {
862864
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
863865
});
864866

865-
it('flushes serial effects before enqueueing work', () => {
867+
it('flushes passive effects when flushing discrete updates', () => {
866868
let _updateCount;
867869
function Counter(props) {
868870
const [count, updateCount] = useState(0);
@@ -880,15 +882,17 @@ describe('ReactHooksWithNoopRenderer', () => {
880882
expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
881883
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
882884

883-
// Enqueuing this update forces the passive effect to be flushed --
885+
// A discrete event forces the passive effect to be flushed --
884886
// updateCount(1) happens first, so 2 wins.
885-
act(() => _updateCount(2));
887+
ReactNoop.interactiveUpdates(() => {
888+
act(() => _updateCount(2));
889+
});
886890
expect(Scheduler).toHaveYielded(['Will set count to 1']);
887891
expect(Scheduler).toFlushAndYield(['Count: 2']);
888892
expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
889893
});
890894

891-
it('flushes serial effects before enqueueing work (with tracing)', () => {
895+
it('flushes passive effects when flushing discrete updates (with tracing)', () => {
892896
const onInteractionScheduledWorkCompleted = jest.fn();
893897
const onWorkCanceled = jest.fn();
894898
SchedulerTracing.unstable_subscribe({
@@ -929,9 +933,11 @@ describe('ReactHooksWithNoopRenderer', () => {
929933

930934
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
931935

932-
// Enqueuing this update forces the passive effect to be flushed --
936+
// A discrete event forces the passive effect to be flushed --
933937
// updateCount(1) happens first, so 2 wins.
934-
act(() => _updateCount(2));
938+
ReactNoop.interactiveUpdates(() => {
939+
act(() => _updateCount(2));
940+
});
935941
expect(Scheduler).toHaveYielded(['Will set count to 1']);
936942
expect(Scheduler).toFlushAndYield(['Count: 2']);
937943
expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
@@ -1472,8 +1478,8 @@ describe('ReactHooksWithNoopRenderer', () => {
14721478
ReactNoop.render(<Counter count={1} />, () =>
14731479
Scheduler.yieldValue('Sync effect'),
14741480
);
1475-
expect(Scheduler).toHaveYielded(['Mount normal [current: 0]']);
14761481
expect(Scheduler).toFlushAndYieldThrough([
1482+
'Mount normal [current: 0]',
14771483
'Unmount layout [current: 0]',
14781484
'Mount layout [current: 1]',
14791485
'Sync effect',

0 commit comments

Comments
 (0)