@samitouri / QOS-React-1 / commits / d4e44545a9

[react-devtools-facade] Add includeHooks option to component lookup (#36874)

The tool now has a boolean argument which controls the presence of hook tree in the return payload. If hooks were requested, and the inspection fails, the error payload will be returned instead.

Ruslan Lesiutin committed Jun 29, 2026 at 23:29 UTC d4e44545a984186fc97783e130896e9c2e5c1a63
3 files changed +102 -34
packages/react-devtools-facade/src/DevToolsFacadeTools.js
+4 -1
@@ -55,7 +55,10 @@ export type Tools = {
55 depth?: number,
56 rootUid?: string,
57 ) => Array<TreeNode> | ToolError,
58 - getComponentByUid: (uid: string) => NodeInfo | ToolError,
58 + getComponentByUid: (
59 + uid: string,
60 + includeHooks?: boolean,
61 + ) => NodeInfo | ToolError,
62 findComponents: (
63 name: string,
64 rootUid?: string,
packages/react-devtools-facade/src/DevToolsFacadeTreeTools.js
+40 -28
@@ -24,7 +24,7 @@ import type {RendererInternals} from './DevToolsFacade';
24 // (to TOON, JSON, etc.) is the integrator's responsibility.
25
26 // Returned by any tool when the requested component/root cannot be resolved.
27 -export type ToolError = {error: string};
27 +export type ToolError = {error: string | Error};
28
29 // A single component in a tree snapshot. firstChild/nextSibling reference other
30 // nodes by their uid, forming an adjacency list the integrator can rebuild.
@@ -81,7 +81,10 @@ export type TreeTools = {
81 depth?: number,
82 rootUid?: string,
83 ) => Array<TreeNode> | ToolError,
84 - getComponentByUid: (uid: string) => NodeInfo | ToolError,
84 + getComponentByUid: (
85 + uid: string,
86 + includeHooks?: boolean,
87 + ) => NodeInfo | ToolError,
88 findComponents: (
89 name: string,
90 rootUid?: string,
@@ -414,16 +417,21 @@ export function createTreeTools(
417 }
418
419 /**
417 - * Returns detailed info about a single component by its uid: type, name,
418 - * key, props (excluding children), and — for function components — the
419 - * inspected hooks tree. Values are normalized to a serialization-safe shape.
420 + * Returns detailed info about a single component by its uid: type, name, key,
421 + * and props (excluding children). Values are normalized to a serialization-safe
422 + * shape.
423 *
421 - * Inspecting hooks re-renders the component's render function (effects are
422 - * not run); failures are tolerated and simply omit `hooks`.
424 + * If includeHooks is true, function components also include the inspected
425 + * hooks tree. Inspecting hooks re-renders the component's render function
426 + * (effects are not run); failures return an error payload.
427 *
428 * @param uid - The component uid (e.g. "r5").
429 + * @param includeHooks - Whether to inspect hooks for function components.
430 */
426 - function getComponentByUid(uid: string): NodeInfo | ToolError {
431 + function getComponentByUid(
432 + uid: string,
433 + includeHooks?: boolean = false,
434 + ): NodeInfo | ToolError {
435 const result = findFiberByUid(uid);
436 if (result.error != null) {
437 return {error: result.error};
@@ -441,26 +449,30 @@ export function createTreeTools(
449 if (props != null) {
450 info.props = props;
451 }
444 - // Hooks are only inspectable for function components, forwardRef, and
445 - // simple-memo components. inspectHooksOfFiberWithoutDefaultDispatcher
446 - // re-renders the component (using the renderer's injected dispatcher, never
447 - // React's shared internals), so guard by tag and tolerate failures (e.g. a
448 - // component that throws).
449 - const {FunctionComponent, SimpleMemoComponent, ForwardRef} =
450 - internals.ReactTypeOfWork;
451 - if (
452 - fiber.tag === FunctionComponent ||
453 - fiber.tag === SimpleMemoComponent ||
454 - fiber.tag === ForwardRef
455 - ) {
456 - try {
457 - const hooksTree = inspectHooksOfFiberWithoutDefaultDispatcher(
458 - fiber,
459 - getDispatcherRef(internals),
460 - );
461 - info.hooks = normalizeHooks(hooksTree);
462 - } catch {
463 - // Hook inspection failed; omit hooks rather than failing the call.
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 + };
475 + }
476 }
477 }
478 return info;
packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js
+58 -5
@@ -47,6 +47,7 @@ describe('react-devtools-facade', () => {
47 });
48
49 afterEach(() => {
50 + jest.dontMock('react-debug-tools');
51 container = null;
52 });
53
@@ -1481,7 +1482,7 @@ describe('react-devtools-facade', () => {
1482 });
1483
1484 const widget = getComponentTree().find(n => n.name === 'Widget');
1484 - const info = getComponentByUid(widget.uid);
1485 + const info = getComponentByUid(widget.uid, true);
1486
1487 // Full structural assertion: every hook node, in order, with its id
1488 // (sequential per primitive hook; custom hooks are null), name, normalized
@@ -1501,6 +1502,58 @@ describe('react-devtools-facade', () => {
1502 ]);
1503 });
1504
1505 + it('does not inspect hooks by default', () => {
1506 + function Widget() {
1507 + React.useState(7);
1508 + return <div>widget</div>;
1509 + }
1510 +
1511 + act(() => {
1512 + ReactDOMClient.createRoot(container).render(<Widget />);
1513 + });
1514 +
1515 + const widget = getComponentTree().find(n => n.name === 'Widget');
1516 + const info = getComponentByUid(widget.uid);
1517 +
1518 + expect(info.hooks).toBeUndefined();
1519 + });
1520 +
1521 + it('returns an error when requested hook inspection fails', () => {
1522 + jest.resetModules();
1523 + jest.doMock('react-debug-tools', () => ({
1524 + inspectHooksOfFiberWithoutDefaultDispatcher() {
1525 + throw new Error('Cannot inspect hooks');
1526 + },
1527 + }));
1528 + delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1529 +
1530 + const facadeAPI = require('../../index');
1531 + const mockedFacade = facadeAPI.installFacade();
1532 + const mockedTools = facadeAPI.createTools(mockedFacade);
1533 + const MockedReact = require('react');
1534 + const MockedReactDOMClient = require('react-dom/client');
1535 +
1536 + function Widget() {
1537 + MockedReact.useState(7);
1538 + return MockedReact.createElement('div', null, 'widget');
1539 + }
1540 +
1541 + MockedReact.act(() => {
1542 + MockedReactDOMClient.createRoot(container).render(
1543 + MockedReact.createElement(Widget),
1544 + );
1545 + });
1546 +
1547 + const widget = mockedTools
1548 + .getComponentTree()
1549 + .find(node => node.name === 'Widget');
1550 + const info = mockedTools.getComponentByUid(widget.uid, true);
1551 +
1552 + expect(info.error).toBeInstanceOf(Error);
1553 + expect(info.error.message).toBe('Failed to inspect hooks.');
1554 + expect(info.error.cause).toEqual(new Error('Cannot inspect hooks'));
1555 + });
1556 +
1557 it('captures the useContext hook with its provided value', () => {
1558 const ThemeContext = React.createContext('light');
1559 function Themed() {
@@ -1526,7 +1579,7 @@ describe('react-devtools-facade', () => {
1579 });
1580
1581 const themed = getComponentTree().find(n => n.name === 'Themed');
1529 - const info = getComponentByUid(themed.uid);
1582 + const info = getComponentByUid(themed.uid, true);
1583 // useContext is captured as a "Context" hook holding the provider's value.
1584 // It does not consume a primitive hook slot, so its id is null; the
1585 // following useState is the first primitive hook (id 0).
@@ -1546,7 +1599,7 @@ describe('react-devtools-facade', () => {
1599 });
1600
1601 const plain = getComponentTree().find(n => n.name === 'Plain');
1549 - const info = getComponentByUid(plain.uid);
1602 + const info = getComponentByUid(plain.uid, true);
1603 expect(info.hooks).toEqual([]);
1604 });
1605
@@ -1562,7 +1615,7 @@ describe('react-devtools-facade', () => {
1615 });
1616
1617 const myClass = getComponentTree().find(n => n.name === 'MyClass');
1565 - const info = getComponentByUid(myClass.uid);
1618 + const info = getComponentByUid(myClass.uid, true);
1619 expect(info.hooks).toBeUndefined();
1620 });
1621
@@ -1576,7 +1629,7 @@ describe('react-devtools-facade', () => {
1629 });
1630
1631 const div = getComponentTree().find(n => n.name === 'div');
1579 - const info = getComponentByUid(div.uid);
1632 + const info = getComponentByUid(div.uid, true);
1633 expect(info.hooks).toBeUndefined();
1634 });
1635 });