@samitouri / QOS-React-2 / commits / e27313127e

[react-devtools-facade] add host instance component lookup (#36820)

Adds a new tool to the Facade, where the input is the `HostInstance` and the output is the corresponding component payload for the `HostFiber` of this `HostInstance`. Supports multiple Host objects from Fiber: `HostComponent`, `HostText`, `HostSingleton`, `HostHoistable`. > [!NOTE] > This doesn't bring much value now, because this returns the payload for the corresponding HostFiber, not the closest user-defined Fiber. This will be improved by adding a parentUid reference to the element payload, so the agent can crawl up through the component tree.

Ruslan Lesiutin committed Jun 30, 2026 at 20:33 UTC e27313127e4b138bed374b2a931598ff42275620
3 files changed +277 -36
packages/react-devtools-facade/src/DevToolsFacadeTools.js
+2
@@ -59,6 +59,7 @@ export type Tools = {
59 uid: string,
60 includeHooks?: boolean,
61 ) => NodeInfo | ToolError,
62 + getComponentByHostInstance: (hostInstance: mixed) => NodeInfo | ToolError,
63 findComponents: (
64 name: string,
65 rootUid?: string,
@@ -97,6 +98,7 @@ export function createTools(facade: Facade): Tools {
98 return {
99 getComponentTree: tree.getComponentTree,
100 getComponentByUid: tree.getComponentByUid,
101 + getComponentByHostInstance: tree.getComponentByHostInstance,
102 findComponents: tree.findComponents,
103 getComponentSource: tree.getComponentSource,
104 getOwnerStackTrace: tree.getOwnerStackTrace,
packages/react-devtools-facade/src/DevToolsFacadeTreeTools.js
+143 -36
@@ -85,6 +85,7 @@ export type TreeTools = {
85 uid: string,
86 includeHooks?: boolean,
87 ) => NodeInfo | ToolError,
88 + getComponentByHostInstance: (hostInstance: mixed) => NodeInfo | ToolError,
89 findComponents: (
90 name: string,
91 rootUid?: string,
@@ -377,6 +378,108 @@ export function createTreeTools(
378 };
379 }
380
381 + function getHostInstanceForFiber(
382 + internals: RendererInternals,
383 + fiber: Fiber,
384 + ): mixed {
385 + const {HostComponent, HostText, HostSingleton, HostHoistable} =
386 + internals.ReactTypeOfWork;
387 +
388 + if (
389 + fiber.tag === HostComponent ||
390 + fiber.tag === HostText ||
391 + fiber.tag === HostSingleton
392 + ) {
393 + return fiber.stateNode;
394 + }
395 +
396 + if (fiber.tag === HostHoistable) {
397 + const resource = fiber.memoizedState;
398 + if (
399 + resource != null &&
400 + typeof resource === 'object' &&
401 + (resource as any).instance != null
402 + ) {
403 + return (resource as any).instance;
404 + }
405 + }
406 +
407 + return null;
408 + }
409 +
410 + function findByHostInstance(
411 + internals: RendererInternals,
412 + root: Fiber,
413 + hostInstance: mixed,
414 + ): Fiber | null {
415 + let current: Fiber | null = root;
416 + while (current !== null) {
417 + if (getHostInstanceForFiber(internals, current) === hostInstance) {
418 + return current;
419 + }
420 +
421 + if (current.child !== null) {
422 + current = current.child;
423 + continue;
424 + }
425 +
426 + while (current !== null && current !== root && current.sibling === null) {
427 + current = current.return;
428 + }
429 + if (current === null || current === root) {
430 + return null;
431 + }
432 + current = current.sibling;
433 + }
434 + return null;
435 + }
436 +
437 + function buildNodeInfo(
438 + fiber: Fiber,
439 + internals: RendererInternals,
440 + includeHooks?: boolean = false,
441 + ): NodeInfo | ToolError {
442 + const info: NodeInfo = {
443 + uid: getUid(fiber),
444 + type: getTypeTagForFiber(internals, fiber),
445 + name: getDisplayName(internals, fiber),
446 + };
447 + if (fiber.key != null) {
448 + info.key = String(fiber.key);
449 + }
450 + const props = normalizeProps(fiber.memoizedProps);
451 + if (props != null) {
452 + info.props = props;
453 + }
454 + if (includeHooks) {
455 + // Hooks are only inspectable for function components, forwardRef, and
456 + // simple-memo components. inspectHooksOfFiberWithoutDefaultDispatcher
457 + // re-renders the component (using the renderer's injected dispatcher,
458 + // never React's shared internals), so guard by tag and tolerate failures
459 + // (e.g. a component that throws).
460 + const {FunctionComponent, SimpleMemoComponent, ForwardRef} =
461 + internals.ReactTypeOfWork;
462 + if (
463 + fiber.tag === FunctionComponent ||
464 + fiber.tag === SimpleMemoComponent ||
465 + fiber.tag === ForwardRef
466 + ) {
467 + try {
468 + const hooksTree = inspectHooksOfFiberWithoutDefaultDispatcher(
469 + fiber,
470 + getDispatcherRef(internals),
471 + );
472 + info.hooks = normalizeHooks(hooksTree);
473 + } catch (error) {
474 + return {
475 + error: new Error('Failed to inspect hooks.', {cause: error}),
476 + };
477 + }
478 + }
479 + }
480 + return info;
481 + }
482 +
483 /**
484 * Returns a snapshot of the component tree as an array of nodes. Each node
485 * includes: uid, type, name, key, firstChild, nextSibling (the last two
@@ -436,46 +539,49 @@ export function createTreeTools(
539 if (result.error != null) {
540 return {error: result.error};
541 }
439 - const {fiber, internals} = result;
440 - const info: NodeInfo = {
441 - uid: getUid(fiber),
442 - type: getTypeTagForFiber(internals, fiber),
443 - name: getDisplayName(internals, fiber),
444 - };
445 - if (fiber.key != null) {
446 - info.key = String(fiber.key);
447 - }
448 - const props = normalizeProps(fiber.memoizedProps);
449 - if (props != null) {
450 - info.props = props;
542 + return buildNodeInfo(result.fiber, result.internals, includeHooks);
543 + }
544 +
545 + /**
546 + * Returns detailed info about the React host component for a host instance
547 + * reference. The reference is opaque: for react-dom it may be a DOM
548 + * Element/Text, but the facade only compares it by identity with host fiber
549 + * state. It does not read platform-specific fields or walk host parents.
550 + *
551 + * @param hostInstance - A renderer host instance reference.
552 + */
553 + function getComponentByHostInstance(
554 + hostInstance: mixed,
555 + ): NodeInfo | ToolError {
556 + if (hostInstance == null) {
557 + return {error: 'Host instance is required'};
558 }
452 - if (includeHooks) {
453 - // Hooks are only inspectable for function components, forwardRef, and
454 - // simple-memo components. inspectHooksOfFiberWithoutDefaultDispatcher
455 - // re-renders the component (using the renderer's injected dispatcher,
456 - // never React's shared internals), so guard by tag and tolerate failures
457 - // (e.g. a component that throws).
458 - const {FunctionComponent, SimpleMemoComponent, ForwardRef} =
459 - internals.ReactTypeOfWork;
460 - if (
461 - fiber.tag === FunctionComponent ||
462 - fiber.tag === SimpleMemoComponent ||
463 - fiber.tag === ForwardRef
464 - ) {
465 - try {
466 - const hooksTree = inspectHooksOfFiberWithoutDefaultDispatcher(
467 - fiber,
468 - getDispatcherRef(internals),
469 - );
470 - info.hooks = normalizeHooks(hooksTree);
471 - } catch (error) {
472 - return {
473 - error: new Error('Failed to inspect hooks.', {cause: error}),
474 - };
559 +
560 + let sawRoot = false;
561 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
562 + for (const [rendererID, roots] of fiberRoots) {
563 + const internals = rendererInternals.get(rendererID);
564 + if (internals == null) {
565 + return {error: 'Missing internals for renderer ' + rendererID};
566 + }
567 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
568 + for (const root of roots) {
569 + sawRoot = true;
570 + const hostFiber = findByHostInstance(
571 + internals,
572 + root.current,
573 + hostInstance,
574 + );
575 + if (hostFiber !== null) {
576 + return buildNodeInfo(hostFiber, internals);
577 }
578 }
579 }
478 - return info;
580 +
581 + if (!sawRoot) {
582 + return {error: 'No mounted React roots found'};
583 + }
584 + return {error: 'Host instance is not managed by React'};
585 }
586
587 function collectMatches(
@@ -677,6 +783,7 @@ export function createTreeTools(
783 return {
784 getComponentTree,
785 getComponentByUid,
786 + getComponentByHostInstance,
787 findComponents,
788 getComponentSource,
789 getOwnerStackTrace,
packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js
+132
@@ -1634,6 +1634,138 @@ describe('react-devtools-facade', () => {
1634 });
1635 });
1636
1637 + describe('getComponentByHostInstance', () => {
1638 + let getComponentTree;
1639 + let getComponentByUid;
1640 + let getComponentByHostInstance;
1641 +
1642 + beforeEach(() => {
1643 + const tools = createTools(facade);
1644 + getComponentTree = tools.getComponentTree;
1645 + getComponentByUid = tools.getComponentByUid;
1646 + getComponentByHostInstance = tools.getComponentByHostInstance;
1647 + });
1648 +
1649 + it('returns the host component for a DOM host element', () => {
1650 + function Child({label}) {
1651 + return <span className="leaf">{label}</span>;
1652 + }
1653 + function App() {
1654 + return (
1655 + <div>
1656 + <Child label="leaf" />
1657 + </div>
1658 + );
1659 + }
1660 +
1661 + act(() => {
1662 + ReactDOMClient.createRoot(container).render(<App />);
1663 + });
1664 +
1665 + const span = container.querySelector('span.leaf');
1666 + const host = getComponentTree().find(n => n.name === 'span');
1667 + const result = getComponentByHostInstance(span);
1668 +
1669 + expect(result).toEqual(getComponentByUid(host.uid));
1670 + expect(result).toMatchObject({
1671 + uid: host.uid,
1672 + type: 'host',
1673 + name: 'span',
1674 + props: {className: 'leaf'},
1675 + });
1676 + });
1677 +
1678 + it('returns the host component rather than the tree owner', () => {
1679 + function Wrapper({children}) {
1680 + return <section className="wrap">{children}</section>;
1681 + }
1682 + function App() {
1683 + return (
1684 + <Wrapper>
1685 + <button className="action">Run</button>
1686 + </Wrapper>
1687 + );
1688 + }
1689 +
1690 + act(() => {
1691 + ReactDOMClient.createRoot(container).render(<App />);
1692 + });
1693 +
1694 + const button = container.querySelector('button.action');
1695 + const tree = getComponentTree();
1696 + const host = tree.find(n => n.name === 'button');
1697 + const wrapper = tree.find(n => n.name === 'Wrapper');
1698 + const app = tree.find(n => n.name === 'App');
1699 + const result = getComponentByHostInstance(button);
1700 +
1701 + expect(result.uid).toBe(host.uid);
1702 + expect(result.uid).not.toBe(wrapper.uid);
1703 + expect(result.uid).not.toBe(app.uid);
1704 + expect(result).toMatchObject({
1705 + type: 'host',
1706 + name: 'button',
1707 + props: {className: 'action'},
1708 + });
1709 + });
1710 +
1711 + it('keeps uids stable across re-renders via alternate fibers', () => {
1712 + function Counter({count}) {
1713 + return <div className="counter">{'Count: ' + count}</div>;
1714 + }
1715 +
1716 + const root = ReactDOMClient.createRoot(container);
1717 + act(() => {
1718 + root.render(<Counter count={0} />);
1719 + });
1720 +
1721 + const div = container.querySelector('div.counter');
1722 + const first = getComponentByHostInstance(div);
1723 +
1724 + act(() => {
1725 + root.render(<Counter count={1} />);
1726 + });
1727 +
1728 + const second = getComponentByHostInstance(div);
1729 + expect(second.uid).toBe(first.uid);
1730 + expect(second.name).toBe('div');
1731 + expect(second.type).toBe('host');
1732 + expect(second.props.className).toBe('counter');
1733 + });
1734 +
1735 + it('does not walk platform parent pointers for unmanaged nested nodes', () => {
1736 + function App() {
1737 + return <div className="host" />;
1738 + }
1739 +
1740 + act(() => {
1741 + ReactDOMClient.createRoot(container).render(<App />);
1742 + });
1743 +
1744 + const host = container.querySelector('div.host');
1745 + const unmanagedChild = document.createElement('i');
1746 + host.appendChild(unmanagedChild);
1747 +
1748 + expect(getComponentByHostInstance(unmanagedChild)).toEqual({
1749 + error: 'Host instance is not managed by React',
1750 + });
1751 + });
1752 +
1753 + it('returns an error when no roots are mounted', () => {
1754 + expect(getComponentByHostInstance({})).toEqual({
1755 + error: 'No mounted React roots found',
1756 + });
1757 + });
1758 +
1759 + it('returns an error for null or undefined references', () => {
1760 + expect(getComponentByHostInstance(null)).toEqual({
1761 + error: 'Host instance is required',
1762 + });
1763 + expect(getComponentByHostInstance(undefined)).toEqual({
1764 + error: 'Host instance is required',
1765 + });
1766 + });
1767 + });
1768 +
1769 describe('profiler', () => {
1770 let startProfiling;
1771 let stopProfiling;