Skip to content

Commit e2c0670

Browse files
committed
[DevTools] Track all public HostInstances in a Map (#30831)
This lets us get from a HostInstance to the nearest DevToolsInstance without relying on `findFiberByHostInstance` and `fiberToDevToolsInstanceMap`. We already did the equivalent of this for Resources in HostHoistables. One issue before was that we'd ideally get away from the `fiberToDevToolsInstanceMap` map in general since we should ideally not treat Fibers as stateful but they could be replaced by something else stateful in principle. This PR also addresses Virtual Instances. Now you can select a DOM node and have it select a Virtual Instance if that's the nearest parent since the parent doesn't have to be a Fiber anymore. However, the other reason for this change is that I'd like to get rid of the need for the `findFiberByHostInstance` from being injected. A renderer should not need to store a reference back from its instance to a Fiber. Without the Synthetic Event system this wouldn't be needed by the renderer so we should be able to remove it. We also don't really need it since we have all the information by just walking the commit to collect the nodes if we just maintain our own Map. There's one subtle nuance that the different renderers do. Typically a HostInstance is the same thing as a PublicInstance in React but technically in Fabric they're not the same. So we need to translate between PublicInstance and HostInstance. I just hardcoded the Fabric implementation of this since it's the only known one that does this but could feature detect other ones too if necessary. On one hand it's more resilient to refactors to not rely on injected helpers and on hand it doesn't follow changes to things like this. For the conflict resolution I added in #30494 I had to make that specific to DOM so we can move the DOM traversal to the backend instead of the injected helper. [ghstack-poisoned]
1 parent 02732c1 commit e2c0670

File tree

6 files changed

+280
-218
lines changed

6 files changed

+280
-218
lines changed

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

+102-63
Original file line numberDiff line numberDiff line change
@@ -342,84 +342,123 @@ export default class Agent extends EventEmitter<{
342342
}
343343

344344
getIDForHostInstance(target: HostInstance): number | null {
345-
let bestMatch: null | HostInstance = null;
346-
let bestRenderer: null | RendererInterface = null;
347-
// Find the nearest ancestor which is mounted by a React.
348-
for (const rendererID in this._rendererInterfaces) {
349-
const renderer = ((this._rendererInterfaces[
350-
(rendererID: any)
351-
]: any): RendererInterface);
352-
const nearestNode: null = renderer.getNearestMountedHostInstance(target);
353-
if (nearestNode !== null) {
354-
if (nearestNode === target) {
355-
// Exact match we can exit early.
356-
bestMatch = nearestNode;
357-
bestRenderer = renderer;
358-
break;
345+
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
346+
// In React Native or non-DOM we simply pick any renderer that has a match.
347+
for (const rendererID in this._rendererInterfaces) {
348+
const renderer = ((this._rendererInterfaces[
349+
(rendererID: any)
350+
]: any): RendererInterface);
351+
try {
352+
const match = renderer.getElementIDForHostInstance(target);
353+
if (match != null) {
354+
return match;
355+
}
356+
} catch (error) {
357+
// Some old React versions might throw if they can't find a match.
358+
// If so we should ignore it...
359359
}
360-
if (
361-
bestMatch === null ||
362-
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
363-
) {
364-
// If this is the first match or the previous match contains the new match,
365-
// so the new match is a deeper and therefore better match.
366-
bestMatch = nearestNode;
367-
bestRenderer = renderer;
360+
}
361+
return null;
362+
} else {
363+
// In the DOM we use a smarter mechanism to find the deepest a DOM node
364+
// that is registered if there isn't an exact match.
365+
let bestMatch: null | Element = null;
366+
let bestRenderer: null | RendererInterface = null;
367+
// Find the nearest ancestor which is mounted by a React.
368+
for (const rendererID in this._rendererInterfaces) {
369+
const renderer = ((this._rendererInterfaces[
370+
(rendererID: any)
371+
]: any): RendererInterface);
372+
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
373+
(target: any),
374+
);
375+
if (nearestNode !== null) {
376+
if (nearestNode === target) {
377+
// Exact match we can exit early.
378+
bestMatch = nearestNode;
379+
bestRenderer = renderer;
380+
break;
381+
}
382+
if (bestMatch === null || bestMatch.contains(nearestNode)) {
383+
// If this is the first match or the previous match contains the new match,
384+
// so the new match is a deeper and therefore better match.
385+
bestMatch = nearestNode;
386+
bestRenderer = renderer;
387+
}
368388
}
369389
}
370-
}
371-
if (bestRenderer != null && bestMatch != null) {
372-
try {
373-
return bestRenderer.getElementIDForHostInstance(bestMatch, true);
374-
} catch (error) {
375-
// Some old React versions might throw if they can't find a match.
376-
// If so we should ignore it...
390+
if (bestRenderer != null && bestMatch != null) {
391+
try {
392+
return bestRenderer.getElementIDForHostInstance(bestMatch);
393+
} catch (error) {
394+
// Some old React versions might throw if they can't find a match.
395+
// If so we should ignore it...
396+
}
377397
}
398+
return null;
378399
}
379-
return null;
380400
}
381401

382402
getComponentNameForHostInstance(target: HostInstance): string | null {
383403
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
384-
let bestMatch: null | HostInstance = null;
385-
let bestRenderer: null | RendererInterface = null;
386-
// Find the nearest ancestor which is mounted by a React.
387-
for (const rendererID in this._rendererInterfaces) {
388-
const renderer = ((this._rendererInterfaces[
389-
(rendererID: any)
390-
]: any): RendererInterface);
391-
const nearestNode = renderer.getNearestMountedHostInstance(target);
392-
if (nearestNode !== null) {
393-
if (nearestNode === target) {
394-
// Exact match we can exit early.
395-
bestMatch = nearestNode;
396-
bestRenderer = renderer;
397-
break;
404+
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
405+
// In React Native or non-DOM we simply pick any renderer that has a match.
406+
for (const rendererID in this._rendererInterfaces) {
407+
const renderer = ((this._rendererInterfaces[
408+
(rendererID: any)
409+
]: any): RendererInterface);
410+
try {
411+
const id = renderer.getElementIDForHostInstance(target);
412+
if (id) {
413+
return renderer.getDisplayNameForElementID(id);
414+
}
415+
} catch (error) {
416+
// Some old React versions might throw if they can't find a match.
417+
// If so we should ignore it...
398418
}
399-
if (
400-
bestMatch === null ||
401-
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
402-
) {
403-
// If this is the first match or the previous match contains the new match,
404-
// so the new match is a deeper and therefore better match.
405-
bestMatch = nearestNode;
406-
bestRenderer = renderer;
419+
}
420+
return null;
421+
} else {
422+
// In the DOM we use a smarter mechanism to find the deepest a DOM node
423+
// that is registered if there isn't an exact match.
424+
let bestMatch: null | Element = null;
425+
let bestRenderer: null | RendererInterface = null;
426+
// Find the nearest ancestor which is mounted by a React.
427+
for (const rendererID in this._rendererInterfaces) {
428+
const renderer = ((this._rendererInterfaces[
429+
(rendererID: any)
430+
]: any): RendererInterface);
431+
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
432+
(target: any),
433+
);
434+
if (nearestNode !== null) {
435+
if (nearestNode === target) {
436+
// Exact match we can exit early.
437+
bestMatch = nearestNode;
438+
bestRenderer = renderer;
439+
break;
440+
}
441+
if (bestMatch === null || bestMatch.contains(nearestNode)) {
442+
// If this is the first match or the previous match contains the new match,
443+
// so the new match is a deeper and therefore better match.
444+
bestMatch = nearestNode;
445+
bestRenderer = renderer;
446+
}
407447
}
408448
}
409-
}
410-
411-
if (bestRenderer != null && bestMatch != null) {
412-
try {
413-
const id = bestRenderer.getElementIDForHostInstance(bestMatch, true);
414-
if (id) {
415-
return bestRenderer.getDisplayNameForElementID(id);
449+
if (bestRenderer != null && bestMatch != null) {
450+
try {
451+
const id = bestRenderer.getElementIDForHostInstance(bestMatch);
452+
if (id) {
453+
return bestRenderer.getDisplayNameForElementID(id);
454+
}
455+
} catch (error) {
456+
// Some old React versions might throw if they can't find a match.
457+
// If so we should ignore it...
416458
}
417-
} catch (error) {
418-
// Some old React versions might throw if they can't find a match.
419-
// If so we should ignore it...
420459
}
460+
return null;
421461
}
422-
return null;
423462
}
424463

425464
getBackendVersion: () => void = () => {

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

+1-11
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,7 @@ export function registerRenderer(
135135
renderer: ReactRenderer,
136136
onErrorOrWarning?: OnErrorOrWarning,
137137
): void {
138-
const {
139-
currentDispatcherRef,
140-
getCurrentFiber,
141-
findFiberByHostInstance,
142-
version,
143-
} = renderer;
144-
145-
// Ignore React v15 and older because they don't expose a component stack anyway.
146-
if (typeof findFiberByHostInstance !== 'function') {
147-
return;
148-
}
138+
const {currentDispatcherRef, getCurrentFiber, version} = renderer;
149139

150140
// currentDispatcherRef gets injected for v16.8+ to support hooks inspection.
151141
// getCurrentFiber gets injected for v16.9+.

0 commit comments

Comments
 (0)