@samitouri / QOS-React-2 / commits / 668fbd651b

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.

Andrew Clark committed May 14, 2019 at 18:08 UTC 668fbd651b6b245da5c42e9e243adc88f0278517
7 files changed +76 -61
packages/react-dom/src/__tests__/ReactDOMHooks-test.js
-36
@@ -72,42 +72,6 @@ describe('ReactDOMHooks', () => {
72 expect(container3.textContent).toBe('6');
73 });
74
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 -
75 it('should not bail out when an update is scheduled from within an event handler', () => {
76 const {createRef, useCallback, useState} = React;
77
packages/react-reconciler/src/ReactFiberClassComponent.js
-4
@@ -52,7 +52,6 @@ import {
52 requestCurrentTime,
53 computeExpirationForFiber,
54 scheduleWork,
55 - flushPassiveEffects,
55 } from './ReactFiberScheduler';
56
57 const fakeInternalInstance = {};
@@ -194,7 +193,6 @@ const classComponentUpdater = {
193 update.callback = callback;
194 }
195
197 - flushPassiveEffects();
196 enqueueUpdate(fiber, update);
197 scheduleWork(fiber, expirationTime);
198 },
@@ -214,7 +212,6 @@ const classComponentUpdater = {
212 update.callback = callback;
213 }
214
217 - flushPassiveEffects();
215 enqueueUpdate(fiber, update);
216 scheduleWork(fiber, expirationTime);
217 },
@@ -233,7 +230,6 @@ const classComponentUpdater = {
230 update.callback = callback;
231 }
232
236 - flushPassiveEffects();
233 enqueueUpdate(fiber, update);
234 scheduleWork(fiber, expirationTime);
235 },
packages/react-reconciler/src/ReactFiberHooks.js
-3
@@ -31,7 +31,6 @@ import {
31 import {
32 scheduleWork,
33 computeExpirationForFiber,
34 - flushPassiveEffects,
34 requestCurrentTime,
35 warnIfNotCurrentlyActingUpdatesInDev,
36 markRenderEventTime,
@@ -1108,8 +1107,6 @@ function dispatchAction<S, A>(
1107 lastRenderPhaseUpdate.next = update;
1108 }
1109 } else {
1111 - flushPassiveEffects();
1112 -
1110 const currentTime = requestCurrentTime();
1111 const expirationTime = computeExpirationForFiber(currentTime, fiber);
1112
packages/react-reconciler/src/ReactFiberReconciler.js
-5
@@ -152,7 +152,6 @@ function scheduleRootUpdate(
152 update.callback = callback;
153 }
154
155 - flushPassiveEffects();
155 enqueueUpdate(current, update);
156 scheduleWork(current, expirationTime);
157
@@ -392,8 +391,6 @@ if (__DEV__) {
391 id--;
392 }
393 if (currentHook !== null) {
395 - flushPassiveEffects();
396 -
394 const newState = copyWithSet(currentHook.memoizedState, path, value);
395 currentHook.memoizedState = newState;
396 currentHook.baseState = newState;
@@ -411,7 +408,6 @@ if (__DEV__) {
408
409 // Support DevTools props for function components, forwardRef, memo, host components, etc.
410 overrideProps = (fiber: Fiber, path: Array<string | number>, value: any) => {
414 - flushPassiveEffects();
411 fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value);
412 if (fiber.alternate) {
413 fiber.alternate.pendingProps = fiber.pendingProps;
@@ -420,7 +416,6 @@ if (__DEV__) {
416 };
417
418 scheduleUpdate = (fiber: Fiber) => {
423 - flushPassiveEffects();
419 scheduleWork(fiber, Sync);
420 };
421
packages/react-reconciler/src/ReactFiberScheduler.js
+5
@@ -560,6 +560,9 @@ export function flushInteractiveUpdates() {
560 return;
561 }
562 flushPendingDiscreteUpdates();
563 + // If the discrete updates scheduled passive effects, flush them now so that
564 + // they fire before the next serial event.
565 + flushPassiveEffects();
566 }
567
568 function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -595,6 +598,8 @@ export function interactiveUpdates<A, B, C, R>(
598 // should explicitly call flushInteractiveUpdates.
599 flushPendingDiscreteUpdates();
600 }
601 + // TODO: Remove this call for the same reason as above.
602 + flushPassiveEffects();
603 return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
604 }
605
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+52
@@ -1719,6 +1719,58 @@ describe('ReactHooks', () => {
1719 ).toThrow('Hello');
1720 });
1721
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 +
1774 // Regression test for https://github.com/facebook/react/issues/14790
1775 it('does not fire a false positive warning when suspending memo', async () => {
1776 const {Suspense, useState} = React;
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+19 -13
@@ -670,8 +670,7 @@ describe('ReactHooksWithNoopRenderer', () => {
670 // Destroying the first child shouldn't prevent the passive effect from
671 // being executed
672 ReactNoop.render([passive]);
673 - expect(Scheduler).toHaveYielded(['Passive effect']);
674 - expect(Scheduler).toFlushAndYield([]);
673 + expect(Scheduler).toFlushAndYield(['Passive effect']);
674 expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
675
676 // (No effects are left to flush.)
@@ -776,11 +775,12 @@ describe('ReactHooksWithNoopRenderer', () => {
775 ReactNoop.render(<Counter count={1} />, () =>
776 Scheduler.yieldValue('Sync effect'),
777 );
779 - expect(Scheduler).toHaveYielded([
778 + expect(Scheduler).toFlushAndYieldThrough([
779 // The previous effect flushes before the reconciliation
780 'Committed state when effect was fired: 0',
781 + 1,
782 + 'Sync effect',
783 ]);
783 - expect(Scheduler).toFlushAndYieldThrough([1, 'Sync effect']);
784 expect(ReactNoop.getChildren()).toEqual([span(1)]);
785
786 ReactNoop.flushPassiveEffects();
@@ -849,8 +849,10 @@ describe('ReactHooksWithNoopRenderer', () => {
849 ReactNoop.render(<Counter count={1} />, () =>
850 Scheduler.yieldValue('Sync effect'),
851 );
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 + ]);
856 expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
857
858 expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
@@ -862,7 +864,7 @@ describe('ReactHooksWithNoopRenderer', () => {
864 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
865 });
866
865 - it('flushes serial effects before enqueueing work', () => {
867 + it('flushes passive effects when flushing discrete updates', () => {
868 let _updateCount;
869 function Counter(props) {
870 const [count, updateCount] = useState(0);
@@ -880,15 +882,17 @@ describe('ReactHooksWithNoopRenderer', () => {
882 expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
883 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
884
883 - // Enqueuing this update forces the passive effect to be flushed --
885 + // A discrete event forces the passive effect to be flushed --
886 // updateCount(1) happens first, so 2 wins.
885 - act(() => _updateCount(2));
887 + ReactNoop.interactiveUpdates(() => {
888 + act(() => _updateCount(2));
889 + });
890 expect(Scheduler).toHaveYielded(['Will set count to 1']);
891 expect(Scheduler).toFlushAndYield(['Count: 2']);
892 expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
893 });
894
891 - it('flushes serial effects before enqueueing work (with tracing)', () => {
895 + it('flushes passive effects when flushing discrete updates (with tracing)', () => {
896 const onInteractionScheduledWorkCompleted = jest.fn();
897 const onWorkCanceled = jest.fn();
898 SchedulerTracing.unstable_subscribe({
@@ -929,9 +933,11 @@ describe('ReactHooksWithNoopRenderer', () => {
933
934 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
935
932 - // Enqueuing this update forces the passive effect to be flushed --
936 + // A discrete event forces the passive effect to be flushed --
937 // updateCount(1) happens first, so 2 wins.
934 - act(() => _updateCount(2));
938 + ReactNoop.interactiveUpdates(() => {
939 + act(() => _updateCount(2));
940 + });
941 expect(Scheduler).toHaveYielded(['Will set count to 1']);
942 expect(Scheduler).toFlushAndYield(['Count: 2']);
943 expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
@@ -1472,8 +1478,8 @@ describe('ReactHooksWithNoopRenderer', () => {
1478 ReactNoop.render(<Counter count={1} />, () =>
1479 Scheduler.yieldValue('Sync effect'),
1480 );
1475 - expect(Scheduler).toHaveYielded(['Mount normal [current: 0]']);
1481 expect(Scheduler).toFlushAndYieldThrough([
1482 + 'Mount normal [current: 0]',
1483 'Unmount layout [current: 0]',
1484 'Mount layout [current: 1]',
1485 'Sync effect',