631
632
// Note that by calling these functions we may be creating the ID for the first time.
633
// If the Fiber is then never mounted, we are responsible for cleaning up after ourselves.
634
- // This is important because getPrimaryFiber() stores a Fiber in the primaryFibers Set.
635
- // If a Fiber never mounts, and we don't clean up after this code, we could leak.
634
+ // This is important because getOrGenerateFiberID() stores a Fiber in a couple of local Maps.
635
+ // If the Fiber never mounts and we don't clean up after this code, we could leak.
636
// Fortunately we would only leak Fibers that have errors/warnings associated with them,
637
// which is hopefully only a small set and only in DEV mode– but this is still not great.
638
// We should clean up Fibers like this when flushing; see recordPendingErrorsAndWarnings().
639
- const fiberID = getFiberID(getPrimaryFiber(fiber));
639
+ const fiberID = getOrGenerateFiberID(fiber);
640
+
641
+ if (__DEBUG__) {
642
+ debug('onErrorOrWarning', fiber, null, `${type}: "${message}"`);
643
+ }
644
645
// Mark this Fiber as needed its warning/error count updated during the next flush.
646
fibersWithChangedErrorOrWarningCounts.add(fiberID);
706
if (__DEBUG__) {
707
const displayName =
708
fiber.tag + ':' + (getDisplayNameForFiber(fiber) || 'null');
705
- const id = getFiberID(fiber);
709
+
710
+ const maybeID = getFiberIDUnsafe(fiber) || '<no id>';
711
const parentDisplayName = parentFiber
712
? parentFiber.tag +
713
':' +
714
(getDisplayNameForFiber(parentFiber) || 'null')
715
: '';
711
- const parentID = parentFiber ? getFiberID(parentFiber) : '';
712
- // NOTE: calling getFiberID or getPrimaryFiber is unsafe here
713
- // because it will put them in the map. For now, we'll omit them.
714
- // TODO: better debugging story for this.
716
+ const maybeParentID = parentFiber
717
+ ? getFiberIDUnsafe(parentFiber) || '<no-id>'
718
+ : '';
719
+
720
console.log(
716
- `[renderer] %c${name} %c${displayName} (${id}) %c${
717
- parentFiber ? `${parentDisplayName} (${parentID})` : ''
721
+ `[renderer] %c${name} %c${displayName} (${maybeID}) %c${
722
+ parentFiber ? `${parentDisplayName} (${maybeParentID})` : ''
723
} %c${extraString}`,
724
'color: red; font-weight: bold;',
725
'color: blue;',
802
throw Error('Cannot modify filter preferences while profiling');
803
}
804
805
+ unmountAndRemountAllRoots(() => {
806
+ applyComponentFilters(componentFilters);
807
+ });
808
+ }
809
+
810
+ function unmountAndRemountAllRoots(callback?: Function) {
811
// Recursively unmount all roots.
812
hook.getFiberRoots(rendererID).forEach(root => {
802
- currentRootID = getFiberID(getPrimaryFiber(root.current));
813
+ currentRootID = getOrGenerateFiberID(root.current);
814
// The TREE_OPERATION_REMOVE_ROOT operation serves two purposes:
815
// 1. It avoids sending unnecessary bridge traffic to clear a root.
816
// 2. It preserves Fiber IDs when remounting (below) which in turn ID to error/warning mapping.
819
currentRootID = -1;
820
});
821
811
- applyComponentFilters(componentFilters);
822
+ if (typeof callback === 'function') {
823
+ callback();
824
+ }
825
826
// Reset pseudo counters so that new path selections will be persisted.
827
rootDisplayNameCounter.clear();
828
829
// Recursively re-mount all roots with new filter criteria applied.
830
hook.getFiberRoots(rendererID).forEach(root => {
818
- currentRootID = getFiberID(getPrimaryFiber(root.current));
831
+ currentRootID = getOrGenerateFiberID(root.current);
832
setRootPseudoKey(currentRootID, root.current);
833
mountFiberRecursively(root.current, null, false, false);
834
flushPendingEvents(root);
959
}
960
}
961
949
- // This is a slightly annoying indirection.
950
- // It is currently necessary because DevTools wants to use unique objects as keys for instances.
951
- // However fibers have two versions.
952
- // We use this set to remember first encountered fiber for each conceptual instance.
953
- function getPrimaryFiber(fiber: Fiber): Fiber {
954
- if (primaryFibers.has(fiber)) {
955
- return fiber;
956
- }
957
- const {alternate} = fiber;
958
- if (alternate != null && primaryFibers.has(alternate)) {
959
- return alternate;
960
- }
961
- primaryFibers.add(fiber);
962
- return fiber;
963
- }
964
-
962
+ // Map of one or more Fibers in a pair to their unique id number.
963
+ // We track both Fibers to support Fast Refresh,
964
+ // which may forcefully replace one of the pair as part of hot reloading.
965
+ // In that case it's still important to be able to locate the previous ID during subsequent renders.
966
const fiberToIDMap: Map<Fiber, number> = new Map();
966
- const idToFiberMap: Map<number, Fiber> = new Map();
967
- const primaryFibers: Set<Fiber> = new Set();
967
+
968
+ // Map of id to one (arbitrary) Fiber in a pair.
969
+ // This Map is used to e.g. get the display name for a Fiber or schedule an update,
970
+ // operations that should be the same whether the current and work-in-progress Fiber is used.
971
+ const idToArbitraryFiberMap: Map<number, Fiber> = new Map();
972
973
// When profiling is supported, we store the latest tree base durations for each Fiber.
974
// This is so that we can quickly capture a snapshot of those values if profiling starts.
983
// When a mount or update is in progress, this value tracks the root that is being operated on.
984
let currentRootID: number = -1;
985
982
- function getFiberID(primaryFiber: Fiber): number {
983
- if (!fiberToIDMap.has(primaryFiber)) {
984
- const id = getUID();
985
- fiberToIDMap.set(primaryFiber, id);
986
- idToFiberMap.set(id, primaryFiber);
986
+ // Returns the unique ID for a Fiber or generates and caches a new one if the Fiber hasn't been seen before.
987
+ // Once this method has been called for a Fiber, untrackFiberID() should always be called later to avoid leaking.
988
+ function getOrGenerateFiberID(fiber: Fiber): number {
989
+ let id = null;
990
+ if (fiberToIDMap.has(fiber)) {
991
+ id = fiberToIDMap.get(fiber);
992
+ } else {
993
+ const {alternate} = fiber;
994
+ if (alternate !== null && fiberToIDMap.has(alternate)) {
995
+ id = fiberToIDMap.get(alternate);
996
+ }
997
+ }
998
+
999
+ if (id === null) {
1000
+ id = getUID();
1001
+ }
1002
+
1003
+ // This refinement is for Flow purposes only.
1004
+ const refinedID = ((id: any): number);
1005
+
1006
+ // Make sure we're tracking this Fiber
1007
+ // e.g. if it just mounted or an error was logged during initial render.
1008
+ if (!fiberToIDMap.has(fiber)) {
1009
+ fiberToIDMap.set(fiber, refinedID);
1010
+ idToArbitraryFiberMap.set(refinedID, fiber);
1011
+ }
1012
+
1013
+ // Also make sure we're tracking its alternate,
1014
+ // e.g. in case this is the first update after mount.
1015
+ const {alternate} = fiber;
1016
+ if (alternate !== null) {
1017
+ if (!fiberToIDMap.has(alternate)) {
1018
+ fiberToIDMap.set(alternate, refinedID);
1019
+ }
1020
+ }
1021
+
1022
+ return refinedID;
1023
+ }
1024
+
1025
+ // Returns an ID if one has already been generated for the Fiber or throws.
1026
+ function getFiberIDThrows(fiber: Fiber): number {
1027
+ const maybeID = getFiberIDUnsafe(fiber);
1028
+ if (maybeID !== null) {
1029
+ return maybeID;
1030
+ }
1031
+ throw Error(
1032
+ `Could not find ID for Fiber "${getDisplayNameForFiber(fiber) || ''}"`,
1033
+ );
1034
+ }
1035
+
1036
+ // Returns an ID if one has already been generated for the Fiber or null if one has not been generated.
1037
+ // Use this method while e.g. logging to avoid over-retaining Fibers.
1038
+ function getFiberIDUnsafe(fiber: Fiber): number | null {
1039
+ if (fiberToIDMap.has(fiber)) {
1040
+ return ((fiberToIDMap.get(fiber): any): number);
1041
+ } else {
1042
+ const {alternate} = fiber;
1043
+ if (alternate !== null && fiberToIDMap.has(alternate)) {
1044
+ return ((fiberToIDMap.get(alternate): any): number);
1045
+ }
1046
+ }
1047
+ return null;
1048
+ }
1049
+
1050
+ // Removes a Fiber (and its alternate) from the Maps used to track their id.
1051
+ // This method should always be called when a Fiber is unmounting.
1052
+ function untrackFiberID(fiber: Fiber) {
1053
+ const fiberID = getFiberIDUnsafe(fiber);
1054
+ if (fiberID !== null) {
1055
+ idToArbitraryFiberMap.delete(fiberID);
1056
+ }
1057
+
1058
+ fiberToIDMap.delete(fiber);
1059
+
1060
+ const {alternate} = fiber;
1061
+ if (alternate !== null) {
1062
+ fiberToIDMap.delete(alternate);
1063
}
988
- return ((fiberToIDMap.get(primaryFiber): any): number);
1064
}
1065
1066
function getChangeDescription(
1121
switch (getElementTypeForFiber(fiber)) {
1122
case ElementTypeClass:
1123
if (idToContextsMap !== null) {
1049
- const id = getFiberID(getPrimaryFiber(fiber));
1124
+ const id = getFiberIDThrows(fiber);
1125
const contexts = getContextsForFiber(fiber);
1126
if (contexts !== null) {
1127
idToContextsMap.set(id, contexts);
1177
switch (getElementTypeForFiber(fiber)) {
1178
case ElementTypeClass:
1179
if (idToContextsMap !== null) {
1105
- const id = getFiberID(getPrimaryFiber(fiber));
1180
+ const id = getFiberIDThrows(fiber);
1181
const prevContexts = idToContextsMap.has(id)
1182
? idToContextsMap.get(id)
1183
: null;
1445
clearPendingErrorsAndWarningsAfterDelay();
1446
1447
fibersWithChangedErrorOrWarningCounts.forEach(fiberID => {
1373
- const fiber = idToFiberMap.get(fiberID);
1448
+ const fiber = idToArbitraryFiberMap.get(fiberID);
1449
if (fiber != null) {
1450
// Don't send updates for Fibers that didn't mount due to e.g. Suspense or an error boundary.
1451
// We may also need to clean up after ourselves to avoid leaks.
1452
// See inline comments in onErrorOrWarning() for more info.
1453
if (isFiberMountedImpl(fiber) !== MOUNTED) {
1379
- fiberToIDMap.delete(fiber);
1380
- idToFiberMap.delete(fiberID);
1381
- primaryFibers.delete(fiber);
1454
+ untrackFiberID(fiber);
1455
return;
1456
}
1457
1612
}
1613
1614
const isRoot = fiber.tag === HostRoot;
1542
- const id = getFiberID(getPrimaryFiber(fiber));
1615
+ const id = getOrGenerateFiberID(fiber);
1616
1617
const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
1618
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
1635
const elementType = getElementTypeForFiber(fiber);
1636
const {_debugOwner} = fiber;
1637
1565
- const ownerID =
1566
- _debugOwner != null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0;
1567
- const parentID = parentFiber
1568
- ? getFiberID(getPrimaryFiber(parentFiber))
1569
- : 0;
1638
+ const ownerID = _debugOwner != null ? getFiberIDThrows(_debugOwner) : 0;
1639
+ const parentID = parentFiber ? getFiberIDThrows(parentFiber) : 0;
1640
1641
const displayNameStringID = getStringID(displayName);
1642
1671
);
1672
}
1673
1674
+ const unsafeID = getFiberIDUnsafe(fiber);
1675
+ if (fiber._debugNeedsRemount) {
1676
+ if (unsafeID === null) {
1677
+ // This inidicates a case we can't recover from:
1678
+ // Fast Refresh has force remounted a component in a way that we don't have an id for.
1679
+ // We could throw but that's a bad user experience.
1680
+ // Or we could ignore the unmount but then Store might end up with a duplicate node.
1681
+ // So a fallback is to completely reset the Store.
1682
+ // This is costly but since Fast Refresh is only used in DEV builds, it should be okay.
1683
+ setTimeout(unmountAndRemountAllRoots, 0);
1684
+ return;
1685
+ }
1686
+ }
1687
+
1688
if (trackedPathMatchFiber !== null) {
1689
// We're in the process of trying to restore previous selection.
1690
// If this fiber matched but is being unmounted, there's no use trying.
1697
}
1698
}
1699
1616
- const isRoot = fiber.tag === HostRoot;
1617
- const primaryFiber = getPrimaryFiber(fiber);
1618
- if (!fiberToIDMap.has(primaryFiber)) {
1700
+ if (unsafeID === null) {
1701
// If we've never seen this Fiber, it might be inside of a legacy render Suspense fragment (so the store is not even aware of it).
1702
// In that case we can just ignore it or it will cause errors later on.
1703
// One example of this is a Lazy component that never resolves before being unmounted.
1704
//
1705
// TODO: This is fragile and can obscure actual bugs.
1624
- //
1625
- // Calling getPrimaryFiber() lazily adds fibers to the Map, so clean up after ourselves before returning.
1626
- primaryFibers.delete(primaryFiber);
1706
return;
1707
}
1629
- const id = getFiberID(primaryFiber);
1708
+
1709
+ // Flow refinement.
1710
+ const id = ((unsafeID: any): number);
1711
+ const isRoot = fiber.tag === HostRoot;
1712
if (isRoot) {
1713
// Roots must be removed only after all children (pending and simulated) have been removed.
1714
// So we track it separately.
1723
pendingRealUnmountedIDs.push(id);
1724
}
1725
}
1644
- fiberToIDMap.delete(primaryFiber);
1645
- idToFiberMap.delete(id);
1646
- primaryFibers.delete(primaryFiber);
1726
1648
- const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
1649
- if (isProfilingSupported) {
1650
- idToRootMap.delete(id);
1651
- idToTreeBaseDurationMap.delete(id);
1727
+ if (!fiber._debugNeedsRemount) {
1728
+ untrackFiberID(fiber);
1729
+
1730
+ const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
1731
+ if (isProfilingSupported) {
1732
+ idToRootMap.delete(id);
1733
+ idToTreeBaseDurationMap.delete(id);
1734
+ }
1735
}
1736
}
1737
1758
const shouldIncludeInTree = !shouldFilterFiber(fiber);
1759
if (shouldIncludeInTree) {
1760
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);
1764
}
1765
1766
if (traceUpdatesEnabled) {
1871
}
1872
1873
function recordProfilingDurations(fiber: Fiber) {
1788
- const id = getFiberID(getPrimaryFiber(fiber));
1874
+ const id = getFiberIDThrows(fiber);
1875
const {actualDuration, treeBaseDuration} = fiber;
1876
1877
idToTreeBaseDurationMap.set(id, treeBaseDuration || 0);
1959
return;
1960
}
1961
pushOperation(TREE_OPERATION_REORDER_CHILDREN);
1876
- pushOperation(getFiberID(getPrimaryFiber(fiber)));
1962
+ pushOperation(getFiberIDThrows(fiber));
1963
pushOperation(numChildren);
1964
for (let i = 0; i < nextChildren.length; i++) {
1965
pushOperation(nextChildren[i]);
1971
nextChildren: Array<number>,
1972
) {
1973
if (!shouldFilterFiber(fiber)) {
1888
- nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
1974
+ nextChildren.push(getFiberIDThrows(fiber));
1975
} else {
1976
let child = fiber.child;
1977
const isTimedOutSuspense =
2009
debug('updateFiberRecursively()', nextFiber, parentFiber);
2010
}
2011
2012
+ const id = getOrGenerateFiberID(nextFiber);
2013
+
2014
if (traceUpdatesEnabled) {
2015
const elementType = getElementTypeForFiber(nextFiber);
2016
if (traceNearestHostComponentUpdate) {
2036
2037
if (
2038
mostRecentlyInspectedElement !== null &&
1951
- mostRecentlyInspectedElement.id ===
1952
- getFiberID(getPrimaryFiber(nextFiber)) &&
2039
+ mostRecentlyInspectedElement.id === id &&
2040
didFiberRender(prevFiber, nextFiber)
2041
) {
2042
// If this Fiber has updated, clear cached inspected data.
2180
// we should fall back to recursively marking the nearest host descendants for highlight.
2181
if (traceNearestHostComponentUpdate) {
2182
const hostFibers = findAllCurrentHostFibers(
2096
- getFiberID(getPrimaryFiber(nextFiber)),
2183
+ getFiberIDThrows(nextFiber),
2184
);
2185
hostFibers.forEach(hostFiber => {
2186
traceUpdatesForNodes.add(hostFiber.stateNode);
2264
}
2265
// If we have not been profiling, then we can just walk the tree and build up its current state as-is.
2266
hook.getFiberRoots(rendererID).forEach(root => {
2180
- currentRootID = getFiberID(getPrimaryFiber(root.current));
2267
+ currentRootID = getOrGenerateFiberID(root.current);
2268
setRootPseudoKey(currentRootID, root.current);
2269
2270
// Handle multi-renderer edge-case where only some v16 renderers support profiling.
2319
const current = root.current;
2320
const alternate = current.alternate;
2321
2235
- currentRootID = getFiberID(getPrimaryFiber(current));
2322
+ currentRootID = getOrGenerateFiberID(current);
2323
2324
// Before the traversals, remember to start tracking
2325
// our path in case we have selection to restore.
2465
}
2466
2467
function getDisplayNameForFiberID(id) {
2381
- const fiber = idToFiberMap.get(id);
2468
+ const fiber = idToArbitraryFiberMap.get(id);
2469
return fiber != null ? getDisplayNameForFiber(((fiber: any): Fiber)) : null;
2470
}
2471
2480
fiber = fiber.return;
2481
}
2482
}
2396
- return getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
2483
+ return getFiberIDThrows(((fiber: any): Fiber));
2484
}
2485
return null;
2486
}
2550
// It would be nice if we updated React to inject this function directly (vs just indirectly via findDOMNode).
2551
// BEGIN copied code
2552
function findCurrentFiberUsingSlowPathById(id: number): Fiber | null {
2466
- const fiber = idToFiberMap.get(id);
2553
+ const fiber = idToArbitraryFiberMap.get(id);
2554
if (fiber == null) {
2555
console.warn(`Could not find Fiber with id "${id}"`);
2556
return null;
2712
}
2713
2714
function prepareViewElementSource(id: number): void {
2628
- const fiber = idToFiberMap.get(id);
2715
+ const fiber = idToArbitraryFiberMap.get(id);
2716
if (fiber == null) {
2717
console.warn(`Could not find Fiber with id "${id}"`);
2718
return;
2746
function fiberToSerializedElement(fiber: Fiber): SerializedElement {
2747
return {
2748
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
2662
- id: getFiberID(getPrimaryFiber(fiber)),
2749
+ id: getFiberIDThrows(fiber),
2750
key: fiber.key,
2751
type: getElementTypeForFiber(fiber),
2752
};
3077
function updateSelectedElement(inspectedElement: InspectedElement): void {
3078
const {hooks, id, props} = inspectedElement;
3079
2993
- const fiber = idToFiberMap.get(id);
3080
+ const fiber = idToArbitraryFiberMap.get(id);
3081
if (fiber == null) {
3082
console.warn(`Could not find Fiber with id "${id}"`);
3083
return;
3595
idToContextsMap = new Map();
3596
3597
hook.getFiberRoots(rendererID).forEach(root => {
3511
- const rootID = getFiberID(getPrimaryFiber(root.current));
3598
+ const rootID = getFiberIDThrows(root.current);
3599
((displayNamesByRootID: any): DisplayNamesByRootID).set(
3600
rootID,
3601
getDisplayNameForRoot(root.current),
3638
3639
const forceFallbackForSuspenseIDs = new Set();
3640
function shouldSuspendFiberAccordingToSet(fiber) {
3554
- const id = getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
3555
- return forceFallbackForSuspenseIDs.has(id);
3641
+ const maybeID = getFiberIDUnsafe(((fiber: any): Fiber));
3642
+ return maybeID !== null && forceFallbackForSuspenseIDs.has(maybeID);
3643
}
3644
3645
function overrideSuspense(id, forceFallback) {
3664
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
3665
}
3666
}
3580
- const fiber = idToFiberMap.get(id);
3667
+ const fiber = idToArbitraryFiberMap.get(id);
3668
if (fiber != null) {
3669
scheduleUpdate(fiber);
3670
}
3815
case HostRoot:
3816
// Roots don't have a real displayName, index, or key.
3817
// Instead, we'll use the pseudo key (childDisplayName:indexWithThatName).
3731
- const id = getFiberID(getPrimaryFiber(fiber));
3818
+ const id = getFiberIDThrows(fiber);
3819
const pseudoKey = rootPseudoKeys.get(id);
3820
if (pseudoKey === undefined) {
3821
throw new Error('Expected mounted root to have known pseudo key.');
3840
// The return path will contain Fibers that are "invisible" to the store
3841
// because their keys and indexes are important to restoring the selection.
3842
function getPathForElement(id: number): Array<PathFrame> | null {
3756
- let fiber = idToFiberMap.get(id);
3843
+ let fiber = idToArbitraryFiberMap.get(id);
3844
if (fiber == null) {
3845
return null;
3846
}
3871
return null;
3872
}
3873
return {
3787
- id: getFiberID(getPrimaryFiber(fiber)),
3874
+ id: getFiberIDThrows(fiber),
3875
isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
3876
};
3877
}