[DevTools] Extract getDispatcherRef and fork inspectHooks() to avoid pulling React into the build artifact (#36681)
This is just a refactor to unblock the work on the Facade. This PR contains 2 changes: 1. `getDispatcherRef` was extracted from `fiber/renderer` into its own shared module. 2. `inspectHooksOfFiber` was forked. The new version specifies dispatcher as required and doesn't set it to default as `ReactSharedInternals`, which are pulling the whole `react` module as part of them: https://github.com/facebook/react/blob/63e95c2b51d9e9b8b9e15d656c1e11c393277699/packages/shared/ReactSharedInternals.js#L10-L15
Ruslan Lesiutin committed
Jun 15, 2026 at 08:27 UTC
bf76955e0f2886b7f0bd6eb24154be1ad90393e5
4 files changed
+107
-46
packages/react-debug-tools/src/ReactDebugHooks.js
+59
-17
@@ -1194,17 +1194,13 @@ function handleRenderFunctionError(error: any): void {
1194
throw wrapperError;
1195
}
1196
1197
-export function inspectHooks<Props>(
1197
+// Shared implementation. Requires an explicit dispatcher and never references
1198
+// ReactSharedInternals, so importing it does not pull React into the bundle.
1199
+function inspectHooksImpl<Props>(
1200
renderFunction: Props => React$Node,
1201
props: Props,
1200
- currentDispatcher: ?CurrentDispatcherRef,
1202
+ currentDispatcher: CurrentDispatcherRef,
1203
): HooksTree {
1202
- // DevTools will pass the current renderer's injected dispatcher.
1203
- // Other apps might compile debug hooks as part of their app though.
1204
- if (currentDispatcher == null) {
1205
- currentDispatcher = ReactSharedInternals;
1206
- }
1207
-
1204
const previousDispatcher = currentDispatcher.H;
1205
currentDispatcher.H = DispatcherProxy;
1206
@@ -1229,6 +1225,31 @@ export function inspectHooks<Props>(
1225
return buildTree(rootStack, readHookLog);
1226
}
1227
1228
+// DevTools will pass the current renderer's injected dispatcher. Other apps
1229
+// might compile debug hooks as part of their app though, so default to the
1230
+// running React's shared internals when no dispatcher is provided.
1231
+export function inspectHooks<Props>(
1232
+ renderFunction: Props => React$Node,
1233
+ props: Props,
1234
+ currentDispatcher: ?CurrentDispatcherRef,
1235
+): HooksTree {
1236
+ return inspectHooksImpl(
1237
+ renderFunction,
1238
+ props,
1239
+ currentDispatcher ?? ReactSharedInternals,
1240
+ );
1241
+}
1242
+
1243
+// Like inspectHooks but requires an explicit dispatcher and never references
1244
+// ReactSharedInternals, so importing it does not pull React into the bundle.
1245
+export function inspectHooksWithoutDefaultDispatcher<Props>(
1246
+ renderFunction: Props => React$Node,
1247
+ props: Props,
1248
+ currentDispatcher: CurrentDispatcherRef,
1249
+): HooksTree {
1250
+ return inspectHooksImpl(renderFunction, props, currentDispatcher);
1251
+}
1252
+
1253
function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
1254
let current: null | Fiber = fiber;
1255
while (current) {
@@ -1295,16 +1316,13 @@ function resolveDefaultProps(Component: any, baseProps: any) {
1316
return baseProps;
1317
}
1318
1298
-export function inspectHooksOfFiber(
1319
+// Shared implementation. Requires an explicit dispatcher and never references
1320
+// ReactSharedInternals (it delegates to inspectHooksImpl), so importing it does
1321
+// not pull React into the bundle.
1322
+function inspectHooksOfFiberImpl(
1323
fiber: Fiber,
1300
- currentDispatcher: ?CurrentDispatcherRef,
1324
+ currentDispatcher: CurrentDispatcherRef,
1325
): HooksTree {
1302
- // DevTools will pass the current renderer's injected dispatcher.
1303
- // Other apps might compile debug hooks as part of their app though.
1304
- if (currentDispatcher == null) {
1305
- currentDispatcher = ReactSharedInternals;
1306
- }
1307
-
1326
if (
1327
fiber.tag !== FunctionComponent &&
1328
fiber.tag !== SimpleMemoComponent &&
@@ -1381,7 +1399,7 @@ export function inspectHooksOfFiber(
1399
);
1400
}
1401
1384
- return inspectHooks(type, props, currentDispatcher);
1402
+ return inspectHooksImpl(type, props, currentDispatcher);
1403
} finally {
1404
currentFiber = null;
1405
currentHook = null;
@@ -1392,3 +1410,27 @@ export function inspectHooksOfFiber(
1410
restoreContexts(contextMap);
1411
}
1412
}
1413
+
1414
+// DevTools will pass the current renderer's injected dispatcher. Other apps
1415
+// might compile debug hooks as part of their app though, so default to the
1416
+// running React's shared internals when no dispatcher is provided.
1417
+export function inspectHooksOfFiber(
1418
+ fiber: Fiber,
1419
+ currentDispatcher: ?CurrentDispatcherRef,
1420
+): HooksTree {
1421
+ return inspectHooksOfFiberImpl(
1422
+ fiber,
1423
+ currentDispatcher ?? ReactSharedInternals,
1424
+ );
1425
+}
1426
+
1427
+// Like inspectHooksOfFiber but requires an explicit dispatcher and never
1428
+// references ReactSharedInternals. Callers that always have the renderer's
1429
+// injected dispatcher (e.g. react-devtools-facade) can use this to avoid
1430
+// pulling React into their bundle.
1431
+export function inspectHooksOfFiberWithoutDefaultDispatcher(
1432
+ fiber: Fiber,
1433
+ currentDispatcher: CurrentDispatcherRef,
1434
+): HooksTree {
1435
+ return inspectHooksOfFiberImpl(fiber, currentDispatcher);
1436
+}
packages/react-debug-tools/src/ReactDebugTools.js
+12
-2
@@ -7,6 +7,16 @@
7
* @flow
8
*/
9
10
-import {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks';
10
+import {
11
+ inspectHooks,
12
+ inspectHooksWithoutDefaultDispatcher,
13
+ inspectHooksOfFiber,
14
+ inspectHooksOfFiberWithoutDefaultDispatcher,
15
+} from './ReactDebugHooks';
16
12
-export {inspectHooks, inspectHooksOfFiber};
17
+export {
18
+ inspectHooks,
19
+ inspectHooksWithoutDefaultDispatcher,
20
+ inspectHooksOfFiber,
21
+ inspectHooksOfFiberWithoutDefaultDispatcher,
22
+};
packages/react-devtools-shared/src/backend/fiber/renderer.js
+1
-27
@@ -171,8 +171,6 @@ import type {
171
RendererInterface,
172
SerializedElement,
173
SerializedAsyncInfo,
174
- CurrentDispatcherRef,
175
- LegacyDispatcherRef,
174
ProfilingSettings,
175
} from '../types';
176
import type {
@@ -194,6 +192,7 @@ import {
192
VIRTUAL_INSTANCE,
193
FILTERED_FIBER_INSTANCE,
194
} from './shared/DevToolsFiberTypes';
195
+import {getDispatcherRef} from '../shared/DevToolsReactDispatcher';
196
import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';
197
import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
198
@@ -275,31 +274,6 @@ function createSuspenseNode(
274
});
275
}
276
278
-export function getDispatcherRef(renderer: {
279
- +currentDispatcherRef?: LegacyDispatcherRef | CurrentDispatcherRef,
280
- ...
281
-}): void | CurrentDispatcherRef {
282
- if (renderer.currentDispatcherRef === undefined) {
283
- return undefined;
284
- }
285
- const injectedRef = renderer.currentDispatcherRef;
286
- if (
287
- typeof injectedRef.H === 'undefined' &&
288
- typeof injectedRef.current !== 'undefined'
289
- ) {
290
- // We got a legacy dispatcher injected, let's create a wrapper proxy to translate.
291
- return {
292
- get H() {
293
- return (injectedRef as any).current;
294
- },
295
- set H(value) {
296
- (injectedRef as any).current = value;
297
- },
298
- };
299
- }
300
- return injectedRef as any;
301
-}
302
-
277
// All environment names we've seen so far. This lets us create a list of filters to apply.
278
// This should ideally include env of filtered Components too so that you can add those as
279
// filters at the same time as removing some other filter.
packages/react-devtools-shared/src/backend/shared/DevToolsReactDispatcher.js
new
+35
@@ -0,0 +1,35 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {CurrentDispatcherRef, LegacyDispatcherRef} from '../types';
11
+
12
+export function getDispatcherRef(renderer: {
13
+ +currentDispatcherRef?: LegacyDispatcherRef | CurrentDispatcherRef,
14
+ ...
15
+}): void | CurrentDispatcherRef {
16
+ if (renderer.currentDispatcherRef === undefined) {
17
+ return undefined;
18
+ }
19
+ const injectedRef = renderer.currentDispatcherRef;
20
+ if (
21
+ typeof injectedRef.H === 'undefined' &&
22
+ typeof injectedRef.current !== 'undefined'
23
+ ) {
24
+ // We got a legacy dispatcher injected, let's create a wrapper proxy to translate.
25
+ return {
26
+ get H() {
27
+ return (injectedRef as any).current;
28
+ },
29
+ set H(value) {
30
+ (injectedRef as any).current = value;
31
+ },
32
+ };
33
+ }
34
+ return injectedRef as any;
35
+}