diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 9b5a3b4994781..b00df7e46d761 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -20,7 +20,7 @@ import { ForwardRef, } from 'shared/ReactWorkTags'; -const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; +type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher; // Used to track hooks called during a render @@ -408,10 +408,17 @@ function buildTree(rootStack, readHookLog): HooksTree { export function inspectHooks( renderFunction: Props => React$Node, props: Props, + currentDispatcher: ?CurrentDispatcherRef, ): HooksTree { - let previousDispatcher = ReactCurrentDispatcher.current; + // 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.ReactCurrentDispatcher; + } + + let previousDispatcher = currentDispatcher.current; let readHookLog; - ReactCurrentDispatcher.current = Dispatcher; + currentDispatcher.current = Dispatcher; let ancestorStackError; try { ancestorStackError = new Error(); @@ -419,7 +426,7 @@ export function inspectHooks( } finally { readHookLog = hookLog; hookLog = []; - ReactCurrentDispatcher.current = previousDispatcher; + currentDispatcher.current = previousDispatcher; } let rootStack = ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); @@ -450,10 +457,11 @@ function inspectHooksOfForwardRef( renderFunction: (Props, Ref) => React$Node, props: Props, ref: Ref, + currentDispatcher: CurrentDispatcherRef, ): HooksTree { - let previousDispatcher = ReactCurrentDispatcher.current; + let previousDispatcher = currentDispatcher.current; let readHookLog; - ReactCurrentDispatcher.current = Dispatcher; + currentDispatcher.current = Dispatcher; let ancestorStackError; try { ancestorStackError = new Error(); @@ -461,7 +469,7 @@ function inspectHooksOfForwardRef( } finally { readHookLog = hookLog; hookLog = []; - ReactCurrentDispatcher.current = previousDispatcher; + currentDispatcher.current = previousDispatcher; } let rootStack = ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); @@ -482,7 +490,16 @@ function resolveDefaultProps(Component, baseProps) { return baseProps; } -export function inspectHooksOfFiber(fiber: Fiber) { +export function inspectHooksOfFiber( + fiber: Fiber, + currentDispatcher: ?CurrentDispatcherRef, +) { + // 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.ReactCurrentDispatcher; + } + if ( fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent && @@ -506,9 +523,14 @@ export function inspectHooksOfFiber(fiber: Fiber) { try { setupContexts(contextMap, fiber); if (fiber.tag === ForwardRef) { - return inspectHooksOfForwardRef(type.render, props, fiber.ref); + return inspectHooksOfForwardRef( + type.render, + props, + fiber.ref, + currentDispatcher, + ); } - return inspectHooks(type, props); + return inspectHooks(type, props, currentDispatcher); } finally { currentHook = null; restoreContexts(contextMap); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js index 326c1581fee99..cb26bfc55636d 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js @@ -216,4 +216,37 @@ describe('ReactHooksInspection', () => { }, ]); }); + + it('should support an injected dispatcher', () => { + function Foo(props) { + let [state] = React.useState('hello world'); + return
{state}
; + } + + let initial = {}; + let current = initial; + let getterCalls = 0; + let setterCalls = []; + let FakeDispatcherRef = { + get current() { + getterCalls++; + return current; + }, + set current(value) { + setterCalls.push(value); + current = value; + }, + }; + + expect(() => { + ReactDebugTools.inspectHooks(Foo, {}, FakeDispatcherRef); + }).toThrow( + 'Hooks can only be called inside the body of a function component.', + ); + + expect(getterCalls).toBe(1); + expect(setterCalls).toHaveLength(2); + expect(setterCalls[0]).not.toBe(initial); + expect(setterCalls[1]).toBe(initial); + }); }); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js index d740bb4fdc79f..b242c4adc5cc0 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js @@ -241,4 +241,39 @@ describe('ReactHooksInspectionIntergration', () => { let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); expect(tree).toEqual([{name: 'State', value: 'def', subHooks: []}]); }); + + it('should support an injected dispatcher', () => { + function Foo(props) { + let [state] = React.useState('hello world'); + return
{state}
; + } + + let initial = {}; + let current = initial; + let getterCalls = 0; + let setterCalls = []; + let FakeDispatcherRef = { + get current() { + getterCalls++; + return current; + }, + set current(value) { + setterCalls.push(value); + current = value; + }, + }; + + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root._currentFiber(); + expect(() => { + ReactDebugTools.inspectHooksOfFiber(childFiber, FakeDispatcherRef); + }).toThrow( + 'Hooks can only be called inside the body of a function component.', + ); + + expect(getterCalls).toBe(1); + expect(setterCalls).toHaveLength(2); + expect(setterCalls[0]).not.toBe(initial); + expect(setterCalls[1]).toBe(initial); + }); });