From e1c7e651feb7d8f0339db5720cfb61b036ee7290 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 26 Feb 2020 09:28:35 -0800 Subject: [PATCH] Update ReactDebugHooks to handle composite hooks (#18130) The useState hook has always composed the useReducer hook. 1:1 composition like this is fine. But some more recent hooks (e.g. useTransition, useDeferredValue) compose multiple hooks internally. This breaks react-debug-tools because it causes off-by-N errors when the debug tools re-renders the function. For example, if a component were to use the useTransition and useMemo hooks, the normal hooks dispatcher would create a list of first state, then callback, then memo hooks, but the debug tools package would expect a list of transition then memo. This can break user code and cause runtime errors in both the react-debug-tools package and in product code. This PR fixes the currently broken hooks by updating debug tools to be aware of the composite hooks (how many times it should call nextHook essentially) and adds tests to make sure they don't get out of sync again. We'll need to add similar tests for future composite hooks (like useMutableSource #18000). --- .../react-debug-tools/src/ReactDebugHooks.js | 12 +++- .../ReactHooksInspectionIntegration-test.js | 58 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index d515bac08e9b9..c2d3ed0a2accf 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -243,7 +243,11 @@ function useResponder( function useTransition( config: SuspenseConfig | null | void, ): [(() => void) => void, boolean] { - nextHook(); + // useTransition() composes multiple hooks internally. + // Advance the current hook index the same number of times + // so that subsequent hooks have the right memoized state. + nextHook(); // State + nextHook(); // Callback hookLog.push({ primitive: 'Transition', stackError: new Error(), @@ -253,7 +257,11 @@ function useTransition( } function useDeferredValue(value: T, config: TimeoutConfig | null | void): T { - nextHook(); + // useDeferredValue() composes multiple hooks internally. + // Advance the current hook index the same number of times + // so that subsequent hooks have the right memoized state. + nextHook(); // State + nextHook(); // Effect hookLog.push({ primitive: 'DeferredValue', stackError: new Error(), diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index a3542d86ceba3..f92191beaf217 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -366,6 +366,64 @@ describe('ReactHooksInspectionIntegration', () => { ]); }); + if (__EXPERIMENTAL__) { + it('should support composite useTransition hook', () => { + function Foo(props) { + React.useTransition(); + const memoizedValue = React.useMemo(() => 'hello', []); + return
{memoizedValue}
; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'Transition', + value: undefined, + subHooks: [], + }, + { + id: 1, + isStateEditable: false, + name: 'Memo', + value: 'hello', + subHooks: [], + }, + ]); + }); + + it('should support composite useDeferredValue hook', () => { + function Foo(props) { + React.useDeferredValue('abc', { + timeoutMs: 500, + }); + const [state] = React.useState(() => 'hello', []); + return
{state}
; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'DeferredValue', + value: 'abc', + subHooks: [], + }, + { + id: 1, + isStateEditable: true, + name: 'State', + value: 'hello', + subHooks: [], + }, + ]); + }); + } + describe('useDebugValue', () => { it('should support inspectable values for multiple custom hooks', () => { function useLabeledValue(label) {