Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ module.exports = {
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
'packages/react-devtools-facade/**/*.js',
'packages/react-devtools-timeline/**/*.js',
'packages/react-native-renderer/**/*.js',
'packages/eslint-plugin-react-hooks/**/*.js',
Expand Down
76 changes: 59 additions & 17 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1196,17 +1196,13 @@ function handleRenderFunctionError(error: any): void {
throw wrapperError;
}

export function inspectHooks<Props>(
// Shared implementation. Requires an explicit dispatcher and never references
// ReactSharedInternals, so importing it does not pull React into the bundle.
function inspectHooksImpl<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: ?CurrentDispatcherRef,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
if (currentDispatcher == null) {
currentDispatcher = ReactSharedInternals;
}

const previousDispatcher = currentDispatcher.H;
currentDispatcher.H = DispatcherProxy;

Expand All @@ -1231,6 +1227,31 @@ export function inspectHooks<Props>(
return buildTree(rootStack, readHookLog);
}

// DevTools will pass the current renderer's injected dispatcher. Other apps
// might compile debug hooks as part of their app though, so default to the
// running React's shared internals when no dispatcher is provided.
export function inspectHooks<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: ?CurrentDispatcherRef,
): HooksTree {
return inspectHooksImpl(
renderFunction,
props,
currentDispatcher ?? ReactSharedInternals,
);
}

// Like inspectHooks but requires an explicit dispatcher and never references
// ReactSharedInternals, so importing it does not pull React into the bundle.
export function inspectHooksWithoutDefaultDispatcher<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
return inspectHooksImpl(renderFunction, props, currentDispatcher);
}

function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
let current: null | Fiber = fiber;
while (current) {
Expand Down Expand Up @@ -1297,16 +1318,13 @@ function resolveDefaultProps(Component: any, baseProps: any) {
return baseProps;
}

export function inspectHooksOfFiber(
// Shared implementation. Requires an explicit dispatcher and never references
// ReactSharedInternals (it delegates to inspectHooksImpl), so importing it does
// not pull React into the bundle.
function inspectHooksOfFiberImpl(
fiber: Fiber,
currentDispatcher: ?CurrentDispatcherRef,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
if (currentDispatcher == null) {
currentDispatcher = ReactSharedInternals;
}

if (
fiber.tag !== FunctionComponent &&
fiber.tag !== SimpleMemoComponent &&
Expand Down Expand Up @@ -1381,7 +1399,7 @@ export function inspectHooksOfFiber(
);
}

return inspectHooks(type, props, currentDispatcher);
return inspectHooksImpl(type, props, currentDispatcher);
} finally {
currentFiber = null;
currentHook = null;
Expand All @@ -1392,3 +1410,27 @@ export function inspectHooksOfFiber(
restoreContexts(contextMap);
}
}

// DevTools will pass the current renderer's injected dispatcher. Other apps
// might compile debug hooks as part of their app though, so default to the
// running React's shared internals when no dispatcher is provided.
export function inspectHooksOfFiber(
fiber: Fiber,
currentDispatcher: ?CurrentDispatcherRef,
): HooksTree {
return inspectHooksOfFiberImpl(
fiber,
currentDispatcher ?? ReactSharedInternals,
);
}

// Like inspectHooksOfFiber but requires an explicit dispatcher and never
// references ReactSharedInternals. Callers that always have the renderer's
// injected dispatcher (e.g. react-devtools-facade) can use this to avoid
// pulling React into their bundle.
export function inspectHooksOfFiberWithoutDefaultDispatcher(
fiber: Fiber,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
return inspectHooksOfFiberImpl(fiber, currentDispatcher);
}
14 changes: 12 additions & 2 deletions packages/react-debug-tools/src/ReactDebugTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
* @flow
*/

import {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks';
import {
inspectHooks,
inspectHooksWithoutDefaultDispatcher,
inspectHooksOfFiber,
inspectHooksOfFiberWithoutDefaultDispatcher,
} from './ReactDebugHooks';

export {inspectHooks, inspectHooksOfFiber};
export {
inspectHooks,
inspectHooksWithoutDefaultDispatcher,
inspectHooksOfFiber,
inspectHooksOfFiberWithoutDefaultDispatcher,
};
37 changes: 37 additions & 0 deletions packages/react-devtools-facade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# react-devtools-facade

Experimental, private package that defines building blocks for querying React runtime state.

The facade installs the `__REACT_DEVTOOLS_GLOBAL_HOOK__` that React looks for at
initialization time — enabling fiber-root tracking without the overhead of the
full DevTools backend — and exposes a small, framework-agnostic library API that
integrators compose into tools.

This package is intentionally low-level. It does **not** install any tool
globals and it does **not** decide how tools are surfaced. It installs the hook,
tracks fiber roots, and hands back building blocks; the integrator (for example,
a `chrome-devtools-mcp` integration) decides everything else — including whether
to expose anything else on globals.

## API

### `installFacade(target = globalThis): Facade`

Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` on `target` and returns a `Facade`
handle holding the hook plus the runtime state it tracks (`fiberRoots`,
`rendererInternals`, `profilingState`). Building blocks read from the returned
`Facade`; they never reach for globals.

Call this **before** React initializes so the hook captures the first commit:

```js
import {installFacade} from 'react-devtools-facade';

const facade = installFacade();
// ...load React, render your app...
```

`installFacade` installs **only** the DevTools hook. It does not install
`__REACT_TOOLS__`, `__REACT_LLM_TOOLS__`, or any other global. Once the tool
building blocks land, an integrator composes them from the returned `Facade` and
decides whether to expose anything on globals.
10 changes: 10 additions & 0 deletions packages/react-devtools-facade/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export * from './src/DevToolsFacade';
13 changes: 13 additions & 0 deletions packages/react-devtools-facade/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "react-devtools-facade",
"version": "0.0.0",
"private": true,
"description": "Building blocks for querying React runtime state",
"license": "MIT",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-devtools-facade"
}
}
Loading
Loading