Skip to content

Commit d4e4454

Browse files
authored
[react-devtools-facade] Add includeHooks option to component lookup (react#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.
1 parent 92f4fda commit d4e4454

3 files changed

Lines changed: 102 additions & 34 deletions

File tree

packages/react-devtools-facade/src/DevToolsFacadeTools.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export type Tools = {
5555
depth?: number,
5656
rootUid?: string,
5757
) => Array<TreeNode> | ToolError,
58-
getComponentByUid: (uid: string) => NodeInfo | ToolError,
58+
getComponentByUid: (
59+
uid: string,
60+
includeHooks?: boolean,
61+
) => NodeInfo | ToolError,
5962
findComponents: (
6063
name: string,
6164
rootUid?: string,

packages/react-devtools-facade/src/DevToolsFacadeTreeTools.js

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {RendererInternals} from './DevToolsFacade';
2424
// (to TOON, JSON, etc.) is the integrator's responsibility.
2525

2626
// 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};
2828

2929
// A single component in a tree snapshot. firstChild/nextSibling reference other
3030
// nodes by their uid, forming an adjacency list the integrator can rebuild.
@@ -81,7 +81,10 @@ export type TreeTools = {
8181
depth?: number,
8282
rootUid?: string,
8383
) => Array<TreeNode> | ToolError,
84-
getComponentByUid: (uid: string) => NodeInfo | ToolError,
84+
getComponentByUid: (
85+
uid: string,
86+
includeHooks?: boolean,
87+
) => NodeInfo | ToolError,
8588
findComponents: (
8689
name: string,
8790
rootUid?: string,
@@ -414,16 +417,21 @@ export function createTreeTools(
414417
}
415418

416419
/**
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.
420423
*
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.
423427
*
424428
* @param uid - The component uid (e.g. "r5").
429+
* @param includeHooks - Whether to inspect hooks for function components.
425430
*/
426-
function getComponentByUid(uid: string): NodeInfo | ToolError {
431+
function getComponentByUid(
432+
uid: string,
433+
includeHooks?: boolean = false,
434+
): NodeInfo | ToolError {
427435
const result = findFiberByUid(uid);
428436
if (result.error != null) {
429437
return {error: result.error};
@@ -441,26 +449,30 @@ export function createTreeTools(
441449
if (props != null) {
442450
info.props = props;
443451
}
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+
}
464476
}
465477
}
466478
return info;

packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ describe('react-devtools-facade', () => {
4747
});
4848

4949
afterEach(() => {
50+
jest.dontMock('react-debug-tools');
5051
container = null;
5152
});
5253

@@ -1481,7 +1482,7 @@ describe('react-devtools-facade', () => {
14811482
});
14821483

14831484
const widget = getComponentTree().find(n => n.name === 'Widget');
1484-
const info = getComponentByUid(widget.uid);
1485+
const info = getComponentByUid(widget.uid, true);
14851486

14861487
// Full structural assertion: every hook node, in order, with its id
14871488
// (sequential per primitive hook; custom hooks are null), name, normalized
@@ -1501,6 +1502,58 @@ describe('react-devtools-facade', () => {
15011502
]);
15021503
});
15031504

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+
15041557
it('captures the useContext hook with its provided value', () => {
15051558
const ThemeContext = React.createContext('light');
15061559
function Themed() {
@@ -1526,7 +1579,7 @@ describe('react-devtools-facade', () => {
15261579
});
15271580

15281581
const themed = getComponentTree().find(n => n.name === 'Themed');
1529-
const info = getComponentByUid(themed.uid);
1582+
const info = getComponentByUid(themed.uid, true);
15301583
// useContext is captured as a "Context" hook holding the provider's value.
15311584
// It does not consume a primitive hook slot, so its id is null; the
15321585
// following useState is the first primitive hook (id 0).
@@ -1546,7 +1599,7 @@ describe('react-devtools-facade', () => {
15461599
});
15471600

15481601
const plain = getComponentTree().find(n => n.name === 'Plain');
1549-
const info = getComponentByUid(plain.uid);
1602+
const info = getComponentByUid(plain.uid, true);
15501603
expect(info.hooks).toEqual([]);
15511604
});
15521605

@@ -1562,7 +1615,7 @@ describe('react-devtools-facade', () => {
15621615
});
15631616

15641617
const myClass = getComponentTree().find(n => n.name === 'MyClass');
1565-
const info = getComponentByUid(myClass.uid);
1618+
const info = getComponentByUid(myClass.uid, true);
15661619
expect(info.hooks).toBeUndefined();
15671620
});
15681621

@@ -1576,7 +1629,7 @@ describe('react-devtools-facade', () => {
15761629
});
15771630

15781631
const div = getComponentTree().find(n => n.name === 'div');
1579-
const info = getComponentByUid(div.uid);
1632+
const info = getComponentByUid(div.uid, true);
15801633
expect(info.hooks).toBeUndefined();
15811634
});
15821635
});

0 commit comments

Comments
 (0)