[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
});