@samitouri / QOS-React-2 / commits / 9ccc25a0ea

Reverting recent flushSync changes (#21816)

Brian Vaughn committed Jul 7, 2021 at 15:07 UTC 9ccc25a0ea322b39ea04f8dfc03620ea90e0da94
16 files changed +338 -132
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": "Immediate",
43 + "priorityLevel": "Normal",
44 "timestamp": 16,
45 "updaters": Array [
46 Object {
@@ -87,7 +87,7 @@ Object {
87 4 => 2,
88 },
89 "passiveEffectDuration": null,
90 - "priorityLevel": "Immediate",
90 + "priorityLevel": "Normal",
91 "timestamp": 15,
92 "updaters": Array [
93 Object {
@@ -186,7 +186,7 @@ Object {
186 6 => 1,
187 },
188 "passiveEffectDuration": null,
189 - "priorityLevel": "Immediate",
189 + "priorityLevel": "Normal",
190 "timestamp": 12,
191 "updaters": Array [
192 Object {
@@ -445,7 +445,7 @@ Object {
445 ],
446 ],
447 "passiveEffectDuration": null,
448 - "priorityLevel": "Immediate",
448 + "priorityLevel": "Normal",
449 "timestamp": 12,
450 "updaters": Array [
451 Object {
@@ -938,7 +938,7 @@ Object {
938 ],
939 ],
940 "passiveEffectDuration": null,
941 - "priorityLevel": "Immediate",
941 + "priorityLevel": "Normal",
942 "timestamp": 11,
943 "updaters": Array [
944 Object {
@@ -1597,7 +1597,7 @@ Object {
1597 17 => 1,
1598 },
1599 "passiveEffectDuration": null,
1600 - "priorityLevel": "Immediate",
1600 + "priorityLevel": "Normal",
1601 "timestamp": 24,
1602 "updaters": Array [
1603 Object {
@@ -1687,7 +1687,7 @@ Object {
1687 "fiberActualDurations": Map {},
1688 "fiberSelfDurations": Map {},
1689 "passiveEffectDuration": 0,
1690 - "priorityLevel": "Immediate",
1690 + "priorityLevel": "Normal",
1691 "timestamp": 34,
1692 "updaters": Array [
1693 Object {
@@ -2223,7 +2223,7 @@ Object {
2223 ],
2224 ],
2225 "passiveEffectDuration": null,
2226 - "priorityLevel": "Immediate",
2226 + "priorityLevel": "Normal",
2227 "timestamp": 24,
2228 "updaters": Array [
2229 Object {
@@ -2310,7 +2310,7 @@ Object {
2310 "fiberActualDurations": Array [],
2311 "fiberSelfDurations": Array [],
2312 "passiveEffectDuration": 0,
2313 - "priorityLevel": "Immediate",
2313 + "priorityLevel": "Normal",
2314 "timestamp": 34,
2315 "updaters": Array [
2316 Object {
@@ -2431,7 +2431,7 @@ Object {
2431 2 => 0,
2432 },
2433 "passiveEffectDuration": null,
2434 - "priorityLevel": "Immediate",
2434 + "priorityLevel": "Normal",
2435 "timestamp": 0,
2436 "updaters": Array [
2437 Object {
@@ -2506,7 +2506,7 @@ Object {
2506 3 => 0,
2507 },
2508 "passiveEffectDuration": 0,
2509 - "priorityLevel": "Immediate",
2509 + "priorityLevel": "Normal",
2510 "timestamp": 0,
2511 "updaters": Array [
2512 Object {
@@ -2715,7 +2715,7 @@ Object {
2715 ],
2716 ],
2717 "passiveEffectDuration": 0,
2718 - "priorityLevel": "Immediate",
2718 + "priorityLevel": "Normal",
2719 "timestamp": 0,
2720 "updaters": Array [
2721 Object {
@@ -3071,7 +3071,7 @@ Object {
3071 7 => 0,
3072 },
3073 "passiveEffectDuration": null,
3074 - "priorityLevel": "Immediate",
3074 + "priorityLevel": "Normal",
3075 "timestamp": 0,
3076 "updaters": Array [
3077 Object {
@@ -3515,7 +3515,7 @@ Object {
3515 ],
3516 ],
3517 "passiveEffectDuration": null,
3518 - "priorityLevel": "Immediate",
3518 + "priorityLevel": "Normal",
3519 "timestamp": 0,
3520 "updaters": Array [
3521 Object {
packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+7 -1
@@ -1154,13 +1154,19 @@ describe('ReactDOMFiber', () => {
1154 expect(ops).toEqual(['A']);
1155
1156 if (__DEV__) {
1157 - expect(console.error.calls.count()).toBe(2);
1157 + const errorCalls = console.error.calls.count();
1158 expect(console.error.calls.argsFor(0)[0]).toMatch(
1159 'ReactDOM.render is no longer supported in React 18',
1160 );
1161 expect(console.error.calls.argsFor(1)[0]).toMatch(
1162 'ReactDOM.render is no longer supported in React 18',
1163 );
1164 + // TODO: this warning shouldn't be firing in the first place if user didn't call it.
1165 + for (let i = 2; i < errorCalls; i++) {
1166 + expect(console.error.calls.argsFor(i)[0]).toMatch(
1167 + 'unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.',
1168 + );
1169 + }
1170 }
1171 });
1172
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 of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
280 + it('initial mount is sync inside batchedUpdates, but task work is deferred until the end of the batch', () => {
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 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!');
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');
309 });
310 + // All updates have flushed.
311 expect(container1.textContent).toEqual('2');
312 expect(container2.textContent).toEqual('a!');
313 });
packages/react-dom/src/client/ReactDOM.js
+2 -2
@@ -23,8 +23,8 @@ import {createEventHandle} from './ReactDOMEventHandle';
23 import {
24 batchedUpdates,
25 discreteUpdates,
26 + flushDiscreteUpdates,
27 flushSync,
27 - flushSyncWithoutWarningIfAlreadyRendering,
28 flushControlled,
29 injectIntoDevTools,
30 attemptSynchronousHydration,
@@ -100,7 +100,7 @@ setRestoreImplementation(restoreControlledState);
100 setBatchingImplementation(
101 batchedUpdates,
102 discreteUpdates,
103 - flushSyncWithoutWarningIfAlreadyRendering,
103 + flushDiscreteUpdates,
104 );
105
106 function createPortal(
packages/react-dom/src/client/ReactDOMLegacy.js
+3 -3
@@ -29,7 +29,7 @@ import {
29 createContainer,
30 findHostInstanceWithNoPortals,
31 updateContainer,
32 - flushSyncWithoutWarningIfAlreadyRendering,
32 + unbatchedUpdates,
33 getPublicRootInstance,
34 findHostInstance,
35 findHostInstanceWithWarning,
@@ -174,7 +174,7 @@ function legacyRenderSubtreeIntoContainer(
174 };
175 }
176 // Initial mount should not be batched.
177 - flushSyncWithoutWarningIfAlreadyRendering(() => {
177 + unbatchedUpdates(() => {
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 - flushSyncWithoutWarningIfAlreadyRendering(() => {
360 + unbatchedUpdates(() => {
361 legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
362 // $FlowFixMe This should probably use `delete container._reactRootContainer`
363 container._reactRootContainer = null;
packages/react-dom/src/events/ReactDOMUpdateBatching.js
+4 -4
@@ -23,7 +23,7 @@ let batchedUpdatesImpl = function(fn, bookkeeping) {
23 let discreteUpdatesImpl = function(fn, a, b, c, d) {
24 return fn(a, b, c, d);
25 };
26 -let flushSyncImpl = function() {};
26 +let flushDiscreteUpdatesImpl = function() {};
27
28 let isInsideEventHandler = false;
29
@@ -39,7 +39,7 @@ function finishEventHandler() {
39 // bails out of the update without touching the DOM.
40 // TODO: Restore state in the microtask, after the discrete updates flush,
41 // instead of early flushing them here.
42 - flushSyncImpl();
42 + flushDiscreteUpdatesImpl();
43 restoreStateIfNeeded();
44 }
45 }
@@ -67,9 +67,9 @@ export function discreteUpdates(fn, a, b, c, d) {
67 export function setBatchingImplementation(
68 _batchedUpdatesImpl,
69 _discreteUpdatesImpl,
70 - _flushSyncImpl,
70 + _flushDiscreteUpdatesImpl,
71 ) {
72 batchedUpdatesImpl = _batchedUpdatesImpl;
73 discreteUpdatesImpl = _discreteUpdatesImpl;
74 - flushSyncImpl = _flushSyncImpl;
74 + flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
75 }
packages/react-noop-renderer/src/ReactNoop.js
+2
@@ -38,8 +38,10 @@ export const {
38 flushExpired,
39 batchedUpdates,
40 deferredUpdates,
41 + unbatchedUpdates,
42 discreteUpdates,
43 idleUpdates,
44 + flushDiscreteUpdates,
45 flushSync,
46 flushPassiveEffects,
47 act,
packages/react-noop-renderer/src/ReactNoopPersistent.js
+1
@@ -38,6 +38,7 @@ export const {
38 flushExpired,
39 batchedUpdates,
40 deferredUpdates,
41 + unbatchedUpdates,
42 discreteUpdates,
43 idleUpdates,
44 flushDiscreteUpdates,
packages/react-noop-renderer/src/createReactNoop.js
+4
@@ -901,6 +901,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
901
902 deferredUpdates: NoopRenderer.deferredUpdates,
903
904 + unbatchedUpdates: NoopRenderer.unbatchedUpdates,
905 +
906 discreteUpdates: NoopRenderer.discreteUpdates,
907
908 idleUpdates<T>(fn: () => T): T {
@@ -913,6 +915,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
915 }
916 },
917
918 + flushDiscreteUpdates: NoopRenderer.flushDiscreteUpdates,
919 +
920 flushSync(fn: () => mixed) {
921 NoopRenderer.flushSync(fn);
922 },
packages/react-reconciler/src/ReactFiberReconciler.js
+10 -5
@@ -18,11 +18,12 @@ import {
18 createContainer as createContainer_old,
19 updateContainer as updateContainer_old,
20 batchedUpdates as batchedUpdates_old,
21 + unbatchedUpdates as unbatchedUpdates_old,
22 deferredUpdates as deferredUpdates_old,
23 discreteUpdates as discreteUpdates_old,
24 + flushDiscreteUpdates as flushDiscreteUpdates_old,
25 flushControlled as flushControlled_old,
26 flushSync as flushSync_old,
25 - flushSyncWithoutWarningIfAlreadyRendering as flushSyncWithoutWarningIfAlreadyRendering_old,
27 flushPassiveEffects as flushPassiveEffects_old,
28 getPublicRootInstance as getPublicRootInstance_old,
29 attemptSynchronousHydration as attemptSynchronousHydration_old,
@@ -55,11 +56,12 @@ import {
56 createContainer as createContainer_new,
57 updateContainer as updateContainer_new,
58 batchedUpdates as batchedUpdates_new,
59 + unbatchedUpdates as unbatchedUpdates_new,
60 deferredUpdates as deferredUpdates_new,
61 discreteUpdates as discreteUpdates_new,
62 + flushDiscreteUpdates as flushDiscreteUpdates_new,
63 flushControlled as flushControlled_new,
64 flushSync as flushSync_new,
62 - flushSyncWithoutWarningIfAlreadyRendering as flushSyncWithoutWarningIfAlreadyRendering_new,
65 flushPassiveEffects as flushPassiveEffects_new,
66 getPublicRootInstance as getPublicRootInstance_new,
67 attemptSynchronousHydration as attemptSynchronousHydration_new,
@@ -97,19 +99,22 @@ export const updateContainer = enableNewReconciler
99 export const batchedUpdates = enableNewReconciler
100 ? batchedUpdates_new
101 : batchedUpdates_old;
102 +export const unbatchedUpdates = enableNewReconciler
103 + ? unbatchedUpdates_new
104 + : unbatchedUpdates_old;
105 export const deferredUpdates = enableNewReconciler
106 ? deferredUpdates_new
107 : deferredUpdates_old;
108 export const discreteUpdates = enableNewReconciler
109 ? discreteUpdates_new
110 : discreteUpdates_old;
111 +export const flushDiscreteUpdates = enableNewReconciler
112 + ? flushDiscreteUpdates_new
113 + : flushDiscreteUpdates_old;
114 export const flushControlled = enableNewReconciler
115 ? flushControlled_new
116 : flushControlled_old;
117 export const flushSync = enableNewReconciler ? flushSync_new : flushSync_old;
110 -export const flushSyncWithoutWarningIfAlreadyRendering = enableNewReconciler
111 - ? flushSyncWithoutWarningIfAlreadyRendering_new
112 - : flushSyncWithoutWarningIfAlreadyRendering_old;
118 export const flushPassiveEffects = enableNewReconciler
119 ? flushPassiveEffects_new
120 : flushPassiveEffects_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
+4 -2
@@ -52,11 +52,12 @@ import {
52 scheduleUpdateOnFiber,
53 flushRoot,
54 batchedUpdates,
55 + unbatchedUpdates,
56 flushSync,
57 flushControlled,
58 deferredUpdates,
59 discreteUpdates,
59 - flushSyncWithoutWarningIfAlreadyRendering,
60 + flushDiscreteUpdates,
61 flushPassiveEffects,
62 } from './ReactFiberWorkLoop.new';
63 import {
@@ -326,11 +327,12 @@ export function updateContainer(
327
328 export {
329 batchedUpdates,
330 + unbatchedUpdates,
331 deferredUpdates,
332 discreteUpdates,
333 + flushDiscreteUpdates,
334 flushControlled,
335 flushSync,
333 - flushSyncWithoutWarningIfAlreadyRendering,
336 flushPassiveEffects,
337 };
338
packages/react-reconciler/src/ReactFiberReconciler.old.js
+4 -2
@@ -52,11 +52,12 @@ import {
52 scheduleUpdateOnFiber,
53 flushRoot,
54 batchedUpdates,
55 + unbatchedUpdates,
56 flushSync,
57 flushControlled,
58 deferredUpdates,
59 discreteUpdates,
59 - flushSyncWithoutWarningIfAlreadyRendering,
60 + flushDiscreteUpdates,
61 flushPassiveEffects,
62 } from './ReactFiberWorkLoop.old';
63 import {
@@ -326,11 +327,12 @@ export function updateContainer(
327
328 export {
329 batchedUpdates,
330 + unbatchedUpdates,
331 deferredUpdates,
332 discreteUpdates,
333 + flushDiscreteUpdates,
334 flushControlled,
335 flushSync,
333 - flushSyncWithoutWarningIfAlreadyRendering,
336 flushPassiveEffects,
337 };
338
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+109 -35
@@ -246,11 +246,12 @@ const {
246
247 type ExecutionContext = number;
248
249 -export const NoContext = /* */ 0b0000;
250 -const BatchedContext = /* */ 0b0001;
251 -const RenderContext = /* */ 0b0010;
252 -const CommitContext = /* */ 0b0100;
253 -export const RetryAfterError = /* */ 0b1000;
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;
255
256 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
257 const RootIncomplete = 0;
@@ -514,19 +515,35 @@ export function scheduleUpdateOnFiber(
515 }
516 }
517
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();
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);
547 }
548
549 return root;
@@ -1027,6 +1044,34 @@ export function getExecutionContext(): ExecutionContext {
1044 return executionContext;
1045 }
1046
1047 +export function flushDiscreteUpdates() {
1048 + // TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
1049 + // However, `act` uses `batchedUpdates`, so there's no way to distinguish
1050 + // those two cases. Need to fix this before exposing flushDiscreteUpdates
1051 + // as a public API.
1052 + if (
1053 + (executionContext & (BatchedContext | RenderContext | CommitContext)) !==
1054 + NoContext
1055 + ) {
1056 + if (__DEV__) {
1057 + if ((executionContext & RenderContext) !== NoContext) {
1058 + console.error(
1059 + 'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
1060 + 'already rendering.',
1061 + );
1062 + }
1063 + }
1064 + // We're already rendering, so we can't synchronously flush pending work.
1065 + // This is probably a nested event dispatch triggered by a lifecycle/effect,
1066 + // like `el.focus()`. Exit.
1067 + return;
1068 + }
1069 + flushSyncCallbacks();
1070 + // If the discrete updates scheduled passive effects, flush them now so that
1071 + // they fire before the next serial event.
1072 + flushPassiveEffects();
1073 +}
1074 +
1075 export function deferredUpdates<A>(fn: () => A): A {
1076 const previousPriority = getCurrentUpdatePriority();
1077 const prevTransition = ReactCurrentBatchConfig.transition;
@@ -1078,10 +1123,26 @@ export function discreteUpdates<A, B, C, D, R>(
1123 }
1124 }
1125
1081 -export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1082 - fn: A => R,
1083 - a: A,
1084 -): R {
1126 +export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1127 + const prevExecutionContext = executionContext;
1128 + executionContext &= ~BatchedContext;
1129 + executionContext |= LegacyUnbatchedContext;
1130 + try {
1131 + return fn(a);
1132 + } finally {
1133 + executionContext = prevExecutionContext;
1134 + // If there were legacy sync updates, flush them at the end of the outer
1135 + // most batchedUpdates-like method.
1136 + if (executionContext === NoContext) {
1137 + resetRenderTimer();
1138 + // TODO: I think this call is redundant, because we flush inside
1139 + // scheduleUpdateOnFiber when LegacyUnbatchedContext is set.
1140 + flushSyncCallbacksOnlyInLegacyMode();
1141 + }
1142 + }
1143 +}
1144 +
1145 +export function flushSync<A, R>(fn: A => R, a: A): R {
1146 const prevExecutionContext = executionContext;
1147 executionContext |= BatchedContext;
1148
@@ -1104,23 +1165,18 @@ export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1165 // the stack.
1166 if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1167 flushSyncCallbacks();
1168 + } else {
1169 + if (__DEV__) {
1170 + console.error(
1171 + 'flushSync was called from inside a lifecycle method. React cannot ' +
1172 + 'flush when React is already rendering. Consider moving this call to ' +
1173 + 'a scheduler task or micro task.',
1174 + );
1175 + }
1176 }
1177 }
1178 }
1179
1111 -export function flushSync<A, R>(fn: A => R, a: A): R {
1112 - if (__DEV__) {
1113 - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
1114 - console.error(
1115 - 'flushSync was called from inside a lifecycle method. React cannot ' +
1116 - 'flush when React is already rendering. Consider moving this call to ' +
1117 - 'a scheduler task or micro task.',
1118 - );
1119 - }
1120 - }
1121 - return flushSyncWithoutWarningIfAlreadyRendering(fn, a);
1122 -}
1123 -
1180 export function flushControlled(fn: () => mixed): void {
1181 const prevExecutionContext = executionContext;
1182 executionContext |= BatchedContext;
@@ -1918,6 +1974,24 @@ function commitRootImpl(root, renderPriorityLevel) {
1974 throw error;
1975 }
1976
1977 + if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
1978 + if (__DEV__) {
1979 + if (enableDebugTracing) {
1980 + logCommitStopped();
1981 + }
1982 + }
1983 +
1984 + if (enableSchedulingProfiler) {
1985 + markCommitStopped();
1986 + }
1987 +
1988 + // This is a legacy edge case. We just committed the initial mount of
1989 + // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
1990 + // synchronously, but layout updates should be deferred until the end
1991 + // of the batch.
1992 + return null;
1993 + }
1994 +
1995 // If the passive effects are the result of a discrete render, flush them
1996 // synchronously at the end of the current task so that the result is
1997 // immediately observable. Otherwise, we assume that they are not
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+109 -35
@@ -246,11 +246,12 @@ const {
246
247 type ExecutionContext = number;
248
249 -export const NoContext = /* */ 0b0000;
250 -const BatchedContext = /* */ 0b0001;
251 -const RenderContext = /* */ 0b0010;
252 -const CommitContext = /* */ 0b0100;
253 -export const RetryAfterError = /* */ 0b1000;
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;
255
256 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
257 const RootIncomplete = 0;
@@ -514,19 +515,35 @@ export function scheduleUpdateOnFiber(
515 }
516 }
517
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();
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);
547 }
548
549 return root;
@@ -1027,6 +1044,34 @@ export function getExecutionContext(): ExecutionContext {
1044 return executionContext;
1045 }
1046
1047 +export function flushDiscreteUpdates() {
1048 + // TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
1049 + // However, `act` uses `batchedUpdates`, so there's no way to distinguish
1050 + // those two cases. Need to fix this before exposing flushDiscreteUpdates
1051 + // as a public API.
1052 + if (
1053 + (executionContext & (BatchedContext | RenderContext | CommitContext)) !==
1054 + NoContext
1055 + ) {
1056 + if (__DEV__) {
1057 + if ((executionContext & RenderContext) !== NoContext) {
1058 + console.error(
1059 + 'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
1060 + 'already rendering.',
1061 + );
1062 + }
1063 + }
1064 + // We're already rendering, so we can't synchronously flush pending work.
1065 + // This is probably a nested event dispatch triggered by a lifecycle/effect,
1066 + // like `el.focus()`. Exit.
1067 + return;
1068 + }
1069 + flushSyncCallbacks();
1070 + // If the discrete updates scheduled passive effects, flush them now so that
1071 + // they fire before the next serial event.
1072 + flushPassiveEffects();
1073 +}
1074 +
1075 export function deferredUpdates<A>(fn: () => A): A {
1076 const previousPriority = getCurrentUpdatePriority();
1077 const prevTransition = ReactCurrentBatchConfig.transition;
@@ -1078,10 +1123,26 @@ export function discreteUpdates<A, B, C, D, R>(
1123 }
1124 }
1125
1081 -export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1082 - fn: A => R,
1083 - a: A,
1084 -): R {
1126 +export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1127 + const prevExecutionContext = executionContext;
1128 + executionContext &= ~BatchedContext;
1129 + executionContext |= LegacyUnbatchedContext;
1130 + try {
1131 + return fn(a);
1132 + } finally {
1133 + executionContext = prevExecutionContext;
1134 + // If there were legacy sync updates, flush them at the end of the outer
1135 + // most batchedUpdates-like method.
1136 + if (executionContext === NoContext) {
1137 + resetRenderTimer();
1138 + // TODO: I think this call is redundant, because we flush inside
1139 + // scheduleUpdateOnFiber when LegacyUnbatchedContext is set.
1140 + flushSyncCallbacksOnlyInLegacyMode();
1141 + }
1142 + }
1143 +}
1144 +
1145 +export function flushSync<A, R>(fn: A => R, a: A): R {
1146 const prevExecutionContext = executionContext;
1147 executionContext |= BatchedContext;
1148
@@ -1104,23 +1165,18 @@ export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1165 // the stack.
1166 if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1167 flushSyncCallbacks();
1168 + } else {
1169 + if (__DEV__) {
1170 + console.error(
1171 + 'flushSync was called from inside a lifecycle method. React cannot ' +
1172 + 'flush when React is already rendering. Consider moving this call to ' +
1173 + 'a scheduler task or micro task.',
1174 + );
1175 + }
1176 }
1177 }
1178 }
1179
1111 -export function flushSync<A, R>(fn: A => R, a: A): R {
1112 - if (__DEV__) {
1113 - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
1114 - console.error(
1115 - 'flushSync was called from inside a lifecycle method. React cannot ' +
1116 - 'flush when React is already rendering. Consider moving this call to ' +
1117 - 'a scheduler task or micro task.',
1118 - );
1119 - }
1120 - }
1121 - return flushSyncWithoutWarningIfAlreadyRendering(fn, a);
1122 -}
1123 -
1180 export function flushControlled(fn: () => mixed): void {
1181 const prevExecutionContext = executionContext;
1182 executionContext |= BatchedContext;
@@ -1918,6 +1974,24 @@ function commitRootImpl(root, renderPriorityLevel) {
1974 throw error;
1975 }
1976
1977 + if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
1978 + if (__DEV__) {
1979 + if (enableDebugTracing) {
1980 + logCommitStopped();
1981 + }
1982 + }
1983 +
1984 + if (enableSchedulingProfiler) {
1985 + markCommitStopped();
1986 + }
1987 +
1988 + // This is a legacy edge case. We just committed the initial mount of
1989 + // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
1990 + // synchronously, but layout updates should be deferred until the end
1991 + // of the batch.
1992 + return null;
1993 + }
1994 +
1995 // If the passive effects are the result of a discrete render, flush them
1996 // synchronously at the end of the current task so that the result is
1997 // immediately observable. Otherwise, we assume that they are not
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
-23
@@ -173,27 +173,4 @@ describe('ReactFlushSync', () => {
173 // Effect flushes after paint.
174 expect(Scheduler).toHaveYielded(['Effect']);
175 });
176 -
177 - test('does not flush pending passive effects', async () => {
178 - function App() {
179 - useEffect(() => {
180 - Scheduler.unstable_yieldValue('Effect');
181 - }, []);
182 - return <Text text="Child" />;
183 - }
184 -
185 - const root = ReactNoop.createRoot();
186 - await act(async () => {
187 - root.render(<App />);
188 - expect(Scheduler).toFlushUntilNextPaint(['Child']);
189 - expect(root).toMatchRenderedOutput('Child');
190 -
191 - // Passive effects are pending. Calling flushSync should not affect them.
192 - ReactNoop.flushSync();
193 - // Effects still haven't fired.
194 - expect(Scheduler).toHaveYielded([]);
195 - });
196 - // Now the effects have fired.
197 - expect(Scheduler).toHaveYielded(['Effect']);
198 - });
176 });
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+59
@@ -349,4 +349,63 @@ 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 + });
411 });