Skip to content

Commit 68dbd84

Browse files
authored
[DevTools] Unmount by walking previous nodes no longer in the new tree (#30644)
This no longer uses the handleCommitFiberUnmount hook to track unmounts. Instead, we can just unmount the DevToolsInstances that we didn't reuse. This doesn't account for cleaning up instances that were unnecessarily created when they weren't in the tree. I have a separate follow up for that. This also removes the queuing of untracking. This was added in #21523 but I don't believe it has been needed for a while because the mentioned flushPendingErrorsAndWarningsAfterDelay hasn't called untrackFiberID for a while so the race condition doesn't exist. It's hard to tell though because from the issues there weren't really any repros submitted.
1 parent 8d74e8c commit 68dbd84

File tree

1 file changed

+51
-92
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+51
-92
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

+51-92
Original file line numberDiff line numberDiff line change
@@ -1405,81 +1405,37 @@ export function attach(
14051405

14061406
// Removes a Fiber (and its alternate) from the Maps used to track their id.
14071407
// This method should always be called when a Fiber is unmounting.
1408-
function untrackFiberID(fiber: Fiber) {
1408+
function untrackFiber(fiberInstance: FiberInstance) {
14091409
if (__DEBUG__) {
1410-
debug('untrackFiberID()', fiber, null, 'schedule after delay');
1410+
debug('untrackFiber()', fiberInstance.data, null);
14111411
}
14121412

1413-
// Untrack Fibers after a slight delay in order to support a Fast Refresh edge case:
1414-
// 1. Component type is updated and Fast Refresh schedules an update+remount.
1415-
// 2. flushPendingErrorsAndWarningsAfterDelay() runs, sees the old Fiber is no longer mounted
1416-
// (it's been disconnected by Fast Refresh), and calls untrackFiberID() to clear it from the Map.
1417-
// 3. React flushes pending passive effects before it runs the next render,
1418-
// which logs an error or warning, which causes a new ID to be generated for this Fiber.
1419-
// 4. DevTools now tries to unmount the old Component with the new ID.
1420-
//
1421-
// The underlying problem here is the premature clearing of the Fiber ID,
1422-
// but DevTools has no way to detect that a given Fiber has been scheduled for Fast Refresh.
1423-
// (The "_debugNeedsRemount" flag won't necessarily be set.)
1424-
//
1425-
// The best we can do is to delay untracking by a small amount,
1426-
// and give React time to process the Fast Refresh delay.
1427-
1428-
untrackFibersSet.add(fiber);
1413+
idToDevToolsInstanceMap.delete(fiberInstance.id);
14291414

1430-
// React may detach alternate pointers during unmount;
1431-
// Since our untracking code is async, we should explicily track the pending alternate here as well.
1432-
const alternate = fiber.alternate;
1433-
if (alternate !== null) {
1434-
untrackFibersSet.add(alternate);
1415+
// Also clear any errors/warnings associated with this fiber.
1416+
clearErrorsForElementID(fiberInstance.id);
1417+
clearWarningsForElementID(fiberInstance.id);
1418+
if (fiberInstance.flags & FORCE_ERROR) {
1419+
fiberInstance.flags &= ~FORCE_ERROR;
1420+
forceErrorCount--;
1421+
if (forceErrorCount === 0 && setErrorHandler != null) {
1422+
setErrorHandler(shouldErrorFiberAlwaysNull);
1423+
}
14351424
}
1436-
1437-
if (untrackFibersTimeoutID === null) {
1438-
untrackFibersTimeoutID = setTimeout(untrackFibers, 1000);
1425+
if (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) {
1426+
fiberInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
1427+
forceFallbackCount--;
1428+
if (forceFallbackCount === 0 && setSuspenseHandler != null) {
1429+
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
1430+
}
14391431
}
1440-
}
1441-
1442-
const untrackFibersSet: Set<Fiber> = new Set();
1443-
let untrackFibersTimeoutID: TimeoutID | null = null;
14441432

1445-
function untrackFibers() {
1446-
if (untrackFibersTimeoutID !== null) {
1447-
clearTimeout(untrackFibersTimeoutID);
1448-
untrackFibersTimeoutID = null;
1433+
const fiber = fiberInstance.data;
1434+
fiberToFiberInstanceMap.delete(fiber);
1435+
const {alternate} = fiber;
1436+
if (alternate !== null) {
1437+
fiberToFiberInstanceMap.delete(alternate);
14491438
}
1450-
1451-
untrackFibersSet.forEach(fiber => {
1452-
const fiberInstance = fiberToFiberInstanceMap.get(fiber);
1453-
if (fiberInstance !== undefined) {
1454-
idToDevToolsInstanceMap.delete(fiberInstance.id);
1455-
1456-
// Also clear any errors/warnings associated with this fiber.
1457-
clearErrorsForElementID(fiberInstance.id);
1458-
clearWarningsForElementID(fiberInstance.id);
1459-
if (fiberInstance.flags & FORCE_ERROR) {
1460-
fiberInstance.flags &= ~FORCE_ERROR;
1461-
forceErrorCount--;
1462-
if (forceErrorCount === 0 && setErrorHandler != null) {
1463-
setErrorHandler(shouldErrorFiberAlwaysNull);
1464-
}
1465-
}
1466-
if (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) {
1467-
fiberInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
1468-
forceFallbackCount--;
1469-
if (forceFallbackCount === 0 && setSuspenseHandler != null) {
1470-
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
1471-
}
1472-
}
1473-
}
1474-
1475-
fiberToFiberInstanceMap.delete(fiber);
1476-
1477-
const {alternate} = fiber;
1478-
if (alternate !== null) {
1479-
fiberToFiberInstanceMap.delete(alternate);
1480-
}
1481-
});
1482-
untrackFibersSet.clear();
14831439
}
14841440

14851441
function getChangeDescription(
@@ -2054,7 +2010,7 @@ export function attach(
20542010
// Fill in the real unmounts in the reverse order.
20552011
// They were inserted parents-first by React, but we want children-first.
20562012
// So we traverse our array backwards.
2057-
for (let j = pendingRealUnmountedIDs.length - 1; j >= 0; j--) {
2013+
for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
20582014
operations[i++] = pendingRealUnmountedIDs[j];
20592015
}
20602016
// Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
@@ -2282,7 +2238,7 @@ export function attach(
22822238
}
22832239

22842240
if (!fiber._debugNeedsRemount) {
2285-
untrackFiberID(fiber);
2241+
untrackFiber(fiberInstance);
22862242

22872243
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
22882244
if (isProfilingSupported) {
@@ -2363,6 +2319,17 @@ export function attach(
23632319
instance.parent = null;
23642320
}
23652321

2322+
function unmountRemainingChildren() {
2323+
let child = remainingReconcilingChildren;
2324+
while (child !== null) {
2325+
if (child.kind === FIBER_INSTANCE) {
2326+
unmountFiberRecursively(child.data, false);
2327+
}
2328+
removeChild(child);
2329+
child = remainingReconcilingChildren;
2330+
}
2331+
}
2332+
23662333
function mountChildrenRecursively(
23672334
firstChild: Fiber,
23682335
traceNearestHostComponentUpdate: boolean,
@@ -2484,8 +2451,8 @@ export function attach(
24842451
}
24852452

24862453
// We use this to simulate unmounting for Suspense trees
2487-
// when we switch from primary to fallback.
2488-
function unmountFiberRecursively(fiber: Fiber) {
2454+
// when we switch from primary to fallback, or deleting a subtree.
2455+
function unmountFiberRecursively(fiber: Fiber, isSimulated: boolean) {
24892456
if (__DEBUG__) {
24902457
debug('unmountFiberRecursively()', fiber, null);
24912458
}
@@ -2526,7 +2493,7 @@ export function attach(
25262493
child = fallbackChildFragment ? fallbackChildFragment.child : null;
25272494
}
25282495

2529-
unmountChildrenRecursively(child);
2496+
unmountChildrenRecursively(child, isSimulated);
25302497
} finally {
25312498
if (shouldIncludeInTree) {
25322499
reconcilingParent = stashedParent;
@@ -2535,19 +2502,20 @@ export function attach(
25352502
}
25362503
}
25372504
if (fiberInstance !== null) {
2538-
recordUnmount(fiber, true);
2505+
recordUnmount(fiber, isSimulated);
25392506
removeChild(fiberInstance);
25402507
}
25412508
}
25422509

2543-
function unmountChildrenRecursively(firstChild: null | Fiber) {
2510+
function unmountChildrenRecursively(
2511+
firstChild: null | Fiber,
2512+
isSimulated: boolean,
2513+
) {
25442514
let child: null | Fiber = firstChild;
25452515
while (child !== null) {
25462516
// Record simulated unmounts children-first.
25472517
// We skip nodes without return because those are real unmounts.
2548-
if (child.return !== null) {
2549-
unmountFiberRecursively(child);
2550-
}
2518+
unmountFiberRecursively(child, isSimulated);
25512519
child = child.sibling;
25522520
}
25532521
}
@@ -2890,7 +2858,7 @@ export function attach(
28902858
// 1. Hide primary set
28912859
// This is not a real unmount, so it won't get reported by React.
28922860
// We need to manually walk the previous tree and record unmounts.
2893-
unmountChildrenRecursively(prevFiber.child);
2861+
unmountChildrenRecursively(prevFiber.child, true);
28942862
// 2. Mount fallback set
28952863
const nextFiberChild = nextFiber.child;
28962864
const nextFallbackChildSet = nextFiberChild
@@ -2922,6 +2890,7 @@ export function attach(
29222890
// All the remaining children will be children of this same fiber so we can just reuse them.
29232891
// I.e. we just restore them by undoing what we did above.
29242892
fiberInstance.firstChild = remainingReconcilingChildren;
2893+
remainingReconcilingChildren = null;
29252894
} else {
29262895
// If this fiber is filtered there might be changes to this set elsewhere so we have
29272896
// to visit each child to place it back in the set. We let the child bail out instead.
@@ -2984,6 +2953,7 @@ export function attach(
29842953
}
29852954
} finally {
29862955
if (shouldIncludeInTree) {
2956+
unmountRemainingChildren();
29872957
reconcilingParent = stashedParent;
29882958
previouslyReconciledSibling = stashedPrevious;
29892959
remainingReconcilingChildren = stashedRemaining;
@@ -3068,15 +3038,8 @@ export function attach(
30683038
}
30693039

30703040
function handleCommitFiberUnmount(fiber: any) {
3071-
// If the untrackFiberSet already has the unmounted Fiber, this means we've already
3072-
// recordedUnmount, so we don't need to do it again. If we don't do this, we might
3073-
// end up double-deleting Fibers in some cases (like Legacy Suspense).
3074-
if (!untrackFibersSet.has(fiber)) {
3075-
// This is not recursive.
3076-
// We can't traverse fibers after unmounting so instead
3077-
// we rely on React telling us about each unmount.
3078-
recordUnmount(fiber, false);
3079-
}
3041+
// This Hook is no longer used. After having shipped DevTools everywhere it is
3042+
// safe to stop calling it from Fiber.
30803043
}
30813044

30823045
function handlePostCommitFiberRoot(root: any) {
@@ -3097,10 +3060,6 @@ export function attach(
30973060
const current = root.current;
30983061
const alternate = current.alternate;
30993062

3100-
// Flush any pending Fibers that we are untracking before processing the new commit.
3101-
// If we don't do this, we might end up double-deleting Fibers in some cases (like Legacy Suspense).
3102-
untrackFibers();
3103-
31043063
currentRootID = getOrGenerateFiberInstance(current).id;
31053064

31063065
// Before the traversals, remember to start tracking
@@ -3158,7 +3117,7 @@ export function attach(
31583117
} else if (wasMounted && !isMounted) {
31593118
// Unmount an existing root.
31603119
removeRootPseudoKey(currentRootID);
3161-
recordUnmount(current, false);
3120+
unmountFiberRecursively(alternate, false);
31623121
}
31633122
} else {
31643123
// Mount a new root.

0 commit comments

Comments
 (0)