mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Bugfix: Flush legacy sync passive effects at beginning of event (#21846)
* Re-land recent flushSync changes Adds back #21776 and #21775, which were removed due to an internal e2e test failure. Will attempt to fix in subsequent commits. * Failing test: Legacy mode sync passive effects In concurrent roots, if a render is synchronous, we flush its passive effects synchronously. In legacy roots, we don't do this because all updates are synchronous — so we need to flush at the beginning of the next event. This is how `discreteUpdates` worked. * Flush legacy passive effects at beginning of event Fixes test added in previous commit.
This commit is contained in:
+14
-14
@@ -40,7 +40,7 @@ Object {
|
||||
6 => 1,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 16,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -87,7 +87,7 @@ Object {
|
||||
4 => 2,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 15,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -186,7 +186,7 @@ Object {
|
||||
6 => 1,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 12,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -445,7 +445,7 @@ Object {
|
||||
],
|
||||
],
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 12,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -938,7 +938,7 @@ Object {
|
||||
],
|
||||
],
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 11,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -1597,7 +1597,7 @@ Object {
|
||||
17 => 1,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 24,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -1687,7 +1687,7 @@ Object {
|
||||
"fiberActualDurations": Map {},
|
||||
"fiberSelfDurations": Map {},
|
||||
"passiveEffectDuration": 0,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 34,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -2223,7 +2223,7 @@ Object {
|
||||
],
|
||||
],
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 24,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -2310,7 +2310,7 @@ Object {
|
||||
"fiberActualDurations": Array [],
|
||||
"fiberSelfDurations": Array [],
|
||||
"passiveEffectDuration": 0,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 34,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -2431,7 +2431,7 @@ Object {
|
||||
2 => 0,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -2506,7 +2506,7 @@ Object {
|
||||
3 => 0,
|
||||
},
|
||||
"passiveEffectDuration": 0,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -2715,7 +2715,7 @@ Object {
|
||||
],
|
||||
],
|
||||
"passiveEffectDuration": 0,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -3071,7 +3071,7 @@ Object {
|
||||
7 => 0,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
@@ -3515,7 +3515,7 @@ Object {
|
||||
],
|
||||
],
|
||||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
"updaters": Array [
|
||||
Object {
|
||||
|
||||
+1
-7
@@ -1154,19 +1154,13 @@ describe('ReactDOMFiber', () => {
|
||||
expect(ops).toEqual(['A']);
|
||||
|
||||
if (__DEV__) {
|
||||
const errorCalls = console.error.calls.count();
|
||||
expect(console.error.calls.count()).toBe(2);
|
||||
expect(console.error.calls.argsFor(0)[0]).toMatch(
|
||||
'ReactDOM.render is no longer supported in React 18',
|
||||
);
|
||||
expect(console.error.calls.argsFor(1)[0]).toMatch(
|
||||
'ReactDOM.render is no longer supported in React 18',
|
||||
);
|
||||
// TODO: this warning shouldn't be firing in the first place if user didn't call it.
|
||||
for (let i = 2; i < errorCalls; i++) {
|
||||
expect(console.error.calls.argsFor(i)[0]).toMatch(
|
||||
'unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+6
-6
@@ -277,7 +277,7 @@ describe('ReactMount', () => {
|
||||
expect(calls).toBe(5);
|
||||
});
|
||||
|
||||
it('initial mount is sync inside batchedUpdates, but task work is deferred until the end of the batch', () => {
|
||||
it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
|
||||
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(<Foo>a</Foo>, container2);
|
||||
// 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');
|
||||
// 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!');
|
||||
});
|
||||
// All updates have flushed.
|
||||
expect(container1.textContent).toEqual('2');
|
||||
expect(container2.textContent).toEqual('a!');
|
||||
});
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@ import {createEventHandle} from './ReactDOMEventHandle';
|
||||
import {
|
||||
batchedUpdates,
|
||||
discreteUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushSync,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
flushControlled,
|
||||
injectIntoDevTools,
|
||||
attemptSynchronousHydration,
|
||||
@@ -100,7 +100,7 @@ setRestoreImplementation(restoreControlledState);
|
||||
setBatchingImplementation(
|
||||
batchedUpdates,
|
||||
discreteUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
);
|
||||
|
||||
function createPortal(
|
||||
|
||||
+3
-3
@@ -29,7 +29,7 @@ import {
|
||||
createContainer,
|
||||
findHostInstanceWithNoPortals,
|
||||
updateContainer,
|
||||
unbatchedUpdates,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
getPublicRootInstance,
|
||||
findHostInstance,
|
||||
findHostInstanceWithWarning,
|
||||
@@ -174,7 +174,7 @@ function legacyRenderSubtreeIntoContainer(
|
||||
};
|
||||
}
|
||||
// Initial mount should not be batched.
|
||||
unbatchedUpdates(() => {
|
||||
flushSyncWithoutWarningIfAlreadyRendering(() => {
|
||||
updateContainer(children, fiberRoot, parentComponent, callback);
|
||||
});
|
||||
} else {
|
||||
@@ -357,7 +357,7 @@ export function unmountComponentAtNode(container: Container) {
|
||||
}
|
||||
|
||||
// Unmount should not be batched.
|
||||
unbatchedUpdates(() => {
|
||||
flushSyncWithoutWarningIfAlreadyRendering(() => {
|
||||
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
|
||||
// $FlowFixMe This should probably use `delete container._reactRootContainer`
|
||||
container._reactRootContainer = null;
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ let batchedUpdatesImpl = function(fn, bookkeeping) {
|
||||
let discreteUpdatesImpl = function(fn, a, b, c, d) {
|
||||
return fn(a, b, c, d);
|
||||
};
|
||||
let flushDiscreteUpdatesImpl = function() {};
|
||||
let flushSyncImpl = function() {};
|
||||
|
||||
let isInsideEventHandler = false;
|
||||
|
||||
@@ -39,7 +39,7 @@ function finishEventHandler() {
|
||||
// bails out of the update without touching the DOM.
|
||||
// TODO: Restore state in the microtask, after the discrete updates flush,
|
||||
// instead of early flushing them here.
|
||||
flushDiscreteUpdatesImpl();
|
||||
flushSyncImpl();
|
||||
restoreStateIfNeeded();
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,9 @@ export function discreteUpdates(fn, a, b, c, d) {
|
||||
export function setBatchingImplementation(
|
||||
_batchedUpdatesImpl,
|
||||
_discreteUpdatesImpl,
|
||||
_flushDiscreteUpdatesImpl,
|
||||
_flushSyncImpl,
|
||||
) {
|
||||
batchedUpdatesImpl = _batchedUpdatesImpl;
|
||||
discreteUpdatesImpl = _discreteUpdatesImpl;
|
||||
flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
|
||||
flushSyncImpl = _flushSyncImpl;
|
||||
}
|
||||
|
||||
@@ -38,10 +38,8 @@ export const {
|
||||
flushExpired,
|
||||
batchedUpdates,
|
||||
deferredUpdates,
|
||||
unbatchedUpdates,
|
||||
discreteUpdates,
|
||||
idleUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushSync,
|
||||
flushPassiveEffects,
|
||||
act,
|
||||
|
||||
@@ -38,7 +38,6 @@ export const {
|
||||
flushExpired,
|
||||
batchedUpdates,
|
||||
deferredUpdates,
|
||||
unbatchedUpdates,
|
||||
discreteUpdates,
|
||||
idleUpdates,
|
||||
flushDiscreteUpdates,
|
||||
|
||||
@@ -901,8 +901,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
|
||||
deferredUpdates: NoopRenderer.deferredUpdates,
|
||||
|
||||
unbatchedUpdates: NoopRenderer.unbatchedUpdates,
|
||||
|
||||
discreteUpdates: NoopRenderer.discreteUpdates,
|
||||
|
||||
idleUpdates<T>(fn: () => T): T {
|
||||
@@ -915,8 +913,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
}
|
||||
},
|
||||
|
||||
flushDiscreteUpdates: NoopRenderer.flushDiscreteUpdates,
|
||||
|
||||
flushSync(fn: () => mixed) {
|
||||
NoopRenderer.flushSync(fn);
|
||||
},
|
||||
|
||||
+5
-10
@@ -18,12 +18,11 @@ 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,
|
||||
flushDiscreteUpdates as flushDiscreteUpdates_old,
|
||||
flushControlled as flushControlled_old,
|
||||
flushSync as flushSync_old,
|
||||
flushSyncWithoutWarningIfAlreadyRendering as flushSyncWithoutWarningIfAlreadyRendering_old,
|
||||
flushPassiveEffects as flushPassiveEffects_old,
|
||||
getPublicRootInstance as getPublicRootInstance_old,
|
||||
attemptSynchronousHydration as attemptSynchronousHydration_old,
|
||||
@@ -56,12 +55,11 @@ 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,
|
||||
flushDiscreteUpdates as flushDiscreteUpdates_new,
|
||||
flushControlled as flushControlled_new,
|
||||
flushSync as flushSync_new,
|
||||
flushSyncWithoutWarningIfAlreadyRendering as flushSyncWithoutWarningIfAlreadyRendering_new,
|
||||
flushPassiveEffects as flushPassiveEffects_new,
|
||||
getPublicRootInstance as getPublicRootInstance_new,
|
||||
attemptSynchronousHydration as attemptSynchronousHydration_new,
|
||||
@@ -99,22 +97,19 @@ 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;
|
||||
export const discreteUpdates = enableNewReconciler
|
||||
? discreteUpdates_new
|
||||
: discreteUpdates_old;
|
||||
export const flushDiscreteUpdates = enableNewReconciler
|
||||
? flushDiscreteUpdates_new
|
||||
: flushDiscreteUpdates_old;
|
||||
export const flushControlled = enableNewReconciler
|
||||
? flushControlled_new
|
||||
: flushControlled_old;
|
||||
export const flushSync = enableNewReconciler ? flushSync_new : flushSync_old;
|
||||
export const flushSyncWithoutWarningIfAlreadyRendering = enableNewReconciler
|
||||
? flushSyncWithoutWarningIfAlreadyRendering_new
|
||||
: flushSyncWithoutWarningIfAlreadyRendering_old;
|
||||
export const flushPassiveEffects = enableNewReconciler
|
||||
? flushPassiveEffects_new
|
||||
: flushPassiveEffects_old;
|
||||
|
||||
@@ -52,12 +52,11 @@ import {
|
||||
scheduleUpdateOnFiber,
|
||||
flushRoot,
|
||||
batchedUpdates,
|
||||
unbatchedUpdates,
|
||||
flushSync,
|
||||
flushControlled,
|
||||
deferredUpdates,
|
||||
discreteUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
flushPassiveEffects,
|
||||
} from './ReactFiberWorkLoop.new';
|
||||
import {
|
||||
@@ -327,12 +326,11 @@ export function updateContainer(
|
||||
|
||||
export {
|
||||
batchedUpdates,
|
||||
unbatchedUpdates,
|
||||
deferredUpdates,
|
||||
discreteUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushControlled,
|
||||
flushSync,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
flushPassiveEffects,
|
||||
};
|
||||
|
||||
|
||||
@@ -52,12 +52,11 @@ import {
|
||||
scheduleUpdateOnFiber,
|
||||
flushRoot,
|
||||
batchedUpdates,
|
||||
unbatchedUpdates,
|
||||
flushSync,
|
||||
flushControlled,
|
||||
deferredUpdates,
|
||||
discreteUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
flushPassiveEffects,
|
||||
} from './ReactFiberWorkLoop.old';
|
||||
import {
|
||||
@@ -327,12 +326,11 @@ export function updateContainer(
|
||||
|
||||
export {
|
||||
batchedUpdates,
|
||||
unbatchedUpdates,
|
||||
deferredUpdates,
|
||||
discreteUpdates,
|
||||
flushDiscreteUpdates,
|
||||
flushControlled,
|
||||
flushSync,
|
||||
flushSyncWithoutWarningIfAlreadyRendering,
|
||||
flushPassiveEffects,
|
||||
};
|
||||
|
||||
|
||||
@@ -246,12 +246,11 @@ const {
|
||||
|
||||
type ExecutionContext = number;
|
||||
|
||||
export const NoContext = /* */ 0b00000;
|
||||
const BatchedContext = /* */ 0b00001;
|
||||
const LegacyUnbatchedContext = /* */ 0b00010;
|
||||
const RenderContext = /* */ 0b00100;
|
||||
const CommitContext = /* */ 0b01000;
|
||||
export const RetryAfterError = /* */ 0b10000;
|
||||
export const NoContext = /* */ 0b0000;
|
||||
const BatchedContext = /* */ 0b0001;
|
||||
const RenderContext = /* */ 0b0010;
|
||||
const CommitContext = /* */ 0b0100;
|
||||
export const RetryAfterError = /* */ 0b1000;
|
||||
|
||||
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
|
||||
const RootIncomplete = 0;
|
||||
@@ -515,35 +514,19 @@ export function scheduleUpdateOnFiber(
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
return root;
|
||||
@@ -1044,34 +1027,6 @@ export function getExecutionContext(): ExecutionContext {
|
||||
return executionContext;
|
||||
}
|
||||
|
||||
export function flushDiscreteUpdates() {
|
||||
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
|
||||
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
|
||||
// those two cases. Need to fix this before exposing flushDiscreteUpdates
|
||||
// as a public API.
|
||||
if (
|
||||
(executionContext & (BatchedContext | RenderContext | CommitContext)) !==
|
||||
NoContext
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if ((executionContext & RenderContext) !== NoContext) {
|
||||
console.error(
|
||||
'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
|
||||
'already rendering.',
|
||||
);
|
||||
}
|
||||
}
|
||||
// We're already rendering, so we can't synchronously flush pending work.
|
||||
// This is probably a nested event dispatch triggered by a lifecycle/effect,
|
||||
// like `el.focus()`. Exit.
|
||||
return;
|
||||
}
|
||||
flushSyncCallbacks();
|
||||
// If the discrete updates scheduled passive effects, flush them now so that
|
||||
// they fire before the next serial event.
|
||||
flushPassiveEffects();
|
||||
}
|
||||
|
||||
export function deferredUpdates<A>(fn: () => A): A {
|
||||
const previousPriority = getCurrentUpdatePriority();
|
||||
const prevTransition = ReactCurrentBatchConfig.transition;
|
||||
@@ -1123,26 +1078,19 @@ export function discreteUpdates<A, B, C, D, R>(
|
||||
}
|
||||
}
|
||||
|
||||
export function unbatchedUpdates<A, R>(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<A, R>(
|
||||
fn: A => R,
|
||||
a: A,
|
||||
): R {
|
||||
// In legacy mode, we flush pending passive effects at the beginning of the
|
||||
// next event, not at the end of the previous one.
|
||||
if (
|
||||
rootWithPendingPassiveEffects !== null &&
|
||||
rootWithPendingPassiveEffects.tag === LegacyRoot
|
||||
) {
|
||||
flushPassiveEffects();
|
||||
}
|
||||
}
|
||||
|
||||
export function flushSync<A, R>(fn: A => R, a: A): R {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= BatchedContext;
|
||||
|
||||
@@ -1165,18 +1113,23 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
|
||||
// the stack.
|
||||
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
|
||||
flushSyncCallbacks();
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'flushSync was called from inside a lifecycle method. React cannot ' +
|
||||
'flush when React is already rendering. Consider moving this call to ' +
|
||||
'a scheduler task or micro task.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function flushSync<A, R>(fn: A => R, a: A): R {
|
||||
if (__DEV__) {
|
||||
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
|
||||
console.error(
|
||||
'flushSync was called from inside a lifecycle method. React cannot ' +
|
||||
'flush when React is already rendering. Consider moving this call to ' +
|
||||
'a scheduler task or micro task.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return flushSyncWithoutWarningIfAlreadyRendering(fn, a);
|
||||
}
|
||||
|
||||
export function flushControlled(fn: () => mixed): void {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= BatchedContext;
|
||||
@@ -1974,24 +1927,6 @@ 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
|
||||
|
||||
@@ -246,12 +246,11 @@ const {
|
||||
|
||||
type ExecutionContext = number;
|
||||
|
||||
export const NoContext = /* */ 0b00000;
|
||||
const BatchedContext = /* */ 0b00001;
|
||||
const LegacyUnbatchedContext = /* */ 0b00010;
|
||||
const RenderContext = /* */ 0b00100;
|
||||
const CommitContext = /* */ 0b01000;
|
||||
export const RetryAfterError = /* */ 0b10000;
|
||||
export const NoContext = /* */ 0b0000;
|
||||
const BatchedContext = /* */ 0b0001;
|
||||
const RenderContext = /* */ 0b0010;
|
||||
const CommitContext = /* */ 0b0100;
|
||||
export const RetryAfterError = /* */ 0b1000;
|
||||
|
||||
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
|
||||
const RootIncomplete = 0;
|
||||
@@ -515,35 +514,19 @@ export function scheduleUpdateOnFiber(
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
return root;
|
||||
@@ -1044,34 +1027,6 @@ export function getExecutionContext(): ExecutionContext {
|
||||
return executionContext;
|
||||
}
|
||||
|
||||
export function flushDiscreteUpdates() {
|
||||
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
|
||||
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
|
||||
// those two cases. Need to fix this before exposing flushDiscreteUpdates
|
||||
// as a public API.
|
||||
if (
|
||||
(executionContext & (BatchedContext | RenderContext | CommitContext)) !==
|
||||
NoContext
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if ((executionContext & RenderContext) !== NoContext) {
|
||||
console.error(
|
||||
'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
|
||||
'already rendering.',
|
||||
);
|
||||
}
|
||||
}
|
||||
// We're already rendering, so we can't synchronously flush pending work.
|
||||
// This is probably a nested event dispatch triggered by a lifecycle/effect,
|
||||
// like `el.focus()`. Exit.
|
||||
return;
|
||||
}
|
||||
flushSyncCallbacks();
|
||||
// If the discrete updates scheduled passive effects, flush them now so that
|
||||
// they fire before the next serial event.
|
||||
flushPassiveEffects();
|
||||
}
|
||||
|
||||
export function deferredUpdates<A>(fn: () => A): A {
|
||||
const previousPriority = getCurrentUpdatePriority();
|
||||
const prevTransition = ReactCurrentBatchConfig.transition;
|
||||
@@ -1123,26 +1078,19 @@ export function discreteUpdates<A, B, C, D, R>(
|
||||
}
|
||||
}
|
||||
|
||||
export function unbatchedUpdates<A, R>(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<A, R>(
|
||||
fn: A => R,
|
||||
a: A,
|
||||
): R {
|
||||
// In legacy mode, we flush pending passive effects at the beginning of the
|
||||
// next event, not at the end of the previous one.
|
||||
if (
|
||||
rootWithPendingPassiveEffects !== null &&
|
||||
rootWithPendingPassiveEffects.tag === LegacyRoot
|
||||
) {
|
||||
flushPassiveEffects();
|
||||
}
|
||||
}
|
||||
|
||||
export function flushSync<A, R>(fn: A => R, a: A): R {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= BatchedContext;
|
||||
|
||||
@@ -1165,18 +1113,23 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
|
||||
// the stack.
|
||||
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
|
||||
flushSyncCallbacks();
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'flushSync was called from inside a lifecycle method. React cannot ' +
|
||||
'flush when React is already rendering. Consider moving this call to ' +
|
||||
'a scheduler task or micro task.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function flushSync<A, R>(fn: A => R, a: A): R {
|
||||
if (__DEV__) {
|
||||
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
|
||||
console.error(
|
||||
'flushSync was called from inside a lifecycle method. React cannot ' +
|
||||
'flush when React is already rendering. Consider moving this call to ' +
|
||||
'a scheduler task or micro task.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return flushSyncWithoutWarningIfAlreadyRendering(fn, a);
|
||||
}
|
||||
|
||||
export function flushControlled(fn: () => mixed): void {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= BatchedContext;
|
||||
@@ -1974,24 +1927,6 @@ 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
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('ReactFlushSync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('do not flush passive effects synchronously in legacy mode', async () => {
|
||||
test('do not flush passive effects synchronously after render in legacy mode', async () => {
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Effect');
|
||||
@@ -152,6 +152,40 @@ describe('ReactFlushSync', () => {
|
||||
expect(Scheduler).toHaveYielded(['Effect']);
|
||||
});
|
||||
|
||||
test('flush pending passive effects before scope is called in legacy mode', async () => {
|
||||
let currentStep = 0;
|
||||
|
||||
function App({step}) {
|
||||
useEffect(() => {
|
||||
currentStep = step;
|
||||
Scheduler.unstable_yieldValue('Effect: ' + step);
|
||||
}, [step]);
|
||||
return <Text text={step} />;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createLegacyRoot();
|
||||
await act(async () => {
|
||||
ReactNoop.flushSync(() => {
|
||||
root.render(<App step={1} />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
1,
|
||||
// Because we're in legacy mode, we shouldn't have flushed the passive
|
||||
// effects yet.
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
|
||||
ReactNoop.flushSync(() => {
|
||||
// This should render step 2 because the passive effect has already
|
||||
// fired, before the scope function is called.
|
||||
root.render(<App step={currentStep + 1} />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Effect: 1', 2]);
|
||||
expect(root).toMatchRenderedOutput('2');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Effect: 2']);
|
||||
});
|
||||
|
||||
test("do not flush passive effects synchronously when they aren't the result of a sync render", async () => {
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
@@ -173,4 +207,27 @@ describe('ReactFlushSync', () => {
|
||||
// Effect flushes after paint.
|
||||
expect(Scheduler).toHaveYielded(['Effect']);
|
||||
});
|
||||
|
||||
test('does not flush pending passive effects', async () => {
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Effect');
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['Child']);
|
||||
expect(root).toMatchRenderedOutput('Child');
|
||||
|
||||
// Passive effects are pending. Calling flushSync should not affect them.
|
||||
ReactNoop.flushSync();
|
||||
// Effects still haven't fired.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
});
|
||||
// Now the effects have fired.
|
||||
expect(Scheduler).toHaveYielded(['Effect']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1795,10 +1795,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
return <Text text={'Count: ' + count} />;
|
||||
}
|
||||
await act(async () => {
|
||||
ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
|
||||
});
|
||||
|
||||
// Even in legacy mode, effects are deferred until after paint
|
||||
ReactNoop.flushSync();
|
||||
expect(Scheduler).toHaveYielded(['Count: (empty)']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
|
||||
});
|
||||
|
||||
@@ -349,63 +349,4 @@ describe('ReactIncrementalScheduling', () => {
|
||||
// The updates should all be flushed with Task priority
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
|
||||
});
|
||||
|
||||
it('can opt-out of batching using unbatchedUpdates', () => {
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.render(<span prop={0} />);
|
||||
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(<span prop={1} />);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
|
||||
ReactNoop.render(<span prop={2} />);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
|
||||
});
|
||||
|
||||
ReactNoop.render(<span prop={3} />);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
|
||||
});
|
||||
// Remaining update is now flushed
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
|
||||
});
|
||||
|
||||
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(<span prop={1} />);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
Scheduler.unstable_yieldValue('render: ' + this.state.step);
|
||||
instance = this;
|
||||
return <span prop={this.state.step} />;
|
||||
}
|
||||
}
|
||||
ReactNoop.render(<Foo />);
|
||||
expect(Scheduler).toFlushAndYield(['render: 0']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
|
||||
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState({step: 1});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['render: 2', 'componentDidUpdate: 2']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user