[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