Skip to content

Commit 950a618

Browse files
author
Brian Vaughn
committed
Account for another DevTools + Fast Refresh edge case
DevTools now 'untrack' Fibers (cleans up the ID-to-Fiber mapping) after a slight delay in order to support a Fast Refresh edge case: 1. Component type is updated and Fast Refresh schedules an update+remount. 2. flushPendingErrorsAndWarningsAfterDelay() runs, sees the old Fiber is no longer mounted (it's been disconnected by Fast Refresh), and calls untrackFiberID() to clear it from the Map. 3. React flushes pending passive effects before it runs the next render, which logs an error or warning, which causes a new ID to be generated for this Fiber. 4. DevTools now tries to unmount the old Component with the new ID. The underlying problem here is the premature clearing of the Fiber ID, but DevTools has no way to detect that a given Fiber has been scheduled for Fast Refresh. (The '_debugNeedsRemount' flag won't necessarily be set.) The best we can do is to delay untracking by a small amount, and give React time to process the Fast Refresh delay.
1 parent 343776f commit 950a618

File tree

2 files changed

+88
-17
lines changed

2 files changed

+88
-17
lines changed

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

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ export function attach(
717717
? getFiberIDUnsafe(parentFiber) || '<no-id>'
718718
: '';
719719

720-
console.log(
720+
console.groupCollapsed(
721721
`[renderer] %c${name} %c${displayName} (${maybeID}) %c${
722722
parentFiber ? `${parentDisplayName} (${maybeParentID})` : ''
723723
} %c${extraString}`,
@@ -726,6 +726,13 @@ export function attach(
726726
'color: purple;',
727727
'color: black;',
728728
);
729+
console.log(
730+
new Error().stack
731+
.split('\n')
732+
.slice(1)
733+
.join('\n'),
734+
);
735+
console.groupEnd();
729736
}
730737
};
731738

@@ -996,7 +1003,9 @@ export function attach(
9961003
}
9971004
}
9981005

1006+
let didGenerateID = false;
9991007
if (id === null) {
1008+
didGenerateID = true;
10001009
id = getUID();
10011010
}
10021011

@@ -1019,6 +1028,17 @@ export function attach(
10191028
}
10201029
}
10211030

1031+
if (__DEBUG__) {
1032+
if (didGenerateID) {
1033+
debug(
1034+
'getOrGenerateFiberID()',
1035+
fiber,
1036+
fiber.return,
1037+
'Generated a new UID',
1038+
);
1039+
}
1040+
}
1041+
10221042
return refinedID;
10231043
}
10241044

@@ -1050,17 +1070,64 @@ export function attach(
10501070
// Removes a Fiber (and its alternate) from the Maps used to track their id.
10511071
// This method should always be called when a Fiber is unmounting.
10521072
function untrackFiberID(fiber: Fiber) {
1053-
const fiberID = getFiberIDUnsafe(fiber);
1054-
if (fiberID !== null) {
1055-
idToArbitraryFiberMap.delete(fiberID);
1073+
if (__DEBUG__) {
1074+
debug('untrackFiberID()', fiber, fiber.return, 'schedule after delay');
10561075
}
10571076

1058-
fiberToIDMap.delete(fiber);
1077+
// Untrack Fibers after a slight delay in order to support a Fast Refresh edge case:
1078+
// 1. Component type is updated and Fast Refresh schedules an update+remount.
1079+
// 2. flushPendingErrorsAndWarningsAfterDelay() runs, sees the old Fiber is no longer mounted
1080+
// (it's been disconnected by Fast Refresh), and calls untrackFiberID() to clear it from the Map.
1081+
// 3. React flushes pending passive effects before it runs the next render,
1082+
// which logs an error or warning, which causes a new ID to be generated for this Fiber.
1083+
// 4. DevTools now tries to unmount the old Component with the new ID.
1084+
//
1085+
// The underlying problem here is the premature clearing of the Fiber ID,
1086+
// but DevTools has no way to detect that a given Fiber has been scheduled for Fast Refresh.
1087+
// (The "_debugNeedsRemount" flag won't necessarily be set.)
1088+
//
1089+
// The best we can do is to delay untracking by a small amount,
1090+
// and give React time to process the Fast Refresh delay.
10591091

1060-
const {alternate} = fiber;
1061-
if (alternate !== null) {
1062-
fiberToIDMap.delete(alternate);
1092+
untrackFibersSet.add(fiber);
1093+
1094+
if (untrackFibersTimeoutID === null) {
1095+
untrackFibersTimeoutID = setTimeout(untrackFibers, 1000);
1096+
}
1097+
}
1098+
1099+
const untrackFibersSet: Set<Fiber> = new Set();
1100+
let untrackFibersTimeoutID: TimeoutID | null = null;
1101+
1102+
function untrackFibers() {
1103+
if (untrackFibersTimeoutID !== null) {
1104+
clearTimeout(untrackFibersTimeoutID);
1105+
untrackFibersTimeoutID = null;
1106+
}
1107+
1108+
if (__DEBUG__) {
1109+
console.log(
1110+
'untrackFibers() after delay:',
1111+
Array.from(untrackFibersSet)
1112+
.map(getFiberIDUnsafe)
1113+
.join(','),
1114+
);
10631115
}
1116+
1117+
untrackFibersSet.forEach(fiber => {
1118+
const fiberID = getFiberIDUnsafe(fiber);
1119+
if (fiberID !== null) {
1120+
idToArbitraryFiberMap.delete(fiberID);
1121+
}
1122+
1123+
fiberToIDMap.delete(fiber);
1124+
1125+
const {alternate} = fiber;
1126+
if (alternate !== null) {
1127+
fiberToIDMap.delete(alternate);
1128+
}
1129+
});
1130+
untrackFibersSet.clear();
10641131
}
10651132

10661133
function getChangeDescription(
@@ -1607,13 +1674,13 @@ export function attach(
16071674
}
16081675

16091676
function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
1677+
const isRoot = fiber.tag === HostRoot;
1678+
const id = getOrGenerateFiberID(fiber);
1679+
16101680
if (__DEBUG__) {
16111681
debug('recordMount()', fiber, parentFiber);
16121682
}
16131683

1614-
const isRoot = fiber.tag === HostRoot;
1615-
const id = getOrGenerateFiberID(fiber);
1616-
16171684
const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
16181685
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
16191686

@@ -1745,6 +1812,9 @@ export function attach(
17451812
// This reduces the chance of stack overflow for wide trees (e.g. lists with many items).
17461813
let fiber: Fiber | null = firstChild;
17471814
while (fiber !== null) {
1815+
// Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling).
1816+
getOrGenerateFiberID(fiber);
1817+
17481818
if (__DEBUG__) {
17491819
debug('mountFiberRecursively()', fiber, parentFiber);
17501820
}
@@ -1758,9 +1828,6 @@ export function attach(
17581828
const shouldIncludeInTree = !shouldFilterFiber(fiber);
17591829
if (shouldIncludeInTree) {
17601830
recordMount(fiber, parentFiber);
1761-
} else {
1762-
// Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling).
1763-
getOrGenerateFiberID(fiber);
17641831
}
17651832

17661833
if (traceUpdatesEnabled) {
@@ -2005,12 +2072,12 @@ export function attach(
20052072
parentFiber: Fiber | null,
20062073
traceNearestHostComponentUpdate: boolean,
20072074
): boolean {
2075+
const id = getOrGenerateFiberID(nextFiber);
2076+
20082077
if (__DEBUG__) {
20092078
debug('updateFiberRecursively()', nextFiber, parentFiber);
20102079
}
20112080

2012-
const id = getOrGenerateFiberID(nextFiber);
2013-
20142081
if (traceUpdatesEnabled) {
20152082
const elementType = getElementTypeForFiber(nextFiber);
20162083
if (traceNearestHostComponentUpdate) {
@@ -2319,6 +2386,10 @@ export function attach(
23192386
const current = root.current;
23202387
const alternate = current.alternate;
23212388

2389+
// Flush any pending Fibers that we are untracking before processing the new commit.
2390+
// If we don't do this, we might end up double-deleting Fibers in some cases (like Legacy Suspense).
2391+
untrackFibers();
2392+
23222393
currentRootID = getOrGenerateFiberID(current);
23232394

23242395
// Before the traversals, remember to start tracking

packages/react-devtools/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* Updated `react` and `react-dom` API imports in preparation for upcoming stable release ([bvaughn](https://github.com/bvaughn) in [#21488](https://github.com/facebook/react/pull/21488))
1515

1616
#### Bugfix
17-
* Reload all roots after Fast Refresh force-remount (to avoid corrupted Store state) ([bvaughn](https://github.com/bvaughn) in [#21516](https://github.com/facebook/react/pull/21516))
17+
* Reload all roots after Fast Refresh force-remount (to avoid corrupted Store state) ([bvaughn](https://github.com/bvaughn) in [#21516](https://github.com/facebook/react/pull/21516) and [#21523](https://github.com/facebook/react/pull/21523))
1818
* Errors thrown by Store can be dismissed so DevTools remain usable in many cases ([bvaughn](https://github.com/bvaughn) in [#21520](https://github.com/facebook/react/pull/21520))
1919
* Fixed string concatenation problem when a `Symbol` was logged to `console.error` or `console.warn` ([bvaughn](https://github.com/bvaughn) in [#21521](https://github.com/facebook/react/pull/21521))
2020
* DevTools: Fixed version range NPM syntax

0 commit comments

Comments
 (0)