Skip to content

Commit f963c80

Browse files
authored
[DevTools] Implement "best renderer" by taking the inner most matched node (#30494)
Stacked on #30491. When going from DOM Node to select a component or highlight a component we find the nearest mounted ancestor. However, when multiple renderers are nested there can be multiple ancestors. The original fix #24665 did this by taking the inner renderer if it was an exact match but if it wasn't it just took the first renderer. Instead, we can track the inner most node we've found so far. Then get the ID from that node (which will be fast since it's now a perfect match). This is a better match. However, the main reason I'm doing this is because the old mechanism leaked the `Fiber` type outside the `RendererInterface` which is supposed to abstract all of that. With the new algorithm this doesn't leak. I've tested this with a new build against the repro in the old issue #24539 and it seems to work.
1 parent 41ecbad commit f963c80

File tree

8 files changed

+98
-46
lines changed

8 files changed

+98
-46
lines changed

packages/react-devtools-shared/src/__tests__/legacy/storeLegacy-v15-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ describe('Store (legacy)', () => {
905905
[root]
906906
▸ <Wrapper>
907907
`);
908-
const deepestedNodeID = global.agent.getIDForNode(ref);
908+
const deepestedNodeID = global.agent.getIDForHostInstance(ref);
909909
act(() => store.toggleIsCollapsed(deepestedNodeID, false));
910910
expect(store).toMatchInlineSnapshot(`
911911
[root]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1217,7 +1217,7 @@ describe('Store', () => {
12171217
▸ <Wrapper>
12181218
`);
12191219

1220-
const deepestedNodeID = agent.getIDForNode(ref.current);
1220+
const deepestedNodeID = agent.getIDForHostInstance(ref.current);
12211221

12221222
act(() => store.toggleIsCollapsed(deepestedNodeID, false));
12231223
expect(store).toMatchInlineSnapshot(`

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

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import type {
4343
ComponentFilter,
4444
BrowserTheme,
4545
} from 'react-devtools-shared/src/frontend/types';
46-
import {isSynchronousXHRSupported} from './utils';
46+
import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils';
4747

4848
const debug = (methodName: string, ...args: Array<string>) => {
4949
if (__DEBUG__) {
@@ -343,31 +343,79 @@ export default class Agent extends EventEmitter<{
343343
return renderer.getInstanceAndStyle(id);
344344
}
345345

346-
getBestMatchingRendererInterface(node: Object): RendererInterface | null {
347-
let bestMatch = null;
346+
getIDForHostInstance(target: HostInstance): number | null {
347+
let bestMatch: null | HostInstance = null;
348+
let bestRenderer: null | RendererInterface = null;
349+
// Find the nearest ancestor which is mounted by a React.
348350
for (const rendererID in this._rendererInterfaces) {
349351
const renderer = ((this._rendererInterfaces[
350352
(rendererID: any)
351353
]: any): RendererInterface);
352-
const fiber = renderer.getFiberForNative(node);
353-
if (fiber !== null) {
354-
// check if fiber.stateNode is matching the original hostInstance
355-
if (fiber.stateNode === node) {
356-
return renderer;
357-
} else if (bestMatch === null) {
358-
bestMatch = renderer;
354+
const nearestNode: null = renderer.getNearestMountedHostInstance(target);
355+
if (nearestNode !== null) {
356+
if (nearestNode === target) {
357+
// Exact match we can exit early.
358+
bestMatch = nearestNode;
359+
bestRenderer = renderer;
360+
break;
361+
}
362+
if (
363+
bestMatch === null ||
364+
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
365+
) {
366+
// If this is the first match or the previous match contains the new match,
367+
// so the new match is a deeper and therefore better match.
368+
bestMatch = nearestNode;
369+
bestRenderer = renderer;
359370
}
360371
}
361372
}
362-
// if an exact match is not found, return the first valid renderer as fallback
363-
return bestMatch;
373+
if (bestRenderer != null && bestMatch != null) {
374+
try {
375+
return bestRenderer.getElementIDForHostInstance(bestMatch, true);
376+
} catch (error) {
377+
// Some old React versions might throw if they can't find a match.
378+
// If so we should ignore it...
379+
}
380+
}
381+
return null;
364382
}
365383

366-
getIDForNode(node: Object): number | null {
367-
const rendererInterface = this.getBestMatchingRendererInterface(node);
368-
if (rendererInterface != null) {
384+
getComponentNameForHostInstance(target: HostInstance): string | null {
385+
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
386+
let bestMatch: null | HostInstance = null;
387+
let bestRenderer: null | RendererInterface = null;
388+
// Find the nearest ancestor which is mounted by a React.
389+
for (const rendererID in this._rendererInterfaces) {
390+
const renderer = ((this._rendererInterfaces[
391+
(rendererID: any)
392+
]: any): RendererInterface);
393+
const nearestNode = renderer.getNearestMountedHostInstance(target);
394+
if (nearestNode !== null) {
395+
if (nearestNode === target) {
396+
// Exact match we can exit early.
397+
bestMatch = nearestNode;
398+
bestRenderer = renderer;
399+
break;
400+
}
401+
if (
402+
bestMatch === null ||
403+
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
404+
) {
405+
// If this is the first match or the previous match contains the new match,
406+
// so the new match is a deeper and therefore better match.
407+
bestMatch = nearestNode;
408+
bestRenderer = renderer;
409+
}
410+
}
411+
}
412+
413+
if (bestRenderer != null && bestMatch != null) {
369414
try {
370-
return rendererInterface.getElementIDForHostInstance(node, true);
415+
const id = bestRenderer.getElementIDForHostInstance(bestMatch, true);
416+
if (id) {
417+
return bestRenderer.getDisplayNameForElementID(id);
418+
}
371419
} catch (error) {
372420
// Some old React versions might throw if they can't find a match.
373421
// If so we should ignore it...
@@ -616,8 +664,8 @@ export default class Agent extends EventEmitter<{
616664
}
617665
};
618666

619-
selectNode(target: Object): void {
620-
const id = this.getIDForNode(target);
667+
selectNode(target: HostInstance): void {
668+
const id = this.getIDForHostInstance(target);
621669
if (id !== null) {
622670
this._bridge.send('selectElement', id);
623671
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2882,8 +2882,14 @@ export function attach(
28822882
return fiber != null ? getDisplayNameForFiber(fiber) : null;
28832883
}
28842884

2885-
function getFiberForNative(hostInstance: HostInstance) {
2886-
return renderer.findFiberByHostInstance(hostInstance);
2885+
function getNearestMountedHostInstance(
2886+
hostInstance: HostInstance,
2887+
): null | HostInstance {
2888+
const mountedHostInstance = renderer.findFiberByHostInstance(hostInstance);
2889+
if (mountedHostInstance != null) {
2890+
return mountedHostInstance.stateNode;
2891+
}
2892+
return null;
28872893
}
28882894

28892895
function getElementIDForHostInstance(
@@ -4659,7 +4665,7 @@ export function attach(
46594665
flushInitialOperations,
46604666
getBestMatchForTrackedPath,
46614667
getDisplayNameForElementID,
4662-
getFiberForNative,
4668+
getNearestMountedHostInstance,
46634669
getElementIDForHostInstance,
46644670
getInstanceAndStyle,
46654671
getOwnersList,

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ export function attach(
145145
let getElementIDForHostInstance: GetElementIDForHostInstance =
146146
((null: any): GetElementIDForHostInstance);
147147
let findHostInstanceForInternalID: (id: number) => ?HostInstance;
148-
let getFiberForNative = (node: HostInstance) => {
148+
let getNearestMountedHostInstance = (
149+
node: HostInstance,
150+
): null | HostInstance => {
149151
// Not implemented.
150152
return null;
151153
};
@@ -160,8 +162,15 @@ export function attach(
160162
const internalInstance = idToInternalInstanceMap.get(id);
161163
return renderer.ComponentTree.getNodeFromInstance(internalInstance);
162164
};
163-
getFiberForNative = (node: HostInstance) => {
164-
return renderer.ComponentTree.getClosestInstanceFromNode(node);
165+
getNearestMountedHostInstance = (
166+
node: HostInstance,
167+
): null | HostInstance => {
168+
const internalInstance =
169+
renderer.ComponentTree.getClosestInstanceFromNode(node);
170+
if (internalInstance != null) {
171+
return renderer.ComponentTree.getNodeFromInstance(internalInstance);
172+
}
173+
return null;
165174
};
166175
} else if (renderer.Mount.getID && renderer.Mount.getNode) {
167176
getElementIDForHostInstance = (node, findNearestUnfilteredAncestor) => {
@@ -1111,7 +1120,7 @@ export function attach(
11111120
flushInitialOperations,
11121121
getBestMatchForTrackedPath,
11131122
getDisplayNameForElementID,
1114-
getFiberForNative,
1123+
getNearestMountedHostInstance,
11151124
getElementIDForHostInstance,
11161125
getInstanceAndStyle,
11171126
findHostInstancesForElementID: (id: number) => {

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,7 @@ type SharedInternalsSubset = {
8686
};
8787
export type CurrentDispatcherRef = SharedInternalsSubset;
8888

89-
export type GetDisplayNameForElementID = (
90-
id: number,
91-
findNearestUnfilteredAncestor?: boolean,
92-
) => string | null;
89+
export type GetDisplayNameForElementID = (id: number) => string | null;
9390

9491
export type GetElementIDForHostInstance = (
9592
component: HostInstance,
@@ -363,7 +360,9 @@ export type RendererInterface = {
363360
findHostInstancesForElementID: FindHostInstancesForElementID,
364361
flushInitialOperations: () => void,
365362
getBestMatchForTrackedPath: () => PathMatch | null,
366-
getFiberForNative: (component: HostInstance) => Fiber | null,
363+
getNearestMountedHostInstance: (
364+
component: HostInstance,
365+
) => HostInstance | null,
367366
getElementIDForHostInstance: GetElementIDForHostInstance,
368367
getDisplayNameForElementID: GetDisplayNameForElementID,
369368
getInstanceAndStyle(id: number): InstanceAndStyle,

packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -233,19 +233,9 @@ export default class Overlay {
233233
name = elements[0].nodeName.toLowerCase();
234234

235235
const node = elements[0];
236-
const rendererInterface =
237-
this.agent.getBestMatchingRendererInterface(node);
238-
if (rendererInterface) {
239-
const id = rendererInterface.getElementIDForHostInstance(node, true);
240-
if (id) {
241-
const ownerName = rendererInterface.getDisplayNameForElementID(
242-
id,
243-
true,
244-
);
245-
if (ownerName) {
246-
name += ' (in ' + ownerName + ')';
247-
}
248-
}
236+
const ownerName = this.agent.getComponentNameForHostInstance(node);
237+
if (ownerName) {
238+
name += ' (in ' + ownerName + ')';
249239
}
250240
}
251241

packages/react-devtools-shared/src/backend/views/Highlighter/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export default function setupHighlighter(
193193

194194
const selectElementForNode = throttle(
195195
memoize((node: HTMLElement) => {
196-
const id = agent.getIDForNode(node);
196+
const id = agent.getIDForHostInstance(node);
197197
if (id !== null) {
198198
bridge.send('selectElement', id);
199199
}

0 commit comments

Comments
 (0)