Repository: facebook/react
Pull request: [DevTools] Allow Highlighting/Inspect HostSingletons/Hoistables and Resources

Basically the new Float types needs to be supported. Resources are a bit special because they're a DOM specific type but we can expect any other implementation using resources to provide and instance on this field if needed.

There's a slightly related case for the reverse lookup. 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. We could in principle just search the tree for the first one or keep a map of currently mounted resources and just pick the first fiber that created it. So that you can select a resource and see what created it. Particularly useful when there's only one Fiber which is most of the time.


Changed files:
filename: packages/react-devtools-shared/src/backend/fiber/renderer.js
patch: @@ -2695,11 +2695,11 @@ export function attach(
           // If we're tracing updates and we've bailed out before reaching a host node,
           // we should fall back to recursively marking the nearest host descendants for highlight.
           if (traceNearestHostComponentUpdate) {
-            const hostFibers = findAllCurrentHostFibers(
+            const hostInstances = findAllCurrentHostInstances(
               getFiberInstanceThrows(nextFiber),
             );
-            hostFibers.forEach(hostFiber => {
-              traceUpdatesForNodes.add(hostFiber.stateNode);
+            hostInstances.forEach(hostInstance => {
+              traceUpdatesForNodes.add(hostInstance);
             });
           }
         }
@@ -2943,31 +2943,54 @@ export function attach(
     currentRootID = -1;
   }
 
-  function findAllCurrentHostFibers(
+  function getResourceInstance(fiber: Fiber): HostInstance | null {
+    if (fiber.tag === HostHoistable) {
+      const resource = fiber.memoizedState;
+      // Feature Detect a DOM Specific Instance of a Resource
+      if (
+        typeof resource === 'object' &&
+        resource !== null &&
+        resource.instance != null
+      ) {
+        return resource.instance;
+      }
+    }
+    return null;
+  }
+
+  function findAllCurrentHostInstances(
     fiberInstance: FiberInstance,
-  ): $ReadOnlyArray<Fiber> {
-    const fibers = [];
+  ): $ReadOnlyArray<HostInstance> {
+    const hostInstances = [];
     const fiber = findCurrentFiberUsingSlowPathByFiberInstance(fiberInstance);
     if (!fiber) {
-      return fibers;
+      return hostInstances;
     }
 
     // Next we'll drill down this component to find all HostComponent/Text.
     let node: Fiber = fiber;
     while (true) {
-      if (node.tag === HostComponent || node.tag === HostText) {
-        fibers.push(node);
+      if (
+        node.tag === HostComponent ||
+        node.tag === HostText ||
+        node.tag === HostSingleton ||
+        node.tag === HostHoistable
+      ) {
+        const hostInstance = node.stateNode || getResourceInstance(node);
+        if (hostInstance) {
+          hostInstances.push(hostInstance);
+        }
       } else if (node.child) {
         node.child.return = node;
         node = node.child;
         continue;
       }
       if (node === fiber) {
-        return fibers;
+        return hostInstances;
       }
       while (!node.sibling) {
         if (!node.return || node.return === fiber) {
-          return fibers;
+          return hostInstances;
         }
         node = node.return;
       }
@@ -2976,7 +2999,7 @@ export function attach(
     }
     // Flow needs the return here, but ESLint complains about it.
     // eslint-disable-next-line no-unreachable
-    return fibers;
+    return hostInstances;
   }
 
   function findHostInstancesForElementID(id: number) {
@@ -2996,8 +3019,8 @@ export function attach(
         return null;
       }
 
-      const hostFibers = findAllCurrentHostFibers(devtoolsInstance);
-      return hostFibers.map(hostFiber => hostFiber.stateNode).filter(Boolean);
+      const hostInstances = findAllCurrentHostInstances(devtoolsInstance);
+      return hostInstances;
     } catch (err) {
       // The fiber might have unmounted by now.
       return null;

filename: packages/react-devtools-shared/src/backend/types.js
patch: @@ -94,7 +94,7 @@ export type GetElementIDForHostInstance = (
 ) => number | null;
 export type FindHostInstancesForElementID = (
   id: number,
-) => ?Array<HostInstance>;
+) => null | $ReadOnlyArray<HostInstance>;
 
 export type ReactProviderType<T> = {
   $$typeof: symbol | number,

filename: packages/react-devtools-shared/src/backend/views/Highlighter/Highlighter.js
patch: @@ -38,12 +38,15 @@ export function hideOverlay(agent: Agent): void {
     : hideOverlayWeb();
 }
 
-function showOverlayNative(elements: Array<HostInstance>, agent: Agent): void {
+function showOverlayNative(
+  elements: $ReadOnlyArray<HostInstance>,
+  agent: Agent,
+): void {
   agent.emit('showNativeHighlight', elements);
 }
 
 function showOverlayWeb(
-  elements: Array<HTMLElement>,
+  elements: $ReadOnlyArray<HTMLElement>,
   componentName: string | null,
   agent: Agent,
   hideAfterTimeout: boolean,
@@ -64,12 +67,17 @@ function showOverlayWeb(
 }
 
 export function showOverlay(
-  elements: Array<HostInstance>,
+  elements: $ReadOnlyArray<HostInstance>,
   componentName: string | null,
   agent: Agent,
   hideAfterTimeout: boolean,
 ): void {
   return isReactNativeEnvironment()
     ? showOverlayNative(elements, agent)
-    : showOverlayWeb((elements: any), componentName, agent, hideAfterTimeout);
+    : showOverlayWeb(
+        (elements: $ReadOnlyArray<any>),
+        componentName,
+        agent,
+        hideAfterTimeout,
+      );
 }

filename: packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js
patch: @@ -187,7 +187,7 @@ export default class Overlay {
     }
   }
 
-  inspect(nodes: Array<HTMLElement>, name?: ?string) {
+  inspect(nodes: $ReadOnlyArray<HTMLElement>, name?: ?string) {
     // We can't get the size of text nodes or comment nodes. React as of v15
     // heavily uses comment nodes to delimit text.
     const elements = nodes.filter(node => node.nodeType === Node.ELEMENT_NODE);

filename: packages/react-devtools-shared/src/backend/views/Highlighter/index.js
patch: @@ -13,7 +13,6 @@ import Agent from 'react-devtools-shared/src/backend/agent';
 import {hideOverlay, showOverlay} from './Highlighter';
 
 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
-import type {HostInstance} from '../../types';
 
 // This plug-in provides in-page highlighting of the selected element.
 // It is used by the browser extension and the standalone DevTools shell (when connected to a browser).
@@ -113,8 +112,7 @@ export default function setupHighlighter(
       return;
     }
 
-    const nodes: ?Array<HostInstance> =
-      renderer.findHostInstancesForElementID(id);
+    const nodes = renderer.findHostInstancesForElementID(id);
 
     if (nodes != null && nodes[0] != null) {
       const node = nodes[0];