@samitouri / QOS-React-2 / commits / ef4ac42f88

[Flare] Update interactiveUpdates flushing heuristics (#15687)

Dominic Gannaway committed May 21, 2019 at 22:03 UTC ef4ac42f8893afd0240d2679db7438f1b599bbd4
14 files changed +343 -89
packages/events/ReactGenericBatching.js
+49 -27
@@ -17,15 +17,33 @@ import {
17 // scheduled work and instead do synchronous work.
18
19 // Defaults
20 -let _batchedUpdatesImpl = function(fn, bookkeeping) {
20 +let batchedUpdatesImpl = function(fn, bookkeeping) {
21 return fn(bookkeeping);
22 };
23 -let _interactiveUpdatesImpl = function(fn, a, b, c) {
23 +let discreteUpdatesImpl = function(fn, a, b, c) {
24 return fn(a, b, c);
25 };
26 -let _flushInteractiveUpdatesImpl = function() {};
26 +let flushDiscreteUpdatesImpl = function() {};
27 +let batchedEventUpdatesImpl = batchedUpdatesImpl;
28
29 let isBatching = false;
30 +
31 +function batchedUpdatesFinally() {
32 + // Here we wait until all updates have propagated, which is important
33 + // when using controlled components within layers:
34 + // https://github.com/facebook/react/issues/1698
35 + // Then we restore state of any controlled component.
36 + isBatching = false;
37 + const controlledComponentsHavePendingUpdates = needsStateRestore();
38 + if (controlledComponentsHavePendingUpdates) {
39 + // If a controlled event was fired, we may need to restore the state of
40 + // the DOM node back to the controlled value. This is necessary when React
41 + // bails out of the update without touching the DOM.
42 + flushDiscreteUpdatesImpl();
43 + restoreStateIfNeeded();
44 + }
45 +}
46 +
47 export function batchedUpdates(fn, bookkeeping) {
48 if (isBatching) {
49 // If we are currently inside another batch, we need to wait until it
@@ -34,38 +52,42 @@ export function batchedUpdates(fn, bookkeeping) {
52 }
53 isBatching = true;
54 try {
37 - return _batchedUpdatesImpl(fn, bookkeeping);
55 + return batchedUpdatesImpl(fn, bookkeeping);
56 + } finally {
57 + batchedUpdatesFinally();
58 + }
59 +}
60 +
61 +export function batchedEventUpdates(fn, bookkeeping) {
62 + if (isBatching) {
63 + // If we are currently inside another batch, we need to wait until it
64 + // fully completes before restoring state.
65 + return fn(bookkeeping);
66 + }
67 + isBatching = true;
68 + try {
69 + return batchedEventUpdatesImpl(fn, bookkeeping);
70 } finally {
39 - // Here we wait until all updates have propagated, which is important
40 - // when using controlled components within layers:
41 - // https://github.com/facebook/react/issues/1698
42 - // Then we restore state of any controlled component.
43 - isBatching = false;
44 - const controlledComponentsHavePendingUpdates = needsStateRestore();
45 - if (controlledComponentsHavePendingUpdates) {
46 - // If a controlled event was fired, we may need to restore the state of
47 - // the DOM node back to the controlled value. This is necessary when React
48 - // bails out of the update without touching the DOM.
49 - _flushInteractiveUpdatesImpl();
50 - restoreStateIfNeeded();
51 - }
71 + batchedUpdatesFinally();
72 }
73 }
74
55 -export function interactiveUpdates(fn, a, b, c) {
56 - return _interactiveUpdatesImpl(fn, a, b, c);
75 +export function discreteUpdates(fn, a, b, c) {
76 + return discreteUpdatesImpl(fn, a, b, c);
77 }
78
59 -export function flushInteractiveUpdates() {
60 - return _flushInteractiveUpdatesImpl();
79 +export function flushDiscreteUpdates() {
80 + return flushDiscreteUpdatesImpl();
81 }
82
83 export function setBatchingImplementation(
64 - batchedUpdatesImpl,
65 - interactiveUpdatesImpl,
66 - flushInteractiveUpdatesImpl,
84 + _batchedUpdatesImpl,
85 + _discreteUpdatesImpl,
86 + _flushDiscreteUpdatesImpl,
87 + _batchedEventUpdatesImpl,
88 ) {
68 - _batchedUpdatesImpl = batchedUpdatesImpl;
69 - _interactiveUpdatesImpl = interactiveUpdatesImpl;
70 - _flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
89 + batchedUpdatesImpl = _batchedUpdatesImpl;
90 + discreteUpdatesImpl = _discreteUpdatesImpl;
91 + flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
92 + batchedEventUpdatesImpl = _batchedEventUpdatesImpl;
93 }
packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+11 -1
@@ -1026,9 +1026,15 @@ describe('ReactDOMFiber', () => {
1026
1027 it('should not update event handlers until commit', () => {
1028 let ops = [];
1029 + let eventErrors = [];
1030 const handlerA = () => ops.push('A');
1031 const handlerB = () => ops.push('B');
1032
1033 + spyOnProd(console, 'error');
1034 + window.addEventListener('error', e => {
1035 + eventErrors.push(e.message);
1036 + });
1037 +
1038 class Example extends React.Component {
1039 state = {flip: false, count: 0};
1040 flip() {
@@ -1090,12 +1096,16 @@ describe('ReactDOMFiber', () => {
1096
1097 // Because the new click handler has not yet committed, we should still
1098 // invoke B.
1093 - expect(ops).toEqual(['B']);
1099 + expect(ops).toEqual([]);
1100 ops = [];
1101
1102 // Any click that happens after commit, should invoke A.
1103 node.click();
1104 expect(ops).toEqual(['A']);
1105 + expect(eventErrors[0]).toEqual(
1106 + 'unstable_flushDiscreteUpdates: Cannot flush ' +
1107 + 'updates when React is already rendering.',
1108 + );
1109 });
1110
1111 it('should not crash encountering low-priority tree', () => {
packages/react-dom/src/client/ReactDOM.js
+14 -5
@@ -26,10 +26,11 @@ import {
26 flushRoot,
27 createContainer,
28 updateContainer,
29 + batchedEventUpdates,
30 batchedUpdates,
31 unbatchedUpdates,
31 - interactiveUpdates,
32 - flushInteractiveUpdates,
32 + discreteUpdates,
33 + flushDiscreteUpdates,
34 flushSync,
35 flushControlled,
36 injectIntoDevTools,
@@ -481,8 +482,9 @@ function shouldHydrateDueToLegacyHeuristic(container) {
482
483 setBatchingImplementation(
484 batchedUpdates,
484 - interactiveUpdates,
485 - flushInteractiveUpdates,
485 + discreteUpdates,
486 + flushDiscreteUpdates,
487 + batchedEventUpdates,
488 );
489
490 let warnedAboutHydrateAPI = false;
@@ -783,7 +785,14 @@ const ReactDOM: Object = {
785
786 unstable_batchedUpdates: batchedUpdates,
787
786 - unstable_interactiveUpdates: interactiveUpdates,
788 + // TODO remove this legacy method, unstable_discreteUpdates replaces it
789 + unstable_interactiveUpdates: (fn, a, b, c) => {
790 + flushDiscreteUpdates();
791 + return discreteUpdates(fn, a, b, c);
792 + },
793 +
794 + unstable_discreteUpdates: discreteUpdates,
795 + unstable_flushDiscreteUpdates: flushDiscreteUpdates,
796
797 flushSync: flushSync,
798
packages/react-dom/src/events/DOMEventResponderSystem.js
+33 -4
@@ -25,7 +25,11 @@ import type {
25 ReactResponderEvent,
26 } from 'shared/ReactTypes';
27 import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
28 -import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
28 +import {
29 + batchedEventUpdates,
30 + discreteUpdates,
31 + flushDiscreteUpdates,
32 +} from 'events/ReactGenericBatching';
33 import type {Fiber} from 'react-reconciler/src/ReactFiber';
34 import warning from 'shared/warning';
35 import {enableEventAPI} from 'shared/ReactFeatureFlags';
@@ -601,11 +605,14 @@ export function processEventQueue(): void {
605 return;
606 }
607 if (discrete) {
604 - interactiveUpdates(() => {
605 - batchedUpdates(processEvents, events);
608 + if (shouldflushDiscreteUpdates(currentTimeStamp)) {
609 + flushDiscreteUpdates();
610 + }
611 + discreteUpdates(() => {
612 + batchedEventUpdates(processEvents, events);
613 });
614 } else {
608 - batchedUpdates(processEvents, events);
615 + batchedEventUpdates(processEvents, events);
616 }
617 }
618
@@ -1004,3 +1011,25 @@ export function generateListeningKey(
1011 const passiveKey = passive ? '_passive' : '_active';
1012 return `${topLevelType}${passiveKey}`;
1013 }
1014 +
1015 +let lastDiscreteEventTimeStamp = 0;
1016 +
1017 +export function shouldflushDiscreteUpdates(timeStamp: number): boolean {
1018 + // event.timeStamp isn't overly reliable due to inconsistencies in
1019 + // how different browsers have historically provided the time stamp.
1020 + // Some browsers provide high-resolution time stamps for all events,
1021 + // some provide low-resoltion time stamps for all events. FF < 52
1022 + // even mixes both time stamps together. Some browsers even report
1023 + // negative time stamps or time stamps that are 0 (iOS9) in some cases.
1024 + // Given we are only comparing two time stamps with equality (!==),
1025 + // we are safe from the resolution differences. If the time stamp is 0
1026 + // we bail-out of preventing the flush, which can affect semantics,
1027 + // such as if an earlier flush removes or adds event listeners that
1028 + // are fired in the subsequent flush. However, this is the same
1029 + // behaviour as we had before this change, so the risks are low.
1030 + if (timeStamp === 0 || lastDiscreteEventTimeStamp !== timeStamp) {
1031 + lastDiscreteEventTimeStamp = timeStamp;
1032 + return true;
1033 + }
1034 + return false;
1035 +}
packages/react-dom/src/events/ReactDOMEventListener.js
+16 -11
@@ -11,9 +11,16 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
11 import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
13
14 -import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
14 +import {
15 + batchedEventUpdates,
16 + discreteUpdates,
17 + flushDiscreteUpdates,
18 +} from 'events/ReactGenericBatching';
19 import {runExtractedPluginEventsInBatch} from 'events/EventPluginHub';
16 -import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
20 +import {
21 + dispatchEventForResponderEventSystem,
22 + shouldflushDiscreteUpdates,
23 +} from '../events/DOMEventResponderSystem';
24 import {isFiberMounted} from 'react-reconciler/reflection';
25 import {HostRoot} from 'shared/ReactWorkTags';
26 import {
@@ -188,7 +195,7 @@ export function trapEventForResponderEventSystem(
195 } else {
196 eventFlags |= IS_ACTIVE;
197 }
191 - // Check if interactive and wrap in interactiveUpdates
198 + // Check if interactive and wrap in discreteUpdates
199 const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
200 if (passiveBrowserEventsSupported) {
201 addEventCaptureListenerWithPassiveFlag(
@@ -212,7 +219,7 @@ function trapEventForPluginEventSystem(
219 ? dispatchInteractiveEvent
220 : dispatchEvent;
221 const rawEventName = getRawEventName(topLevelType);
215 - // Check if interactive and wrap in interactiveUpdates
222 + // Check if interactive and wrap in discreteUpdates
223 const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
224 if (capture) {
225 addEventCaptureListener(element, rawEventName, listener);
@@ -222,12 +229,10 @@ function trapEventForPluginEventSystem(
229 }
230
231 function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) {
225 - interactiveUpdates(
226 - dispatchEvent,
227 - topLevelType,
228 - eventSystemFlags,
229 - nativeEvent,
230 - );
232 + if (!enableEventAPI || shouldflushDiscreteUpdates(nativeEvent.timeStamp)) {
233 + flushDiscreteUpdates();
234 + }
235 + discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
236 }
237
238 function dispatchEventForPluginEventSystem(
@@ -245,7 +250,7 @@ function dispatchEventForPluginEventSystem(
250 try {
251 // Event queue being processed in the same cycle allows
252 // `preventDefault`.
248 - batchedUpdates(handleTopLevel, bookKeeping);
253 + batchedEventUpdates(handleTopLevel, bookKeeping);
254 } finally {
255 releaseTopLevelCallbackBookKeeping(bookKeeping);
256 }
packages/react-dom/src/fire/ReactFire.js
+14 -5
@@ -31,10 +31,11 @@ import {
31 flushRoot,
32 createContainer,
33 updateContainer,
34 + batchedEventUpdates,
35 batchedUpdates,
36 unbatchedUpdates,
36 - interactiveUpdates,
37 - flushInteractiveUpdates,
37 + discreteUpdates,
38 + flushDiscreteUpdates,
39 flushSync,
40 flushControlled,
41 injectIntoDevTools,
@@ -487,8 +488,9 @@ function shouldHydrateDueToLegacyHeuristic(container) {
488
489 setBatchingImplementation(
490 batchedUpdates,
490 - interactiveUpdates,
491 - flushInteractiveUpdates,
491 + discreteUpdates,
492 + flushDiscreteUpdates,
493 + batchedEventUpdates,
494 );
495
496 let warnedAboutHydrateAPI = false;
@@ -789,7 +791,14 @@ const ReactDOM: Object = {
791
792 unstable_batchedUpdates: batchedUpdates,
793
792 - unstable_interactiveUpdates: interactiveUpdates,
794 + // TODO remove this legacy method, unstable_discreteUpdates replaces it
795 + unstable_interactiveUpdates: (fn, a, b, c) => {
796 + flushDiscreteUpdates();
797 + return discreteUpdates(fn, a, b, c);
798 + },
799 +
800 + unstable_discreteUpdates: discreteUpdates,
801 + unstable_flushDiscreteUpdates: flushDiscreteUpdates,
802
803 flushSync: flushSync,
804
packages/react-events/src/__tests__/Press-test.internal.js
+140
@@ -13,6 +13,7 @@ let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15 let Press;
16 +let Scheduler;
17
18 const DEFAULT_LONG_PRESS_DELAY = 500;
19
@@ -45,6 +46,7 @@ describe('Event responder: Press', () => {
46 React = require('react');
47 ReactDOM = require('react-dom');
48 Press = require('react-events/press');
49 + Scheduler = require('scheduler');
50
51 container = document.createElement('div');
52 document.body.appendChild(container);
@@ -2301,4 +2303,142 @@ describe('Event responder: Press', () => {
2303 },
2304 ]);
2305 });
2306 +
2307 + function dispatchEventWithTimeStamp(elem, name, timeStamp) {
2308 + const event = createEvent(name);
2309 + Object.defineProperty(event, 'timeStamp', {
2310 + value: timeStamp,
2311 + });
2312 + elem.dispatchEvent(event);
2313 + }
2314 +
2315 + it('should properly only flush sync once when the event systems are mixed', () => {
2316 + const ref = React.createRef();
2317 + let renderCounts = 0;
2318 +
2319 + function MyComponent() {
2320 + const [, updateCounter] = React.useState(0);
2321 + renderCounts++;
2322 +
2323 + function handlePress() {
2324 + updateCounter(count => count + 1);
2325 + }
2326 +
2327 + return (
2328 + <div>
2329 + <Press onPress={handlePress}>
2330 + <button
2331 + ref={ref}
2332 + onClick={() => {
2333 + updateCounter(count => count + 1);
2334 + }}>
2335 + Press me
2336 + </button>
2337 + </Press>
2338 + </div>
2339 + );
2340 + }
2341 +
2342 + const newContainer = document.createElement('div');
2343 + const root = ReactDOM.unstable_createRoot(newContainer);
2344 + document.body.appendChild(newContainer);
2345 + root.render(<MyComponent />);
2346 + Scheduler.flushAll();
2347 +
2348 + dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100);
2349 + dispatchEventWithTimeStamp(ref.current, 'pointerup', 100);
2350 + dispatchEventWithTimeStamp(ref.current, 'click', 100);
2351 +
2352 + if (__DEV__) {
2353 + expect(renderCounts).toBe(2);
2354 + } else {
2355 + expect(renderCounts).toBe(1);
2356 + }
2357 + Scheduler.flushAll();
2358 + if (__DEV__) {
2359 + expect(renderCounts).toBe(4);
2360 + } else {
2361 + expect(renderCounts).toBe(2);
2362 + }
2363 +
2364 + dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100);
2365 + dispatchEventWithTimeStamp(ref.current, 'pointerup', 100);
2366 + // Ensure the timeStamp logic works
2367 + dispatchEventWithTimeStamp(ref.current, 'click', 101);
2368 +
2369 + if (__DEV__) {
2370 + expect(renderCounts).toBe(6);
2371 + } else {
2372 + expect(renderCounts).toBe(3);
2373 + }
2374 +
2375 + Scheduler.flushAll();
2376 + document.body.removeChild(newContainer);
2377 + });
2378 +
2379 + it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
2380 + const ref = React.createRef();
2381 + let renderCounts = 0;
2382 +
2383 + function MyComponent() {
2384 + const [, updateCounter] = React.useState(0);
2385 + renderCounts++;
2386 +
2387 + function handlePress() {
2388 + updateCounter(count => count + 1);
2389 + }
2390 +
2391 + return (
2392 + <div>
2393 + <Press onPress={handlePress}>
2394 + <button
2395 + ref={ref}
2396 + onClick={() => {
2397 + // This should flush synchronously
2398 + ReactDOM.unstable_flushDiscreteUpdates();
2399 + updateCounter(count => count + 1);
2400 + }}>
2401 + Press me
2402 + </button>
2403 + </Press>
2404 + </div>
2405 + );
2406 + }
2407 +
2408 + const newContainer = document.createElement('div');
2409 + const root = ReactDOM.unstable_createRoot(newContainer);
2410 + document.body.appendChild(newContainer);
2411 + root.render(<MyComponent />);
2412 + Scheduler.flushAll();
2413 +
2414 + dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100);
2415 + dispatchEventWithTimeStamp(ref.current, 'pointerup', 100);
2416 + dispatchEventWithTimeStamp(ref.current, 'click', 100);
2417 +
2418 + if (__DEV__) {
2419 + expect(renderCounts).toBe(4);
2420 + } else {
2421 + expect(renderCounts).toBe(2);
2422 + }
2423 + Scheduler.flushAll();
2424 + if (__DEV__) {
2425 + expect(renderCounts).toBe(6);
2426 + } else {
2427 + expect(renderCounts).toBe(3);
2428 + }
2429 +
2430 + dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100);
2431 + dispatchEventWithTimeStamp(ref.current, 'pointerup', 100);
2432 + // Ensure the timeStamp logic works
2433 + dispatchEventWithTimeStamp(ref.current, 'click', 101);
2434 +
2435 + if (__DEV__) {
2436 + expect(renderCounts).toBe(8);
2437 + } else {
2438 + expect(renderCounts).toBe(4);
2439 + }
2440 +
2441 + Scheduler.flushAll();
2442 + document.body.removeChild(newContainer);
2443 + });
2444 });
packages/react-native-renderer/src/ReactFabric.js
+6 -4
@@ -15,9 +15,10 @@ import './ReactFabricInjection';
15 import {
16 findHostInstance,
17 findHostInstanceWithWarning,
18 + batchedEventUpdates,
19 batchedUpdates as batchedUpdatesImpl,
19 - interactiveUpdates,
20 - flushInteractiveUpdates,
20 + discreteUpdates,
21 + flushDiscreteUpdates,
22 createContainer,
23 updateContainer,
24 injectIntoDevTools,
@@ -94,8 +95,9 @@ function findNodeHandle(componentOrHandle: any): ?number {
95
96 setBatchingImplementation(
97 batchedUpdatesImpl,
97 - interactiveUpdates,
98 - flushInteractiveUpdates,
98 + discreteUpdates,
99 + flushDiscreteUpdates,
100 + batchedEventUpdates,
101 );
102
103 const roots = new Map();
packages/react-native-renderer/src/ReactNativeRenderer.js
+6 -4
@@ -16,8 +16,9 @@ import {
16 findHostInstance,
17 findHostInstanceWithWarning,
18 batchedUpdates as batchedUpdatesImpl,
19 - interactiveUpdates,
20 - flushInteractiveUpdates,
19 + batchedEventUpdates,
20 + discreteUpdates,
21 + flushDiscreteUpdates,
22 createContainer,
23 updateContainer,
24 injectIntoDevTools,
@@ -99,8 +100,9 @@ function findNodeHandle(componentOrHandle: any): ?number {
100
101 setBatchingImplementation(
102 batchedUpdatesImpl,
102 - interactiveUpdates,
103 - flushInteractiveUpdates,
103 + discreteUpdates,
104 + flushDiscreteUpdates,
105 + batchedEventUpdates,
106 );
107
108 function computeComponentStackForErrorReporting(reactTag: number): string {
packages/react-noop-renderer/src/createReactNoop.js
+3 -1
@@ -1124,7 +1124,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1124
1125 unbatchedUpdates: NoopRenderer.unbatchedUpdates,
1126
1127 - interactiveUpdates: NoopRenderer.interactiveUpdates,
1127 + discreteUpdates: NoopRenderer.discreteUpdates,
1128 +
1129 + flushDiscreteUpdates: NoopRenderer.flushDiscreteUpdates,
1130
1131 flushSync(fn: () => mixed) {
1132 NoopRenderer.flushSync(fn);
packages/react-reconciler/src/ReactFiberReconciler.js
+6 -4
@@ -46,14 +46,15 @@ import {
46 computeExpirationForFiber,
47 scheduleWork,
48 flushRoot,
49 + batchedEventUpdates,
50 batchedUpdates,
51 unbatchedUpdates,
52 flushSync,
53 flushControlled,
54 deferredUpdates,
55 syncUpdates,
55 - interactiveUpdates,
56 - flushInteractiveUpdates,
56 + discreteUpdates,
57 + flushDiscreteUpdates,
58 flushPassiveEffects,
59 } from './ReactFiberScheduler';
60 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
@@ -321,12 +322,13 @@ export function updateContainer(
322 export {
323 flushRoot,
324 computeUniqueAsyncExpiration,
325 + batchedEventUpdates,
326 batchedUpdates,
327 unbatchedUpdates,
328 deferredUpdates,
329 syncUpdates,
328 - interactiveUpdates,
329 - flushInteractiveUpdates,
330 + discreteUpdates,
331 + flushDiscreteUpdates,
332 flushControlled,
333 flushSync,
334 flushPassiveEffects,
packages/react-reconciler/src/ReactFiberScheduler.js
+38 -18
@@ -178,13 +178,14 @@ const {
178 ReactShouldWarnActingUpdates,
179 } = ReactSharedInternals;
180
181 -type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5;
181 +type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6;
182 const NotWorking = 0;
183 const BatchedPhase = 1;
184 const LegacyUnbatchedPhase = 2;
185 const FlushSyncPhase = 3;
186 const RenderPhase = 4;
187 const CommitPhase = 5;
188 +const BatchedEventPhase = 6;
189
190 type RootExitStatus = 0 | 1 | 2 | 3 | 4;
191 const RootIncomplete = 0;
@@ -566,14 +567,22 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
567 flushSyncCallbackQueue();
568 }
569
569 -export function flushInteractiveUpdates() {
570 - if (workPhase === RenderPhase || workPhase === CommitPhase) {
571 - // Can't synchronously flush interactive updates if React is already
572 - // working. This is currently a no-op.
573 - // TODO: Should we fire a warning? This happens if you synchronously invoke
574 - // an input event inside an effect, like with `element.click()`.
570 +export function flushDiscreteUpdates() {
571 + // TODO: we ideally do not want to early reurn for BatchedPhase here either.
572 + // Removing this causes act() tests to fail, so we should follow up.
573 + if (workPhase === CommitPhase || workPhase === BatchedPhase) {
574 + // We're inside the commit phase or batched phase, so we can't
575 + // synchronously flush pending work. This is probably a nested event
576 + // dispatch triggered by a lifecycle/effect, like `el.focus()`. Exit.
577 return;
578 }
579 + if (workPhase === RenderPhase) {
580 + invariant(
581 + false,
582 + 'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
583 + 'already rendering.',
584 + );
585 + }
586 flushPendingDiscreteUpdates();
587 if (!revertPassiveEffectsChange) {
588 // If the discrete updates scheduled passive effects, flush them now so that
@@ -604,21 +613,12 @@ export function deferredUpdates<A>(fn: () => A): A {
613 return runWithPriority(NormalPriority, fn);
614 }
615
607 -export function interactiveUpdates<A, B, C, R>(
616 +export function discreteUpdates<A, B, C, R>(
617 fn: (A, B, C) => R,
618 a: A,
619 b: B,
620 c: C,
621 ): R {
613 - if (workPhase === NotWorking) {
614 - // TODO: Remove this call. Instead of doing this automatically, the caller
615 - // should explicitly call flushInteractiveUpdates.
616 - flushPendingDiscreteUpdates();
617 - }
618 - if (!revertPassiveEffectsChange) {
619 - // TODO: Remove this call for the same reason as above.
620 - flushPassiveEffects();
621 - }
622 return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
623 }
624
@@ -660,8 +660,28 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
660 }
661 }
662
663 +export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
664 + if (workPhase !== NotWorking) {
665 + // We're already working, or inside a batch, so batchedUpdates is a no-op.
666 + return fn(a);
667 + }
668 + const prevWorkPhase = workPhase;
669 + workPhase = BatchedEventPhase;
670 + try {
671 + return fn(a);
672 + } finally {
673 + workPhase = prevWorkPhase;
674 + // Flush the immediate callbacks that were scheduled during this batch
675 + flushSyncCallbackQueue();
676 + }
677 +}
678 +
679 export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
664 - if (workPhase !== BatchedPhase && workPhase !== FlushSyncPhase) {
680 + if (
681 + workPhase !== BatchedPhase &&
682 + workPhase !== FlushSyncPhase &&
683 + workPhase !== BatchedEventPhase
684 + ) {
685 // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is
686 // a no-op.
687 return fn(a);
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+4 -2
@@ -885,7 +885,8 @@ describe('ReactHooksWithNoopRenderer', () => {
885 // A discrete event forces the passive effect to be flushed --
886 // updateCount(1) happens first, so 2 wins.
887
888 - ReactNoop.interactiveUpdates(() => {
888 + ReactNoop.flushDiscreteUpdates();
889 + ReactNoop.discreteUpdates(() => {
890 // (use batchedUpdates to silence the act() warning)
891 ReactNoop.batchedUpdates(() => {
892 _updateCount(2);
@@ -939,7 +940,8 @@ describe('ReactHooksWithNoopRenderer', () => {
940
941 // A discrete event forces the passive effect to be flushed --
942 // updateCount(1) happens first, so 2 wins.
942 - ReactNoop.interactiveUpdates(() => {
943 + ReactNoop.flushDiscreteUpdates();
944 + ReactNoop.discreteUpdates(() => {
945 // use batchedUpdates to silence the act warning
946 ReactNoop.batchedUpdates(() => _updateCount(2));
947 });
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+3 -3
@@ -431,7 +431,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
431
432 // Schedule a high pri update and a low pri update, without rendering in
433 // between.
434 - ReactNoop.interactiveUpdates(() => {
434 + ReactNoop.discreteUpdates(() => {
435 // High pri
436 ReactNoop.render(<App />);
437 });
@@ -1443,7 +1443,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1443 );
1444 }
1445
1446 - ReactNoop.interactiveUpdates(() => ReactNoop.render(<Foo />));
1446 + ReactNoop.discreteUpdates(() => ReactNoop.render(<Foo />));
1447 expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1448
1449 // Advance some time.
@@ -1504,7 +1504,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1504 );
1505 }
1506
1507 - ReactNoop.interactiveUpdates(() => ReactNoop.render(<App />));
1507 + ReactNoop.discreteUpdates(() => ReactNoop.render(<App />));
1508 Scheduler.flushAll();
1509
1510 // Warning is not flushed until the commit phase