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

[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 +}