diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
index 2ab8751e3cbcb..b27231efdffc7 100644
--- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
@@ -40,7 +40,7 @@ Object {
6 => 1,
},
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 16,
"updaters": Array [
Object {
@@ -87,7 +87,7 @@ Object {
4 => 2,
},
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 15,
"updaters": Array [
Object {
@@ -186,7 +186,7 @@ Object {
6 => 1,
},
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 12,
"updaters": Array [
Object {
@@ -445,7 +445,7 @@ Object {
],
],
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 12,
"updaters": Array [
Object {
@@ -938,7 +938,7 @@ Object {
],
],
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 11,
"updaters": Array [
Object {
@@ -1597,7 +1597,7 @@ Object {
17 => 1,
},
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 24,
"updaters": Array [
Object {
@@ -1687,7 +1687,7 @@ Object {
"fiberActualDurations": Map {},
"fiberSelfDurations": Map {},
"passiveEffectDuration": 0,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 34,
"updaters": Array [
Object {
@@ -2223,7 +2223,7 @@ Object {
],
],
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 24,
"updaters": Array [
Object {
@@ -2310,7 +2310,7 @@ Object {
"fiberActualDurations": Array [],
"fiberSelfDurations": Array [],
"passiveEffectDuration": 0,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 34,
"updaters": Array [
Object {
@@ -2431,7 +2431,7 @@ Object {
2 => 0,
},
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 0,
"updaters": Array [
Object {
@@ -2506,7 +2506,7 @@ Object {
3 => 0,
},
"passiveEffectDuration": 0,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 0,
"updaters": Array [
Object {
@@ -2715,7 +2715,7 @@ Object {
],
],
"passiveEffectDuration": 0,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 0,
"updaters": Array [
Object {
@@ -3071,7 +3071,7 @@ Object {
7 => 0,
},
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 0,
"updaters": Array [
Object {
@@ -3515,7 +3515,7 @@ Object {
],
],
"passiveEffectDuration": null,
- "priorityLevel": "Immediate",
+ "priorityLevel": "Normal",
"timestamp": 0,
"updaters": Array [
Object {
diff --git a/packages/react-dom/src/__tests__/ReactMount-test.js b/packages/react-dom/src/__tests__/ReactMount-test.js
index 9571905edaf52..a214c32f58d06 100644
--- a/packages/react-dom/src/__tests__/ReactMount-test.js
+++ b/packages/react-dom/src/__tests__/ReactMount-test.js
@@ -277,7 +277,7 @@ describe('ReactMount', () => {
expect(calls).toBe(5);
});
- it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
+ it('initial mount is sync inside batchedUpdates, but task work is deferred until the end of the batch', () => {
const container1 = document.createElement('div');
const container2 = document.createElement('div');
@@ -302,12 +302,12 @@ describe('ReactMount', () => {
// Initial mount on another root. Should flush immediately.
ReactDOM.render(a, container2);
- // The earlier update also flushed, since flushSync flushes all pending
- // sync work across all roots.
- expect(container1.textContent).toEqual('2');
- // Layout updates are also flushed synchronously
- expect(container2.textContent).toEqual('a!');
+ // The update did not flush yet.
+ expect(container1.textContent).toEqual('1');
+ // The initial mount flushed, but not the update scheduled in cDM.
+ expect(container2.textContent).toEqual('a');
});
+ // All updates have flushed.
expect(container1.textContent).toEqual('2');
expect(container2.textContent).toEqual('a!');
});
diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js
index f03350cb29240..a62a7fb444c5b 100644
--- a/packages/react-dom/src/client/ReactDOMLegacy.js
+++ b/packages/react-dom/src/client/ReactDOMLegacy.js
@@ -29,7 +29,7 @@ import {
createContainer,
findHostInstanceWithNoPortals,
updateContainer,
- flushSyncWithoutWarningIfAlreadyRendering,
+ unbatchedUpdates,
getPublicRootInstance,
findHostInstance,
findHostInstanceWithWarning,
@@ -174,7 +174,7 @@ function legacyRenderSubtreeIntoContainer(
};
}
// Initial mount should not be batched.
- flushSyncWithoutWarningIfAlreadyRendering(() => {
+ unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
@@ -357,7 +357,7 @@ export function unmountComponentAtNode(container: Container) {
}
// Unmount should not be batched.
- flushSyncWithoutWarningIfAlreadyRendering(() => {
+ unbatchedUpdates(() => {
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
// $FlowFixMe This should probably use `delete container._reactRootContainer`
container._reactRootContainer = null;
diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js
index a225deaf961f1..5e5567f9e0947 100644
--- a/packages/react-noop-renderer/src/ReactNoop.js
+++ b/packages/react-noop-renderer/src/ReactNoop.js
@@ -38,6 +38,7 @@ export const {
flushExpired,
batchedUpdates,
deferredUpdates,
+ unbatchedUpdates,
discreteUpdates,
idleUpdates,
flushSync,
diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js
index 86bb87c065e48..97876990a9b57 100644
--- a/packages/react-noop-renderer/src/ReactNoopPersistent.js
+++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js
@@ -38,6 +38,7 @@ export const {
flushExpired,
batchedUpdates,
deferredUpdates,
+ unbatchedUpdates,
discreteUpdates,
idleUpdates,
flushDiscreteUpdates,
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 5ea510d73d6ea..eb7e74193341c 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -901,6 +901,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
deferredUpdates: NoopRenderer.deferredUpdates,
+ unbatchedUpdates: NoopRenderer.unbatchedUpdates,
+
discreteUpdates: NoopRenderer.discreteUpdates,
idleUpdates(fn: () => T): T {
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index d25783164e984..bf78e9e41c390 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -18,6 +18,7 @@ import {
createContainer as createContainer_old,
updateContainer as updateContainer_old,
batchedUpdates as batchedUpdates_old,
+ unbatchedUpdates as unbatchedUpdates_old,
deferredUpdates as deferredUpdates_old,
discreteUpdates as discreteUpdates_old,
flushControlled as flushControlled_old,
@@ -55,6 +56,7 @@ import {
createContainer as createContainer_new,
updateContainer as updateContainer_new,
batchedUpdates as batchedUpdates_new,
+ unbatchedUpdates as unbatchedUpdates_new,
deferredUpdates as deferredUpdates_new,
discreteUpdates as discreteUpdates_new,
flushControlled as flushControlled_new,
@@ -97,6 +99,9 @@ export const updateContainer = enableNewReconciler
export const batchedUpdates = enableNewReconciler
? batchedUpdates_new
: batchedUpdates_old;
+export const unbatchedUpdates = enableNewReconciler
+ ? unbatchedUpdates_new
+ : unbatchedUpdates_old;
export const deferredUpdates = enableNewReconciler
? deferredUpdates_new
: deferredUpdates_old;
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index 7ab995a5fcb02..c8b61182d5737 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -52,6 +52,7 @@ import {
scheduleUpdateOnFiber,
flushRoot,
batchedUpdates,
+ unbatchedUpdates,
flushSync,
flushControlled,
deferredUpdates,
@@ -326,6 +327,7 @@ export function updateContainer(
export {
batchedUpdates,
+ unbatchedUpdates,
deferredUpdates,
discreteUpdates,
flushControlled,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index da127689c7c6e..7e74bf6a1feea 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -52,6 +52,7 @@ import {
scheduleUpdateOnFiber,
flushRoot,
batchedUpdates,
+ unbatchedUpdates,
flushSync,
flushControlled,
deferredUpdates,
@@ -326,6 +327,7 @@ export function updateContainer(
export {
batchedUpdates,
+ unbatchedUpdates,
deferredUpdates,
discreteUpdates,
flushControlled,
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index aa36d6da831e7..c8e13fa3a5eb2 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -246,11 +246,12 @@ const {
type ExecutionContext = number;
-export const NoContext = /* */ 0b0000;
-const BatchedContext = /* */ 0b0001;
-const RenderContext = /* */ 0b0010;
-const CommitContext = /* */ 0b0100;
-export const RetryAfterError = /* */ 0b1000;
+export const NoContext = /* */ 0b00000;
+const BatchedContext = /* */ 0b00001;
+const LegacyUnbatchedContext = /* */ 0b00010;
+const RenderContext = /* */ 0b00100;
+const CommitContext = /* */ 0b01000;
+export const RetryAfterError = /* */ 0b10000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0;
@@ -514,19 +515,35 @@ export function scheduleUpdateOnFiber(
}
}
- ensureRootIsScheduled(root, eventTime);
- if (
- lane === SyncLane &&
- executionContext === NoContext &&
- (fiber.mode & ConcurrentMode) === NoMode
- ) {
- // Flush the synchronous work now, unless we're already working or inside
- // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
- // scheduleCallbackForFiber to preserve the ability to schedule a callback
- // without immediately flushing it. We only do this for user-initiated
- // updates, to preserve historical behavior of legacy mode.
- resetRenderTimer();
- flushSyncCallbacksOnlyInLegacyMode();
+ if (lane === SyncLane) {
+ if (
+ // Check if we're inside unbatchedUpdates
+ (executionContext & LegacyUnbatchedContext) !== NoContext &&
+ // Check if we're not already rendering
+ (executionContext & (RenderContext | CommitContext)) === NoContext
+ ) {
+ // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
+ // root inside of batchedUpdates should be synchronous, but layout updates
+ // should be deferred until the end of the batch.
+ performSyncWorkOnRoot(root);
+ } else {
+ ensureRootIsScheduled(root, eventTime);
+ if (
+ executionContext === NoContext &&
+ (fiber.mode & ConcurrentMode) === NoMode
+ ) {
+ // Flush the synchronous work now, unless we're already working or inside
+ // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
+ // scheduleCallbackForFiber to preserve the ability to schedule a callback
+ // without immediately flushing it. We only do this for user-initiated
+ // updates, to preserve historical behavior of legacy mode.
+ resetRenderTimer();
+ flushSyncCallbacksOnlyInLegacyMode();
+ }
+ }
+ } else {
+ // Schedule other updates after in case the callback is sync.
+ ensureRootIsScheduled(root, eventTime);
}
return root;
@@ -1078,6 +1095,25 @@ export function discreteUpdates(
}
}
+export function unbatchedUpdates(fn: (a: A) => R, a: A): R {
+ const prevExecutionContext = executionContext;
+ executionContext &= ~BatchedContext;
+ executionContext |= LegacyUnbatchedContext;
+ try {
+ return fn(a);
+ } finally {
+ executionContext = prevExecutionContext;
+ // If there were legacy sync updates, flush them at the end of the outer
+ // most batchedUpdates-like method.
+ if (executionContext === NoContext) {
+ resetRenderTimer();
+ // TODO: I think this call is redundant, because we flush inside
+ // scheduleUpdateOnFiber when LegacyUnbatchedContext is set.
+ flushSyncCallbacksOnlyInLegacyMode();
+ }
+ }
+}
+
export function flushSyncWithoutWarningIfAlreadyRendering(
fn: A => R,
a: A,
@@ -1918,6 +1954,24 @@ function commitRootImpl(root, renderPriorityLevel) {
throw error;
}
+ if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
+
+ if (enableSchedulingProfiler) {
+ markCommitStopped();
+ }
+
+ // This is a legacy edge case. We just committed the initial mount of
+ // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
+ // synchronously, but layout updates should be deferred until the end
+ // of the batch.
+ return null;
+ }
+
// If the passive effects are the result of a discrete render, flush them
// synchronously at the end of the current task so that the result is
// immediately observable. Otherwise, we assume that they are not
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 2fd46c7cda7ca..c4f51481eaf98 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -246,11 +246,12 @@ const {
type ExecutionContext = number;
-export const NoContext = /* */ 0b0000;
-const BatchedContext = /* */ 0b0001;
-const RenderContext = /* */ 0b0010;
-const CommitContext = /* */ 0b0100;
-export const RetryAfterError = /* */ 0b1000;
+export const NoContext = /* */ 0b00000;
+const BatchedContext = /* */ 0b00001;
+const LegacyUnbatchedContext = /* */ 0b00010;
+const RenderContext = /* */ 0b00100;
+const CommitContext = /* */ 0b01000;
+export const RetryAfterError = /* */ 0b10000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0;
@@ -514,19 +515,35 @@ export function scheduleUpdateOnFiber(
}
}
- ensureRootIsScheduled(root, eventTime);
- if (
- lane === SyncLane &&
- executionContext === NoContext &&
- (fiber.mode & ConcurrentMode) === NoMode
- ) {
- // Flush the synchronous work now, unless we're already working or inside
- // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
- // scheduleCallbackForFiber to preserve the ability to schedule a callback
- // without immediately flushing it. We only do this for user-initiated
- // updates, to preserve historical behavior of legacy mode.
- resetRenderTimer();
- flushSyncCallbacksOnlyInLegacyMode();
+ if (lane === SyncLane) {
+ if (
+ // Check if we're inside unbatchedUpdates
+ (executionContext & LegacyUnbatchedContext) !== NoContext &&
+ // Check if we're not already rendering
+ (executionContext & (RenderContext | CommitContext)) === NoContext
+ ) {
+ // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
+ // root inside of batchedUpdates should be synchronous, but layout updates
+ // should be deferred until the end of the batch.
+ performSyncWorkOnRoot(root);
+ } else {
+ ensureRootIsScheduled(root, eventTime);
+ if (
+ executionContext === NoContext &&
+ (fiber.mode & ConcurrentMode) === NoMode
+ ) {
+ // Flush the synchronous work now, unless we're already working or inside
+ // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
+ // scheduleCallbackForFiber to preserve the ability to schedule a callback
+ // without immediately flushing it. We only do this for user-initiated
+ // updates, to preserve historical behavior of legacy mode.
+ resetRenderTimer();
+ flushSyncCallbacksOnlyInLegacyMode();
+ }
+ }
+ } else {
+ // Schedule other updates after in case the callback is sync.
+ ensureRootIsScheduled(root, eventTime);
}
return root;
@@ -1078,6 +1095,25 @@ export function discreteUpdates(
}
}
+export function unbatchedUpdates(fn: (a: A) => R, a: A): R {
+ const prevExecutionContext = executionContext;
+ executionContext &= ~BatchedContext;
+ executionContext |= LegacyUnbatchedContext;
+ try {
+ return fn(a);
+ } finally {
+ executionContext = prevExecutionContext;
+ // If there were legacy sync updates, flush them at the end of the outer
+ // most batchedUpdates-like method.
+ if (executionContext === NoContext) {
+ resetRenderTimer();
+ // TODO: I think this call is redundant, because we flush inside
+ // scheduleUpdateOnFiber when LegacyUnbatchedContext is set.
+ flushSyncCallbacksOnlyInLegacyMode();
+ }
+ }
+}
+
export function flushSyncWithoutWarningIfAlreadyRendering(
fn: A => R,
a: A,
@@ -1918,6 +1954,24 @@ function commitRootImpl(root, renderPriorityLevel) {
throw error;
}
+ if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
+
+ if (enableSchedulingProfiler) {
+ markCommitStopped();
+ }
+
+ // This is a legacy edge case. We just committed the initial mount of
+ // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
+ // synchronously, but layout updates should be deferred until the end
+ // of the batch.
+ return null;
+ }
+
// If the passive effects are the result of a discrete render, flush them
// synchronously at the end of the current task so that the result is
// immediately observable. Otherwise, we assume that they are not
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
index 9c94900851475..b603b03c696f5 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
@@ -349,4 +349,63 @@ describe('ReactIncrementalScheduling', () => {
// The updates should all be flushed with Task priority
expect(ReactNoop).toMatchRenderedOutput();
});
+
+ it('can opt-out of batching using unbatchedUpdates', () => {
+ ReactNoop.flushSync(() => {
+ ReactNoop.render();
+ expect(ReactNoop.getChildren()).toEqual([]);
+ // Should not have flushed yet because we're still batching
+
+ // unbatchedUpdates reverses the effect of batchedUpdates, so sync
+ // updates are not batched
+ ReactNoop.unbatchedUpdates(() => {
+ ReactNoop.render();
+ expect(ReactNoop).toMatchRenderedOutput();
+ ReactNoop.render();
+ expect(ReactNoop).toMatchRenderedOutput();
+ });
+
+ ReactNoop.render();
+ expect(ReactNoop).toMatchRenderedOutput();
+ });
+ // Remaining update is now flushed
+ expect(ReactNoop).toMatchRenderedOutput();
+ });
+
+ it('nested updates are always deferred, even inside unbatchedUpdates', () => {
+ let instance;
+ class Foo extends React.Component {
+ state = {step: 0};
+ componentDidUpdate() {
+ Scheduler.unstable_yieldValue('componentDidUpdate: ' + this.state.step);
+ if (this.state.step === 1) {
+ ReactNoop.unbatchedUpdates(() => {
+ // This is a nested state update, so it should not be
+ // flushed synchronously, even though we wrapped it
+ // in unbatchedUpdates.
+ this.setState({step: 2});
+ });
+ expect(Scheduler).toHaveYielded([
+ 'render: 1',
+ 'componentDidUpdate: 1',
+ ]);
+ expect(ReactNoop).toMatchRenderedOutput();
+ }
+ }
+ render() {
+ Scheduler.unstable_yieldValue('render: ' + this.state.step);
+ instance = this;
+ return ;
+ }
+ }
+ ReactNoop.render();
+ expect(Scheduler).toFlushAndYield(['render: 0']);
+ expect(ReactNoop).toMatchRenderedOutput();
+
+ ReactNoop.flushSync(() => {
+ instance.setState({step: 1});
+ });
+ expect(Scheduler).toHaveYielded(['render: 2', 'componentDidUpdate: 2']);
+ expect(ReactNoop).toMatchRenderedOutput();
+ });
});