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

Replace unbatchedUpdates with flushSync (#21776)

There's a weird quirk leftover from the old Stack (pre-Fiber) implementation where the initial mount of a leagcy (ReactDOM.render) root is flushed synchronously even inside `batchedUpdates`. The original workaround for this was an internal method called `unbatchedUpdates`. We've since added another API that works almost the same way, `flushSync`. The only difference is that `unbatchedUpdates` would not cause other pending updates to flush too, only the newly mounted root. `flushSync` flushes all pending sync work across all roots. This was to preserve the exact behavior of the Stack implementation. But since it's close enough, let's just use `flushSync`. It's unlikely anyone's app accidentally relies on this subtle difference, and the legacy API is deprecated in 18, anyway.

Andrew Clark committed Jul 1, 2021 at 18:14 UTC ed6c091fe961a3b95e956ebcefe8f152177b1fb7
12 files changed +59 -239
packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+14 -14
@@ -40,7 +40,7 @@ Object {
40 6 => 1,
41 },
42 "passiveEffectDuration": null,
43 - "priorityLevel": "Normal",
43 + "priorityLevel": "Immediate",
44 "timestamp": 16,
45 "updaters": Array [
46 Object {
@@ -87,7 +87,7 @@ Object {
87 4 => 2,
88 },
89 "passiveEffectDuration": null,
90 - "priorityLevel": "Normal",
90 + "priorityLevel": "Immediate",
91 "timestamp": 15,
92 "updaters": Array [
93 Object {
@@ -186,7 +186,7 @@ Object {
186 6 => 1,
187 },
188 "passiveEffectDuration": null,
189 - "priorityLevel": "Normal",
189 + "priorityLevel": "Immediate",
190 "timestamp": 12,
191 "updaters": Array [
192 Object {
@@ -445,7 +445,7 @@ Object {
445 ],
446 ],
447 "passiveEffectDuration": null,
448 - "priorityLevel": "Normal",
448 + "priorityLevel": "Immediate",
449 "timestamp": 12,
450 "updaters": Array [
451 Object {
@@ -938,7 +938,7 @@ Object {
938 ],
939 ],
940 "passiveEffectDuration": null,
941 - "priorityLevel": "Normal",
941 + "priorityLevel": "Immediate",
942 "timestamp": 11,
943 "updaters": Array [
944 Object {
@@ -1597,7 +1597,7 @@ Object {
1597 17 => 1,
1598 },
1599 "passiveEffectDuration": null,
1600 - "priorityLevel": "Normal",
1600 + "priorityLevel": "Immediate",
1601 "timestamp": 24,
1602 "updaters": Array [
1603 Object {
@@ -1687,7 +1687,7 @@ Object {
1687 "fiberActualDurations": Map {},
1688 "fiberSelfDurations": Map {},
1689 "passiveEffectDuration": 0,
1690 - "priorityLevel": "Normal",
1690 + "priorityLevel": "Immediate",
1691 "timestamp": 34,
1692 "updaters": Array [
1693 Object {
@@ -2223,7 +2223,7 @@ Object {
2223 ],
2224 ],
2225 "passiveEffectDuration": null,
2226 - "priorityLevel": "Normal",
2226 + "priorityLevel": "Immediate",
2227 "timestamp": 24,
2228 "updaters": Array [
2229 Object {
@@ -2310,7 +2310,7 @@ Object {
2310 "fiberActualDurations": Array [],
2311 "fiberSelfDurations": Array [],
2312 "passiveEffectDuration": 0,
2313 - "priorityLevel": "Normal",
2313 + "priorityLevel": "Immediate",
2314 "timestamp": 34,
2315 "updaters": Array [
2316 Object {
@@ -2431,7 +2431,7 @@ Object {
2431 2 => 0,
2432 },
2433 "passiveEffectDuration": null,
2434 - "priorityLevel": "Normal",
2434 + "priorityLevel": "Immediate",
2435 "timestamp": 0,
2436 "updaters": Array [
2437 Object {
@@ -2506,7 +2506,7 @@ Object {
2506 3 => 0,
2507 },
2508 "passiveEffectDuration": 0,
2509 - "priorityLevel": "Normal",
2509 + "priorityLevel": "Immediate",
2510 "timestamp": 0,
2511 "updaters": Array [
2512 Object {
@@ -2715,7 +2715,7 @@ Object {
2715 ],
2716 ],
2717 "passiveEffectDuration": 0,
2718 - "priorityLevel": "Normal",
2718 + "priorityLevel": "Immediate",
2719 "timestamp": 0,
2720 "updaters": Array [
2721 Object {
@@ -3071,7 +3071,7 @@ Object {
3071 7 => 0,
3072 },
3073 "passiveEffectDuration": null,
3074 - "priorityLevel": "Normal",
3074 + "priorityLevel": "Immediate",
3075 "timestamp": 0,
3076 "updaters": Array [
3077 Object {
@@ -3515,7 +3515,7 @@ Object {
3515 ],
3516 ],
3517 "passiveEffectDuration": null,
3518 - "priorityLevel": "Normal",
3518 + "priorityLevel": "Immediate",
3519 "timestamp": 0,
3520 "updaters": Array [
3521 Object {
packages/react-dom/src/__tests__/ReactMount-test.js
+6 -6
@@ -277,7 +277,7 @@ describe('ReactMount', () => {
277 expect(calls).toBe(5);
278 });
279
280 - it('initial mount is sync inside batchedUpdates, but task work is deferred until the end of the batch', () => {
280 + it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
281 const container1 = document.createElement('div');
282 const container2 = document.createElement('div');
283
@@ -302,12 +302,12 @@ describe('ReactMount', () => {
302
303 // Initial mount on another root. Should flush immediately.
304 ReactDOM.render(<Foo>a</Foo>, container2);
305 - // The update did not flush yet.
306 - expect(container1.textContent).toEqual('1');
307 - // The initial mount flushed, but not the update scheduled in cDM.
308 - expect(container2.textContent).toEqual('a');
305 + // The earlier update also flushed, since flushSync flushes all pending
306 + // sync work across all roots.
307 + expect(container1.textContent).toEqual('2');
308 + // Layout updates are also flushed synchronously
309 + expect(container2.textContent).toEqual('a!');
310 });
310 - // All updates have flushed.
311 expect(container1.textContent).toEqual('2');
312 expect(container2.textContent).toEqual('a!');
313 });
packages/react-dom/src/client/ReactDOMLegacy.js
+3 -3
@@ -29,7 +29,7 @@ import {
29 createContainer,
30 findHostInstanceWithNoPortals,
31 updateContainer,
32 - unbatchedUpdates,
32 + flushSyncWithoutWarningIfAlreadyRendering,
33 getPublicRootInstance,
34 findHostInstance,
35 findHostInstanceWithWarning,
@@ -174,7 +174,7 @@ function legacyRenderSubtreeIntoContainer(
174 };
175 }
176 // Initial mount should not be batched.
177 - unbatchedUpdates(() => {
177 + flushSyncWithoutWarningIfAlreadyRendering(() => {
178 updateContainer(children, fiberRoot, parentComponent, callback);
179 });
180 } else {
@@ -357,7 +357,7 @@ export function unmountComponentAtNode(container: Container) {
357 }
358
359 // Unmount should not be batched.
360 - unbatchedUpdates(() => {
360 + flushSyncWithoutWarningIfAlreadyRendering(() => {
361 legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
362 // $FlowFixMe This should probably use `delete container._reactRootContainer`
363 container._reactRootContainer = null;
packages/react-noop-renderer/src/ReactNoop.js
-1
@@ -38,7 +38,6 @@ export const {
38 flushExpired,
39 batchedUpdates,
40 deferredUpdates,
41 - unbatchedUpdates,
41 discreteUpdates,
42 idleUpdates,
43 flushSync,
packages/react-noop-renderer/src/ReactNoopPersistent.js
-1
@@ -38,7 +38,6 @@ export const {
38 flushExpired,
39 batchedUpdates,
40 deferredUpdates,
41 - unbatchedUpdates,
41 discreteUpdates,
42 idleUpdates,
43 flushDiscreteUpdates,
packages/react-noop-renderer/src/createReactNoop.js
-2
@@ -901,8 +901,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
901
902 deferredUpdates: NoopRenderer.deferredUpdates,
903
904 - unbatchedUpdates: NoopRenderer.unbatchedUpdates,
905 -
904 discreteUpdates: NoopRenderer.discreteUpdates,
905
906 idleUpdates<T>(fn: () => T): T {
packages/react-reconciler/src/ReactFiberReconciler.js
-5
@@ -18,7 +18,6 @@ import {
18 createContainer as createContainer_old,
19 updateContainer as updateContainer_old,
20 batchedUpdates as batchedUpdates_old,
21 - unbatchedUpdates as unbatchedUpdates_old,
21 deferredUpdates as deferredUpdates_old,
22 discreteUpdates as discreteUpdates_old,
23 flushControlled as flushControlled_old,
@@ -56,7 +55,6 @@ import {
55 createContainer as createContainer_new,
56 updateContainer as updateContainer_new,
57 batchedUpdates as batchedUpdates_new,
59 - unbatchedUpdates as unbatchedUpdates_new,
58 deferredUpdates as deferredUpdates_new,
59 discreteUpdates as discreteUpdates_new,
60 flushControlled as flushControlled_new,
@@ -99,9 +97,6 @@ export const updateContainer = enableNewReconciler
97 export const batchedUpdates = enableNewReconciler
98 ? batchedUpdates_new
99 : batchedUpdates_old;
102 -export const unbatchedUpdates = enableNewReconciler
103 - ? unbatchedUpdates_new
104 - : unbatchedUpdates_old;
100 export const deferredUpdates = enableNewReconciler
101 ? deferredUpdates_new
102 : deferredUpdates_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
-2
@@ -52,7 +52,6 @@ import {
52 scheduleUpdateOnFiber,
53 flushRoot,
54 batchedUpdates,
55 - unbatchedUpdates,
55 flushSync,
56 flushControlled,
57 deferredUpdates,
@@ -327,7 +326,6 @@ export function updateContainer(
326
327 export {
328 batchedUpdates,
330 - unbatchedUpdates,
329 deferredUpdates,
330 discreteUpdates,
331 flushControlled,
packages/react-reconciler/src/ReactFiberReconciler.old.js
-2
@@ -52,7 +52,6 @@ import {
52 scheduleUpdateOnFiber,
53 flushRoot,
54 batchedUpdates,
55 - unbatchedUpdates,
55 flushSync,
56 flushControlled,
57 deferredUpdates,
@@ -327,7 +326,6 @@ export function updateContainer(
326
327 export {
328 batchedUpdates,
330 - unbatchedUpdates,
329 deferredUpdates,
330 discreteUpdates,
331 flushControlled,
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+18 -72
@@ -246,12 +246,11 @@ const {
246
247 type ExecutionContext = number;
248
249 -export const NoContext = /* */ 0b00000;
250 -const BatchedContext = /* */ 0b00001;
251 -const LegacyUnbatchedContext = /* */ 0b00010;
252 -const RenderContext = /* */ 0b00100;
253 -const CommitContext = /* */ 0b01000;
254 -export const RetryAfterError = /* */ 0b10000;
249 +export const NoContext = /* */ 0b0000;
250 +const BatchedContext = /* */ 0b0001;
251 +const RenderContext = /* */ 0b0010;
252 +const CommitContext = /* */ 0b0100;
253 +export const RetryAfterError = /* */ 0b1000;
254
255 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
256 const RootIncomplete = 0;
@@ -515,35 +514,19 @@ export function scheduleUpdateOnFiber(
514 }
515 }
516
518 - if (lane === SyncLane) {
519 - if (
520 - // Check if we're inside unbatchedUpdates
521 - (executionContext & LegacyUnbatchedContext) !== NoContext &&
522 - // Check if we're not already rendering
523 - (executionContext & (RenderContext | CommitContext)) === NoContext
524 - ) {
525 - // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
526 - // root inside of batchedUpdates should be synchronous, but layout updates
527 - // should be deferred until the end of the batch.
528 - performSyncWorkOnRoot(root);
529 - } else {
530 - ensureRootIsScheduled(root, eventTime);
531 - if (
532 - executionContext === NoContext &&
533 - (fiber.mode & ConcurrentMode) === NoMode
534 - ) {
535 - // Flush the synchronous work now, unless we're already working or inside
536 - // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
537 - // scheduleCallbackForFiber to preserve the ability to schedule a callback
538 - // without immediately flushing it. We only do this for user-initiated
539 - // updates, to preserve historical behavior of legacy mode.
540 - resetRenderTimer();
541 - flushSyncCallbacksOnlyInLegacyMode();
542 - }
543 - }
544 - } else {
545 - // Schedule other updates after in case the callback is sync.
546 - ensureRootIsScheduled(root, eventTime);
517 + ensureRootIsScheduled(root, eventTime);
518 + if (
519 + lane === SyncLane &&
520 + executionContext === NoContext &&
521 + (fiber.mode & ConcurrentMode) === NoMode
522 + ) {
523 + // Flush the synchronous work now, unless we're already working or inside
524 + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
525 + // scheduleCallbackForFiber to preserve the ability to schedule a callback
526 + // without immediately flushing it. We only do this for user-initiated
527 + // updates, to preserve historical behavior of legacy mode.
528 + resetRenderTimer();
529 + flushSyncCallbacksOnlyInLegacyMode();
530 }
531
532 return root;
@@ -1095,25 +1078,6 @@ export function discreteUpdates<A, B, C, D, R>(
1078 }
1079 }
1080
1098 -export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1099 - const prevExecutionContext = executionContext;
1100 - executionContext &= ~BatchedContext;
1101 - executionContext |= LegacyUnbatchedContext;
1102 - try {
1103 - return fn(a);
1104 - } finally {
1105 - executionContext = prevExecutionContext;
1106 - // If there were legacy sync updates, flush them at the end of the outer
1107 - // most batchedUpdates-like method.
1108 - if (executionContext === NoContext) {
1109 - resetRenderTimer();
1110 - // TODO: I think this call is redundant, because we flush inside
1111 - // scheduleUpdateOnFiber when LegacyUnbatchedContext is set.
1112 - flushSyncCallbacksOnlyInLegacyMode();
1113 - }
1114 - }
1115 -}
1116 -
1081 export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1082 fn: A => R,
1083 a: A,
@@ -1954,24 +1918,6 @@ function commitRootImpl(root, renderPriorityLevel) {
1918 throw error;
1919 }
1920
1957 - if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
1958 - if (__DEV__) {
1959 - if (enableDebugTracing) {
1960 - logCommitStopped();
1961 - }
1962 - }
1963 -
1964 - if (enableSchedulingProfiler) {
1965 - markCommitStopped();
1966 - }
1967 -
1968 - // This is a legacy edge case. We just committed the initial mount of
1969 - // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
1970 - // synchronously, but layout updates should be deferred until the end
1971 - // of the batch.
1972 - return null;
1973 - }
1974 -
1921 // If the passive effects are the result of a discrete render, flush them
1922 // synchronously at the end of the current task so that the result is
1923 // immediately observable. Otherwise, we assume that they are not
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+18 -72
@@ -246,12 +246,11 @@ const {
246
247 type ExecutionContext = number;
248
249 -export const NoContext = /* */ 0b00000;
250 -const BatchedContext = /* */ 0b00001;
251 -const LegacyUnbatchedContext = /* */ 0b00010;
252 -const RenderContext = /* */ 0b00100;
253 -const CommitContext = /* */ 0b01000;
254 -export const RetryAfterError = /* */ 0b10000;
249 +export const NoContext = /* */ 0b0000;
250 +const BatchedContext = /* */ 0b0001;
251 +const RenderContext = /* */ 0b0010;
252 +const CommitContext = /* */ 0b0100;
253 +export const RetryAfterError = /* */ 0b1000;
254
255 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
256 const RootIncomplete = 0;
@@ -515,35 +514,19 @@ export function scheduleUpdateOnFiber(
514 }
515 }
516
518 - if (lane === SyncLane) {
519 - if (
520 - // Check if we're inside unbatchedUpdates
521 - (executionContext & LegacyUnbatchedContext) !== NoContext &&
522 - // Check if we're not already rendering
523 - (executionContext & (RenderContext | CommitContext)) === NoContext
524 - ) {
525 - // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
526 - // root inside of batchedUpdates should be synchronous, but layout updates
527 - // should be deferred until the end of the batch.
528 - performSyncWorkOnRoot(root);
529 - } else {
530 - ensureRootIsScheduled(root, eventTime);
531 - if (
532 - executionContext === NoContext &&
533 - (fiber.mode & ConcurrentMode) === NoMode
534 - ) {
535 - // Flush the synchronous work now, unless we're already working or inside
536 - // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
537 - // scheduleCallbackForFiber to preserve the ability to schedule a callback
538 - // without immediately flushing it. We only do this for user-initiated
539 - // updates, to preserve historical behavior of legacy mode.
540 - resetRenderTimer();
541 - flushSyncCallbacksOnlyInLegacyMode();
542 - }
543 - }
544 - } else {
545 - // Schedule other updates after in case the callback is sync.
546 - ensureRootIsScheduled(root, eventTime);
517 + ensureRootIsScheduled(root, eventTime);
518 + if (
519 + lane === SyncLane &&
520 + executionContext === NoContext &&
521 + (fiber.mode & ConcurrentMode) === NoMode
522 + ) {
523 + // Flush the synchronous work now, unless we're already working or inside
524 + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
525 + // scheduleCallbackForFiber to preserve the ability to schedule a callback
526 + // without immediately flushing it. We only do this for user-initiated
527 + // updates, to preserve historical behavior of legacy mode.
528 + resetRenderTimer();
529 + flushSyncCallbacksOnlyInLegacyMode();
530 }
531
532 return root;
@@ -1095,25 +1078,6 @@ export function discreteUpdates<A, B, C, D, R>(
1078 }
1079 }
1080
1098 -export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1099 - const prevExecutionContext = executionContext;
1100 - executionContext &= ~BatchedContext;
1101 - executionContext |= LegacyUnbatchedContext;
1102 - try {
1103 - return fn(a);
1104 - } finally {
1105 - executionContext = prevExecutionContext;
1106 - // If there were legacy sync updates, flush them at the end of the outer
1107 - // most batchedUpdates-like method.
1108 - if (executionContext === NoContext) {
1109 - resetRenderTimer();
1110 - // TODO: I think this call is redundant, because we flush inside
1111 - // scheduleUpdateOnFiber when LegacyUnbatchedContext is set.
1112 - flushSyncCallbacksOnlyInLegacyMode();
1113 - }
1114 - }
1115 -}
1116 -
1081 export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1082 fn: A => R,
1083 a: A,
@@ -1954,24 +1918,6 @@ function commitRootImpl(root, renderPriorityLevel) {
1918 throw error;
1919 }
1920
1957 - if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
1958 - if (__DEV__) {
1959 - if (enableDebugTracing) {
1960 - logCommitStopped();
1961 - }
1962 - }
1963 -
1964 - if (enableSchedulingProfiler) {
1965 - markCommitStopped();
1966 - }
1967 -
1968 - // This is a legacy edge case. We just committed the initial mount of
1969 - // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
1970 - // synchronously, but layout updates should be deferred until the end
1971 - // of the batch.
1972 - return null;
1973 - }
1974 -
1921 // If the passive effects are the result of a discrete render, flush them
1922 // synchronously at the end of the current task so that the result is
1923 // immediately observable. Otherwise, we assume that they are not
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
-59
@@ -349,63 +349,4 @@ describe('ReactIncrementalScheduling', () => {
349 // The updates should all be flushed with Task priority
350 expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
351 });
352 -
353 - it('can opt-out of batching using unbatchedUpdates', () => {
354 - ReactNoop.flushSync(() => {
355 - ReactNoop.render(<span prop={0} />);
356 - expect(ReactNoop.getChildren()).toEqual([]);
357 - // Should not have flushed yet because we're still batching
358 -
359 - // unbatchedUpdates reverses the effect of batchedUpdates, so sync
360 - // updates are not batched
361 - ReactNoop.unbatchedUpdates(() => {
362 - ReactNoop.render(<span prop={1} />);
363 - expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
364 - ReactNoop.render(<span prop={2} />);
365 - expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
366 - });
367 -
368 - ReactNoop.render(<span prop={3} />);
369 - expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
370 - });
371 - // Remaining update is now flushed
372 - expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
373 - });
374 -
375 - it('nested updates are always deferred, even inside unbatchedUpdates', () => {
376 - let instance;
377 - class Foo extends React.Component {
378 - state = {step: 0};
379 - componentDidUpdate() {
380 - Scheduler.unstable_yieldValue('componentDidUpdate: ' + this.state.step);
381 - if (this.state.step === 1) {
382 - ReactNoop.unbatchedUpdates(() => {
383 - // This is a nested state update, so it should not be
384 - // flushed synchronously, even though we wrapped it
385 - // in unbatchedUpdates.
386 - this.setState({step: 2});
387 - });
388 - expect(Scheduler).toHaveYielded([
389 - 'render: 1',
390 - 'componentDidUpdate: 1',
391 - ]);
392 - expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
393 - }
394 - }
395 - render() {
396 - Scheduler.unstable_yieldValue('render: ' + this.state.step);
397 - instance = this;
398 - return <span prop={this.state.step} />;
399 - }
400 - }
401 - ReactNoop.render(<Foo />);
402 - expect(Scheduler).toFlushAndYield(['render: 0']);
403 - expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
404 -
405 - ReactNoop.flushSync(() => {
406 - instance.setState({step: 1});
407 - });
408 - expect(Scheduler).toHaveYielded(['render: 2', 'componentDidUpdate: 2']);
409 - expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
410 - });
352 });