Skip to content

Commit 8a70d31

Browse files
authored
[DevTools] Track DOM nodes to Fiber map for HostHoistable Resources (#30590)
Follow up from #30584. You can already select a singleton or hoistable (that's not a resource) in the browser elements panel and it'll select the corresponding node in the RDT Components panel. That works because it uses the same mechanism as event dispatching and those need to be able to receive events. However, you can't select a resource. Because that's conceptually one to many. This keeps track of which fiber is acquiring which resource so we can find all the corresponding instances. E.g. now you can select the `<link rel="stylesheet">` in the Flight fixture in the Element panel and then the component that rendered it in the Components panel will be selected. If we had a concept multi-selection we could potentially select all of them. This similar to how a Server Component can be rendered in more than one place and if we want to select all matching ones. It's kind of weird though and both cases are edge cases. Notably imperative preloads do have elements that don't have any corresponding component but that's ok. So they'll just select `<head>`. Maybe in dev we could track the owners of those.
1 parent 3af905d commit 8a70d31

File tree

1 file changed

+112
-36
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+112
-36
lines changed

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

+112-36
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,40 @@ const fiberToFiberInstanceMap: Map<Fiber, FiberInstance> = new Map();
696696
// operations that should be the same whether the current and work-in-progress Fiber is used.
697697
const idToDevToolsInstanceMap: Map<number, DevToolsInstance> = new Map();
698698

699+
// Map of resource DOM nodes to all the Fibers that depend on it.
700+
const hostResourceToFiberMap: Map<HostInstance, Set<Fiber>> = new Map();
701+
702+
function aquireHostResource(
703+
fiber: Fiber,
704+
resource: ?{instance?: HostInstance},
705+
): void {
706+
const hostInstance = resource && resource.instance;
707+
if (hostInstance) {
708+
let resourceFibers = hostResourceToFiberMap.get(hostInstance);
709+
if (resourceFibers === undefined) {
710+
resourceFibers = new Set();
711+
hostResourceToFiberMap.set(hostInstance, resourceFibers);
712+
}
713+
resourceFibers.add(fiber);
714+
}
715+
}
716+
717+
function releaseHostResource(
718+
fiber: Fiber,
719+
resource: ?{instance?: HostInstance},
720+
): void {
721+
const hostInstance = resource && resource.instance;
722+
if (hostInstance) {
723+
const resourceFibers = hostResourceToFiberMap.get(hostInstance);
724+
if (resourceFibers !== undefined) {
725+
resourceFibers.delete(fiber);
726+
if (resourceFibers.size === 0) {
727+
hostResourceToFiberMap.delete(hostInstance);
728+
}
729+
}
730+
}
731+
}
732+
699733
export function attach(
700734
hook: DevToolsHook,
701735
rendererID: number,
@@ -2283,6 +2317,10 @@ export function attach(
22832317
// because we don't want to highlight every host node inside of a newly mounted subtree.
22842318
}
22852319

2320+
if (fiber.tag === HostHoistable) {
2321+
aquireHostResource(fiber, fiber.memoizedState);
2322+
}
2323+
22862324
if (fiber.tag === SuspenseComponent) {
22872325
const isTimedOut = fiber.memoizedState !== null;
22882326
if (isTimedOut) {
@@ -2344,8 +2382,11 @@ export function attach(
23442382

23452383
// We might meet a nested Suspense on our way.
23462384
const isTimedOutSuspense =
2347-
fiber.tag === ReactTypeOfWork.SuspenseComponent &&
2348-
fiber.memoizedState !== null;
2385+
fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2386+
2387+
if (fiber.tag === HostHoistable) {
2388+
releaseHostResource(fiber, fiber.memoizedState);
2389+
}
23492390

23502391
let child = fiber.child;
23512392
if (isTimedOutSuspense) {
@@ -2621,6 +2662,12 @@ export function attach(
26212662
const newParentInstance = shouldIncludeInTree
26222663
? fiberInstance
26232664
: parentInstance;
2665+
2666+
if (nextFiber.tag === HostHoistable) {
2667+
releaseHostResource(prevFiber, prevFiber.memoizedState);
2668+
aquireHostResource(nextFiber, nextFiber.memoizedState);
2669+
}
2670+
26242671
const isSuspense = nextFiber.tag === SuspenseComponent;
26252672
let shouldResetChildren = false;
26262673
// The behavior of timed-out Suspense trees is unique.
@@ -3070,9 +3117,55 @@ export function attach(
30703117
function getNearestMountedHostInstance(
30713118
hostInstance: HostInstance,
30723119
): null | HostInstance {
3073-
const mountedHostInstance = renderer.findFiberByHostInstance(hostInstance);
3074-
if (mountedHostInstance != null) {
3075-
return mountedHostInstance.stateNode;
3120+
const mountedFiber = renderer.findFiberByHostInstance(hostInstance);
3121+
if (mountedFiber != null) {
3122+
if (mountedFiber.stateNode !== hostInstance) {
3123+
// If it's not a perfect match the specific one might be a resource.
3124+
// We don't need to look at any parents because host resources don't have
3125+
// children so it won't be in any parent if it's not this one.
3126+
if (hostResourceToFiberMap.has(hostInstance)) {
3127+
return hostInstance;
3128+
}
3129+
}
3130+
return mountedFiber.stateNode;
3131+
}
3132+
if (hostResourceToFiberMap.has(hostInstance)) {
3133+
return hostInstance;
3134+
}
3135+
return null;
3136+
}
3137+
3138+
function findNearestUnfilteredElementID(searchFiber: Fiber) {
3139+
let fiber: null | Fiber = searchFiber;
3140+
while (fiber !== null) {
3141+
const fiberInstance = getFiberInstanceUnsafe(fiber);
3142+
if (fiberInstance !== null) {
3143+
// TODO: Ideally we would not have any filtered FiberInstances which
3144+
// would make this logic much simpler. Unfortunately, we sometimes
3145+
// eagerly add to the map and some times don't eagerly clean it up.
3146+
// TODO: If the fiber is filtered, the FiberInstance wouldn't really
3147+
// exist which would mean that we also don't have a way to get to the
3148+
// VirtualInstances.
3149+
if (!shouldFilterFiber(fiberInstance.data)) {
3150+
return fiberInstance.id;
3151+
}
3152+
// We couldn't use this Fiber but we might have a VirtualInstance
3153+
// that is the nearest unfiltered instance.
3154+
let parentInstance = fiberInstance.parent;
3155+
while (parentInstance !== null) {
3156+
if (parentInstance.kind === FIBER_INSTANCE) {
3157+
// If we find a parent Fiber, it might not be the nearest parent
3158+
// so we break out and continue walking the Fiber tree instead.
3159+
break;
3160+
} else {
3161+
if (!shouldFilterVirtual(parentInstance.data)) {
3162+
return parentInstance.id;
3163+
}
3164+
}
3165+
parentInstance = parentInstance.parent;
3166+
}
3167+
}
3168+
fiber = fiber.return;
30763169
}
30773170
return null;
30783171
}
@@ -3081,42 +3174,25 @@ export function attach(
30813174
hostInstance: HostInstance,
30823175
findNearestUnfilteredAncestor: boolean = false,
30833176
): number | null {
3084-
let fiber = renderer.findFiberByHostInstance(hostInstance);
3177+
const resourceFibers = hostResourceToFiberMap.get(hostInstance);
3178+
if (resourceFibers !== undefined) {
3179+
// This is a resource. Find the first unfiltered instance.
3180+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
3181+
for (const resourceFiber of resourceFibers) {
3182+
const elementID = findNearestUnfilteredElementID(resourceFiber);
3183+
if (elementID !== null) {
3184+
return elementID;
3185+
}
3186+
}
3187+
// If we don't find one, fallthrough to select the parent instead.
3188+
}
3189+
const fiber = renderer.findFiberByHostInstance(hostInstance);
30853190
if (fiber != null) {
30863191
if (!findNearestUnfilteredAncestor) {
30873192
// TODO: Remove this option. It's not used.
30883193
return getFiberIDThrows(fiber);
30893194
}
3090-
while (fiber !== null) {
3091-
const fiberInstance = getFiberInstanceUnsafe(fiber);
3092-
if (fiberInstance !== null) {
3093-
// TODO: Ideally we would not have any filtered FiberInstances which
3094-
// would make this logic much simpler. Unfortunately, we sometimes
3095-
// eagerly add to the map and some times don't eagerly clean it up.
3096-
// TODO: If the fiber is filtered, the FiberInstance wouldn't really
3097-
// exist which would mean that we also don't have a way to get to the
3098-
// VirtualInstances.
3099-
if (!shouldFilterFiber(fiberInstance.data)) {
3100-
return fiberInstance.id;
3101-
}
3102-
// We couldn't use this Fiber but we might have a VirtualInstance
3103-
// that is the nearest unfiltered instance.
3104-
let parentInstance = fiberInstance.parent;
3105-
while (parentInstance !== null) {
3106-
if (parentInstance.kind === FIBER_INSTANCE) {
3107-
// If we find a parent Fiber, it might not be the nearest parent
3108-
// so we break out and continue walking the Fiber tree instead.
3109-
break;
3110-
} else {
3111-
if (!shouldFilterVirtual(parentInstance.data)) {
3112-
return parentInstance.id;
3113-
}
3114-
}
3115-
parentInstance = parentInstance.parent;
3116-
}
3117-
}
3118-
fiber = fiber.return;
3119-
}
3195+
return findNearestUnfilteredElementID(fiber);
31203196
}
31213197
return null;
31223198
}

0 commit comments

Comments
 (0)