Skip to content

Commit 486b52e

Browse files
committed
Don't "schedule" discrete work if we're scheduling sync work
1 parent ac533fd commit 486b52e

File tree

5 files changed

+98
-77
lines changed

5 files changed

+98
-77
lines changed

packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let React;
1313

1414
let ReactDOM;
1515
let Scheduler;
16+
let act;
1617

1718
const setUntrackedInputValue = Object.getOwnPropertyDescriptor(
1819
HTMLInputElement.prototype,
@@ -27,6 +28,7 @@ describe('ReactDOMFiberAsync', () => {
2728
container = document.createElement('div');
2829
React = require('react');
2930
ReactDOM = require('react-dom');
31+
act = require('react-dom/test-utils').act;
3032
Scheduler = require('scheduler');
3133

3234
document.body.appendChild(container);
@@ -635,4 +637,48 @@ describe('ReactDOMFiberAsync', () => {
635637
expect(container.textContent).toEqual('ABC');
636638
});
637639
});
640+
641+
// @gate experimental
642+
it('unmounted roots should never clear newer root content from a container', () => {
643+
const ref = React.createRef();
644+
645+
function OldApp() {
646+
const [value, setValue] = React.useState('old');
647+
function hideOnClick() {
648+
// Schedule a discrete update.
649+
setValue('update');
650+
// Synchronously unmount this root.
651+
ReactDOM.flushSync(() => oldRoot.unmount());
652+
}
653+
return (
654+
<button onClick={hideOnClick} ref={ref}>
655+
{value}
656+
</button>
657+
);
658+
}
659+
660+
function NewApp() {
661+
return <button ref={ref}>new</button>;
662+
}
663+
664+
const oldRoot = ReactDOM.createRoot(container);
665+
act(() => {
666+
oldRoot.render(<OldApp />);
667+
});
668+
669+
// Invoke discrete event.
670+
ref.current.click();
671+
672+
// The root should now be unmounted.
673+
expect(container.textContent).toBe('');
674+
675+
// We can now render a new one.
676+
const newRoot = ReactDOM.createRoot(container);
677+
ReactDOM.flushSync(() => {
678+
newRoot.render(<NewApp />);
679+
});
680+
ref.current.click();
681+
682+
expect(container.textContent).toBe('new');
683+
});
638684
});

packages/react-reconciler/src/ReactFiberCompleteWork.new.js

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -686,22 +686,7 @@ function completeWork(
686686
// This handles the case of React rendering into a container with previous children.
687687
// It's also safe to do for updates too, because current.child would only be null
688688
// if the previous render was null (so the the container would already be empty).
689-
//
690-
// The additional root.hydrate check above is required for hydration in legacy mode with no fallback.
691-
//
692-
// The root container check below also avoids a potential legacy mode problem
693-
// where unmounting from a container then rendering into it again
694-
// can sometimes cause the container to be cleared after the new render.
695-
const containerInfo = fiberRoot.containerInfo;
696-
const legacyRootContainer =
697-
containerInfo == null ? null : containerInfo._reactRootContainer;
698-
if (
699-
legacyRootContainer == null ||
700-
legacyRootContainer._internalRoot == null ||
701-
legacyRootContainer._internalRoot === fiberRoot
702-
) {
703-
workInProgress.effectTag |= Snapshot;
704-
}
689+
workInProgress.effectTag |= Snapshot;
705690
}
706691
}
707692
updateHostContainer(workInProgress);

packages/react-reconciler/src/ReactFiberCompleteWork.old.js

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -684,22 +684,7 @@ function completeWork(
684684
// This handles the case of React rendering into a container with previous children.
685685
// It's also safe to do for updates too, because current.child would only be null
686686
// if the previous render was null (so the the container would already be empty).
687-
//
688-
// The additional root.hydrate check above is required for hydration in legacy mode with no fallback.
689-
//
690-
// The root container check below also avoids a potential legacy mode problem
691-
// where unmounting from a container then rendering into it again
692-
// can sometimes cause the container to be cleared after the new render.
693-
const containerInfo = fiberRoot.containerInfo;
694-
const legacyRootContainer =
695-
containerInfo == null ? null : containerInfo._reactRootContainer;
696-
if (
697-
legacyRootContainer == null ||
698-
legacyRootContainer._internalRoot == null ||
699-
legacyRootContainer._internalRoot === fiberRoot
700-
) {
701-
workInProgress.effectTag |= Snapshot;
702-
}
687+
workInProgress.effectTag |= Snapshot;
703688
}
704689
}
705690
updateHostContainer(workInProgress);

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -474,30 +474,31 @@ export function scheduleUpdateOnFiber(
474474
}
475475
}
476476
} else {
477-
ensureRootIsScheduled(root);
478-
schedulePendingInteractions(root, expirationTime);
479-
}
480-
481-
if (
482-
(executionContext & DiscreteEventContext) !== NoContext &&
483-
// Only updates at user-blocking priority or greater are considered
484-
// discrete, even inside a discrete event.
485-
(priorityLevel === UserBlockingSchedulerPriority ||
486-
priorityLevel === ImmediateSchedulerPriority)
487-
) {
488-
// This is the result of a discrete event. Track the lowest priority
489-
// discrete update per root so we can flush them early, if needed.
490-
if (rootsWithPendingDiscreteUpdates === null) {
491-
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
492-
} else {
493-
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
494-
if (
495-
lastDiscreteTime === undefined ||
496-
!isSameOrHigherPriority(expirationTime, lastDiscreteTime)
497-
) {
498-
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
477+
// Schedule a discrete update but only if it's not Sync.
478+
if (
479+
(executionContext & DiscreteEventContext) !== NoContext &&
480+
// Only updates at user-blocking priority or greater are considered
481+
// discrete, even inside a discrete event.
482+
(priorityLevel === UserBlockingSchedulerPriority ||
483+
priorityLevel === ImmediateSchedulerPriority)
484+
) {
485+
// This is the result of a discrete event. Track the lowest priority
486+
// discrete update per root so we can flush them early, if needed.
487+
if (rootsWithPendingDiscreteUpdates === null) {
488+
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
489+
} else {
490+
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
491+
if (
492+
lastDiscreteTime === undefined ||
493+
!isSameOrHigherPriority(expirationTime, lastDiscreteTime)
494+
) {
495+
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
496+
}
499497
}
500498
}
499+
// Schedule other updates after in case the callback is sync.
500+
ensureRootIsScheduled(root);
501+
schedulePendingInteractions(root, expirationTime);
501502
}
502503
}
503504

@@ -1155,9 +1156,9 @@ function flushPendingDiscreteUpdates() {
11551156
markRootExpiredAtTime(root, expirationTime);
11561157
ensureRootIsScheduled(root);
11571158
});
1158-
// Now flush the immediate queue.
1159-
flushSyncCallbackQueue();
11601159
}
1160+
// Now flush the immediate queue.
1161+
flushSyncCallbackQueue();
11611162
}
11621163

11631164
export function batchedUpdates<A, R>(fn: A => R, a: A): R {

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -456,27 +456,31 @@ export function scheduleUpdateOnFiber(
456456
}
457457
}
458458
} else {
459-
ensureRootIsScheduled(root);
460-
schedulePendingInteractions(root, expirationTime);
461-
}
462-
463-
if (
464-
(executionContext & DiscreteEventContext) !== NoContext &&
465-
// Only updates at user-blocking priority or greater are considered
466-
// discrete, even inside a discrete event.
467-
(priorityLevel === UserBlockingPriority ||
468-
priorityLevel === ImmediatePriority)
469-
) {
470-
// This is the result of a discrete event. Track the lowest priority
471-
// discrete update per root so we can flush them early, if needed.
472-
if (rootsWithPendingDiscreteUpdates === null) {
473-
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
474-
} else {
475-
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
476-
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
477-
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
459+
// Schedule a discrete update but only if it's not Sync.
460+
if (
461+
(executionContext & DiscreteEventContext) !== NoContext &&
462+
// Only updates at user-blocking priority or greater are considered
463+
// discrete, even inside a discrete event.
464+
(priorityLevel === UserBlockingPriority ||
465+
priorityLevel === ImmediatePriority)
466+
) {
467+
// This is the result of a discrete event. Track the lowest priority
468+
// discrete update per root so we can flush them early, if needed.
469+
if (rootsWithPendingDiscreteUpdates === null) {
470+
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
471+
} else {
472+
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
473+
if (
474+
lastDiscreteTime === undefined ||
475+
lastDiscreteTime > expirationTime
476+
) {
477+
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
478+
}
478479
}
479480
}
481+
// Schedule other updates after in case the callback is sync.
482+
ensureRootIsScheduled(root);
483+
schedulePendingInteractions(root, expirationTime);
480484
}
481485
}
482486

@@ -1080,9 +1084,9 @@ function flushPendingDiscreteUpdates() {
10801084
markRootExpiredAtTime(root, expirationTime);
10811085
ensureRootIsScheduled(root);
10821086
});
1083-
// Now flush the immediate queue.
1084-
flushSyncCallbackQueue();
10851087
}
1088+
// Now flush the immediate queue.
1089+
flushSyncCallbackQueue();
10861090
}
10871091

10881092
export function batchedUpdates<A, R>(fn: A => R, a: A): R {

0 commit comments

Comments
 (0)