@samitouri / QOS-React / commits / ed94ea146a

[DevTools] Allow Highlighting/Inspect HostSingletons/Hoistables and Resources (#30584)

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. --------- Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>

Sebastian Markbåge committed Aug 2, 2024 at 17:04 UTC ed94ea146a111124711910c87af44d7760bfd409
5 files changed +52 -23
packages/react-devtools-shared/src/backend/fiber/renderer.js
+37 -14
@@ -2695,11 +2695,11 @@ export function attach(
2695 // If we're tracing updates and we've bailed out before reaching a host node,
2696 // we should fall back to recursively marking the nearest host descendants for highlight.
2697 if (traceNearestHostComponentUpdate) {
2698 - const hostFibers = findAllCurrentHostFibers(
2698 + const hostInstances = findAllCurrentHostInstances(
2699 getFiberInstanceThrows(nextFiber),
2700 );
2701 - hostFibers.forEach(hostFiber => {
2702 - traceUpdatesForNodes.add(hostFiber.stateNode);
2701 + hostInstances.forEach(hostInstance => {
2702 + traceUpdatesForNodes.add(hostInstance);
2703 });
2704 }
2705 }
@@ -2943,31 +2943,54 @@ export function attach(
2943 currentRootID = -1;
2944 }
2945
2946 - function findAllCurrentHostFibers(
2946 + function getResourceInstance(fiber: Fiber): HostInstance | null {
2947 + if (fiber.tag === HostHoistable) {
2948 + const resource = fiber.memoizedState;
2949 + // Feature Detect a DOM Specific Instance of a Resource
2950 + if (
2951 + typeof resource === 'object' &&
2952 + resource !== null &&
2953 + resource.instance != null
2954 + ) {
2955 + return resource.instance;
2956 + }
2957 + }
2958 + return null;
2959 + }
2960 +
2961 + function findAllCurrentHostInstances(
2962 fiberInstance: FiberInstance,
2948 - ): $ReadOnlyArray<Fiber> {
2949 - const fibers = [];
2963 + ): $ReadOnlyArray<HostInstance> {
2964 + const hostInstances = [];
2965 const fiber = findCurrentFiberUsingSlowPathByFiberInstance(fiberInstance);
2966 if (!fiber) {
2952 - return fibers;
2967 + return hostInstances;
2968 }
2969
2970 // Next we'll drill down this component to find all HostComponent/Text.
2971 let node: Fiber = fiber;
2972 while (true) {
2958 - if (node.tag === HostComponent || node.tag === HostText) {
2959 - fibers.push(node);
2973 + if (
2974 + node.tag === HostComponent ||
2975 + node.tag === HostText ||
2976 + node.tag === HostSingleton ||
2977 + node.tag === HostHoistable
2978 + ) {
2979 + const hostInstance = node.stateNode || getResourceInstance(node);
2980 + if (hostInstance) {
2981 + hostInstances.push(hostInstance);
2982 + }
2983 } else if (node.child) {
2984 node.child.return = node;
2985 node = node.child;
2986 continue;
2987 }
2988 if (node === fiber) {
2966 - return fibers;
2989 + return hostInstances;
2990 }
2991 while (!node.sibling) {
2992 if (!node.return || node.return === fiber) {
2970 - return fibers;
2993 + return hostInstances;
2994 }
2995 node = node.return;
2996 }
@@ -2976,7 +2999,7 @@ export function attach(
2999 }
3000 // Flow needs the return here, but ESLint complains about it.
3001 // eslint-disable-next-line no-unreachable
2979 - return fibers;
3002 + return hostInstances;
3003 }
3004
3005 function findHostInstancesForElementID(id: number) {
@@ -2996,8 +3019,8 @@ export function attach(
3019 return null;
3020 }
3021
2999 - const hostFibers = findAllCurrentHostFibers(devtoolsInstance);
3000 - return hostFibers.map(hostFiber => hostFiber.stateNode).filter(Boolean);
3022 + const hostInstances = findAllCurrentHostInstances(devtoolsInstance);
3023 + return hostInstances;
3024 } catch (err) {
3025 // The fiber might have unmounted by now.
3026 return null;
packages/react-devtools-shared/src/backend/types.js
+1 -1
@@ -94,7 +94,7 @@ export type GetElementIDForHostInstance = (
94 ) => number | null;
95 export type FindHostInstancesForElementID = (
96 id: number,
97 -) => ?Array<HostInstance>;
97 +) => null | $ReadOnlyArray<HostInstance>;
98
99 export type ReactProviderType<T> = {
100 $$typeof: symbol | number,
packages/react-devtools-shared/src/backend/views/Highlighter/Highlighter.js
+12 -4
@@ -38,12 +38,15 @@ export function hideOverlay(agent: Agent): void {
38 : hideOverlayWeb();
39 }
40
41 -function showOverlayNative(elements: Array<HostInstance>, agent: Agent): void {
41 +function showOverlayNative(
42 + elements: $ReadOnlyArray<HostInstance>,
43 + agent: Agent,
44 +): void {
45 agent.emit('showNativeHighlight', elements);
46 }
47
48 function showOverlayWeb(
46 - elements: Array<HTMLElement>,
49 + elements: $ReadOnlyArray<HTMLElement>,
50 componentName: string | null,
51 agent: Agent,
52 hideAfterTimeout: boolean,
@@ -64,12 +67,17 @@ function showOverlayWeb(
67 }
68
69 export function showOverlay(
67 - elements: Array<HostInstance>,
70 + elements: $ReadOnlyArray<HostInstance>,
71 componentName: string | null,
72 agent: Agent,
73 hideAfterTimeout: boolean,
74 ): void {
75 return isReactNativeEnvironment()
76 ? showOverlayNative(elements, agent)
74 - : showOverlayWeb((elements: any), componentName, agent, hideAfterTimeout);
77 + : showOverlayWeb(
78 + (elements: $ReadOnlyArray<any>),
79 + componentName,
80 + agent,
81 + hideAfterTimeout,
82 + );
83 }
packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js
+1 -1
@@ -187,7 +187,7 @@ export default class Overlay {
187 }
188 }
189
190 - inspect(nodes: Array<HTMLElement>, name?: ?string) {
190 + inspect(nodes: $ReadOnlyArray<HTMLElement>, name?: ?string) {
191 // We can't get the size of text nodes or comment nodes. React as of v15
192 // heavily uses comment nodes to delimit text.
193 const elements = nodes.filter(node => node.nodeType === Node.ELEMENT_NODE);
packages/react-devtools-shared/src/backend/views/Highlighter/index.js
+1 -3
@@ -13,7 +13,6 @@ import Agent from 'react-devtools-shared/src/backend/agent';
13 import {hideOverlay, showOverlay} from './Highlighter';
14
15 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
16 -import type {HostInstance} from '../../types';
16
17 // This plug-in provides in-page highlighting of the selected element.
18 // 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(
112 return;
113 }
114
116 - const nodes: ?Array<HostInstance> =
117 - renderer.findHostInstancesForElementID(id);
115 + const nodes = renderer.findHostInstancesForElementID(id);
116
117 if (nodes != null && nodes[0] != null) {
118 const node = nodes[0];