Skip to content

Commit 13ddf10

Browse files
authored
[DevTools] Find owners from the parent path that matches the Fiber or ReactComponentInfo (#30717)
This enables finding Server Components on the owner path. Server Components aren't stateful so there's not actually one specific owner that it necessarily matches. So it can't be a global look up. E.g. the same Server Component can be rendered in two places or even nested inside each other. Therefore we need to find an appropriate instance using a heuristic. We can do that by traversing the parent path since the owner is likely also a parent. Not always but almost always. To simplify things we can also do the same for Fibers. That brings us one step closer to being able to get rid of the global fiberToFiberInstance map since we can just use the shadow tree to find this information. This does mean that we can't find owners that aren't parents which is usually ok. However, there is a test case that's interesting where you have a React ART tree inside a DOM tree. In that case the owners actually span multiple renderers and roots so the owner is not on the parent stack. Usually this is fine since you'd just care about the owners within React ART but ideally we'd support this. However, I think that really the fix to this is that the React ART tree itself should actually show up inside the DOM tree in DevTools and in the virtual shadow tree because that's conceptually where it belongs. That would then solve this particular issue. We'd just need some way to associate the root with a DOM parent when it gets mounted.
1 parent a58276c commit 13ddf10

File tree

2 files changed

+159
-92
lines changed

2 files changed

+159
-92
lines changed

packages/react-devtools-shared/src/__tests__/inspectedElement-test.js

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,26 +2893,29 @@ describe('InspectedElement', () => {
28932893
`);
28942894

28952895
const inspectedElement = await inspectElementAtIndex(4);
2896-
expect(inspectedElement.owners).toMatchInlineSnapshot(`
2897-
[
2898-
{
2899-
"compiledWithForget": false,
2900-
"displayName": "Child",
2901-
"hocDisplayNames": null,
2902-
"id": 8,
2903-
"key": null,
2904-
"type": 5,
2905-
},
2906-
{
2907-
"compiledWithForget": false,
2908-
"displayName": "App",
2909-
"hocDisplayNames": null,
2910-
"id": 7,
2911-
"key": null,
2912-
"type": 5,
2913-
},
2914-
]
2915-
`);
2896+
// TODO: Ideally this should match the owners of the Group but those are
2897+
// part of a different parent tree. Ideally the Group would be parent of
2898+
// that parent tree though which would fix this issue.
2899+
//
2900+
// [
2901+
// {
2902+
// "compiledWithForget": false,
2903+
// "displayName": "Child",
2904+
// "hocDisplayNames": null,
2905+
// "id": 8,
2906+
// "key": null,
2907+
// "type": 5,
2908+
// },
2909+
// {
2910+
// "compiledWithForget": false,
2911+
// "displayName": "App",
2912+
// "hocDisplayNames": null,
2913+
// "id": 7,
2914+
// "key": null,
2915+
// "type": 5,
2916+
// },
2917+
// ]
2918+
expect(inspectedElement.owners).toMatchInlineSnapshot(`[]`);
29162919
});
29172920

29182921
describe('error boundary', () => {

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

Lines changed: 136 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,29 +2143,18 @@ export function attach(
21432143
const {key} = fiber;
21442144
const displayName = getDisplayNameForFiber(fiber);
21452145
const elementType = getElementTypeForFiber(fiber);
2146-
const debugOwner = fiber._debugOwner;
2147-
2148-
// Ideally we should call getFiberIDThrows() for _debugOwner,
2149-
// since owners are almost always higher in the tree (and so have already been processed),
2150-
// but in some (rare) instances reported in open source, a descendant mounts before an owner.
2151-
// Since this is a DEV only field it's probably okay to also just lazily generate and ID here if needed.
2152-
// See https://github.com/facebook/react/issues/21445
2153-
let ownerID: number;
2154-
if (debugOwner != null) {
2155-
if (typeof debugOwner.tag === 'number') {
2156-
const ownerFiberInstance = getFiberInstanceUnsafe((debugOwner: any));
2157-
if (ownerFiberInstance !== null) {
2158-
ownerID = ownerFiberInstance.id;
2159-
} else {
2160-
ownerID = 0;
2161-
}
2162-
} else {
2163-
// TODO: Track Server Component Owners.
2164-
ownerID = 0;
2165-
}
2166-
} else {
2167-
ownerID = 0;
2168-
}
2146+
2147+
// Finding the owner instance might require traversing the whole parent path which
2148+
// doesn't have great big O notation. Ideally we'd lazily fetch the owner when we
2149+
// need it but we have some synchronous operations in the front end like Alt+Left
2150+
// which selects the owner immediately. Typically most owners are only a few parents
2151+
// away so maybe it's not so bad.
2152+
const debugOwner = getUnfilteredOwner(fiber);
2153+
const ownerInstance = findNearestOwnerInstance(
2154+
parentInstance,
2155+
debugOwner,
2156+
);
2157+
const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
21692158
const parentID = parentInstance ? parentInstance.id : 0;
21702159

21712160
const displayNameStringID = getStringID(displayName);
@@ -2231,11 +2220,15 @@ export function attach(
22312220
displayName = env + '(' + displayName + ')';
22322221
}
22332222
const elementType = ElementTypeVirtual;
2234-
// TODO: Support Virtual Owners. To do this we need to find a matching
2235-
// virtual instance which is not a super cheap parent traversal and so
2236-
// we should ideally only do that lazily. We should maybe change the
2237-
// frontend to get it lazily.
2238-
const ownerID: number = 0;
2223+
2224+
// Finding the owner instance might require traversing the whole parent path which
2225+
// doesn't have great big O notation. Ideally we'd lazily fetch the owner when we
2226+
// need it but we have some synchronous operations in the front end like Alt+Left
2227+
// which selects the owner immediately. Typically most owners are only a few parents
2228+
// away so maybe it's not so bad.
2229+
const debugOwner = getUnfilteredOwner(componentInfo);
2230+
const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner);
2231+
const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
22392232
const parentID = parentInstance ? parentInstance.id : 0;
22402233

22412234
const displayNameStringID = getStringID(displayName);
@@ -3354,11 +3347,19 @@ export function attach(
33543347
}
33553348

33563349
function getUpdatersList(root: any): Array<SerializedElement> | null {
3357-
return root.memoizedUpdaters != null
3358-
? Array.from(root.memoizedUpdaters)
3359-
.filter(fiber => getFiberIDUnsafe(fiber) !== null)
3360-
.map(fiberToSerializedElement)
3361-
: null;
3350+
const updaters = root.memoizedUpdaters;
3351+
if (updaters == null) {
3352+
return null;
3353+
}
3354+
const result = [];
3355+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
3356+
for (const updater of updaters) {
3357+
const inst = getFiberInstanceUnsafe(updater);
3358+
if (inst !== null) {
3359+
result.push(instanceToSerializedElement(inst));
3360+
}
3361+
}
3362+
return result;
33623363
}
33633364

33643365
function handleCommitFiberUnmount(fiber: any) {
@@ -3923,13 +3924,26 @@ export function attach(
39233924
}
39243925
}
39253926

3926-
function fiberToSerializedElement(fiber: Fiber): SerializedElement {
3927-
return {
3928-
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
3929-
id: getFiberIDThrows(fiber),
3930-
key: fiber.key,
3931-
type: getElementTypeForFiber(fiber),
3932-
};
3927+
function instanceToSerializedElement(
3928+
instance: DevToolsInstance,
3929+
): SerializedElement {
3930+
if (instance.kind === FIBER_INSTANCE) {
3931+
const fiber = instance.data;
3932+
return {
3933+
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
3934+
id: instance.id,
3935+
key: fiber.key,
3936+
type: getElementTypeForFiber(fiber),
3937+
};
3938+
} else {
3939+
const componentInfo = instance.data;
3940+
return {
3941+
displayName: componentInfo.name || 'Anonymous',
3942+
id: instance.id,
3943+
key: componentInfo.key == null ? null : componentInfo.key,
3944+
type: ElementTypeVirtual,
3945+
};
3946+
}
39333947
}
39343948

39353949
function getOwnersList(id: number): Array<SerializedElement> | null {
@@ -3938,33 +3952,97 @@ export function attach(
39383952
console.warn(`Could not find DevToolsInstance with id "${id}"`);
39393953
return null;
39403954
}
3941-
if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3942-
// TODO: Handle VirtualInstance.
3943-
return null;
3955+
const self = instanceToSerializedElement(devtoolsInstance);
3956+
const owners = getOwnersListFromInstance(devtoolsInstance);
3957+
// This is particular API is prefixed with the current instance too for some reason.
3958+
if (owners === null) {
3959+
return [self];
39443960
}
3945-
const fiber =
3946-
findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
3947-
if (fiber == null) {
3961+
owners.unshift(self);
3962+
owners.reverse();
3963+
return owners;
3964+
}
3965+
3966+
function getOwnersListFromInstance(
3967+
instance: DevToolsInstance,
3968+
): Array<SerializedElement> | null {
3969+
let owner = getUnfilteredOwner(instance.data);
3970+
if (owner === null) {
39483971
return null;
39493972
}
3973+
const owners: Array<SerializedElement> = [];
3974+
let parentInstance: null | DevToolsInstance = instance.parent;
3975+
while (parentInstance !== null && owner !== null) {
3976+
const ownerInstance = findNearestOwnerInstance(parentInstance, owner);
3977+
if (ownerInstance !== null) {
3978+
owners.push(instanceToSerializedElement(ownerInstance));
3979+
// Get the next owner and keep searching from the previous match.
3980+
owner = getUnfilteredOwner(owner);
3981+
parentInstance = ownerInstance.parent;
3982+
} else {
3983+
break;
3984+
}
3985+
}
3986+
return owners;
3987+
}
39503988

3951-
const owners: Array<SerializedElement> = [fiberToSerializedElement(fiber)];
3952-
3953-
let owner = fiber._debugOwner;
3954-
while (owner != null) {
3989+
function getUnfilteredOwner(
3990+
owner: ReactComponentInfo | Fiber | null | void,
3991+
): ReactComponentInfo | Fiber | null {
3992+
if (owner == null) {
3993+
return null;
3994+
}
3995+
if (typeof owner.tag === 'number') {
3996+
const ownerFiber: Fiber = (owner: any); // Refined
3997+
owner = ownerFiber._debugOwner;
3998+
} else {
3999+
const ownerInfo: ReactComponentInfo = (owner: any); // Refined
4000+
owner = ownerInfo.owner;
4001+
}
4002+
while (owner) {
39554003
if (typeof owner.tag === 'number') {
39564004
const ownerFiber: Fiber = (owner: any); // Refined
39574005
if (!shouldFilterFiber(ownerFiber)) {
3958-
owners.unshift(fiberToSerializedElement(ownerFiber));
4006+
return ownerFiber;
39594007
}
39604008
owner = ownerFiber._debugOwner;
39614009
} else {
3962-
// TODO: Track Server Component Owners.
3963-
break;
4010+
const ownerInfo: ReactComponentInfo = (owner: any); // Refined
4011+
if (!shouldFilterVirtual(ownerInfo)) {
4012+
return ownerInfo;
4013+
}
4014+
owner = ownerInfo.owner;
39644015
}
39654016
}
4017+
return null;
4018+
}
39664019

3967-
return owners;
4020+
function findNearestOwnerInstance(
4021+
parentInstance: null | DevToolsInstance,
4022+
owner: void | null | ReactComponentInfo | Fiber,
4023+
): null | DevToolsInstance {
4024+
if (owner == null) {
4025+
return null;
4026+
}
4027+
// Search the parent path for any instance that matches this kind of owner.
4028+
while (parentInstance !== null) {
4029+
if (
4030+
parentInstance.data === owner ||
4031+
// Typically both owner and instance.data would refer to the current version of a Fiber
4032+
// but it is possible for memoization to ignore the owner on the JSX. Then the new Fiber
4033+
// isn't propagated down as the new owner. In that case we might match the alternate
4034+
// instead. This is a bit hacky but the fastest check since type casting owner to a Fiber
4035+
// needs a duck type check anyway.
4036+
parentInstance.data === (owner: any).alternate
4037+
) {
4038+
return parentInstance;
4039+
}
4040+
parentInstance = parentInstance.parent;
4041+
}
4042+
// It is technically possible to create an element and render it in a different parent
4043+
// but this is a weird edge case and it is worth not having to scan the tree or keep
4044+
// a register for every fiber/component info.
4045+
return null;
39684046
}
39694047

39704048
// Fast path props lookup for React Native style editor.
@@ -4047,7 +4125,6 @@ export function attach(
40474125
}
40484126

40494127
const {
4050-
_debugOwner: debugOwner,
40514128
stateNode,
40524129
key,
40534130
memoizedProps,
@@ -4174,21 +4251,8 @@ export function attach(
41744251
context = {value: context};
41754252
}
41764253

4177-
let owners: null | Array<SerializedElement> = null;
4178-
let owner = debugOwner;
4179-
while (owner != null) {
4180-
if (typeof owner.tag === 'number') {
4181-
const ownerFiber: Fiber = (owner: any); // Refined
4182-
if (owners === null) {
4183-
owners = [];
4184-
}
4185-
owners.push(fiberToSerializedElement(ownerFiber));
4186-
owner = ownerFiber._debugOwner;
4187-
} else {
4188-
// TODO: Track Server Component Owners.
4189-
break;
4190-
}
4191-
}
4254+
const owners: null | Array<SerializedElement> =
4255+
getOwnersListFromInstance(fiberInstance);
41924256

41934257
const isTimedOutSuspense =
41944258
tag === SuspenseComponent && memoizedState !== null;
@@ -4352,8 +4416,8 @@ export function attach(
43524416
displayName = env + '(' + displayName + ')';
43534417
}
43544418

4355-
// TODO: Support Virtual Owners.
4356-
const owners: null | Array<SerializedElement> = null;
4419+
const owners: null | Array<SerializedElement> =
4420+
getOwnersListFromInstance(virtualInstance);
43574421

43584422
let rootType = null;
43594423
let targetErrorBoundaryID = null;

0 commit comments

Comments
 (0)