From b66beff6fa7cee5b27e3262cd95c0f190ff4c6e9 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 5 Mar 2024 11:08:23 +0100 Subject: [PATCH 1/7] DevTools: Add support for `useFormStatus` --- .../react-debug-tools/src/ReactDebugHooks.js | 85 +++++++++- ...ReactHooksInspectionIntegrationDOM-test.js | 154 ++++++++++++++++++ .../app/InspectableElements/CustomHooks.js | 9 +- 3 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index a5a4080a7195b..3d39d54cc4a9b 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -21,6 +21,7 @@ import type { Fiber, Dispatcher as DispatcherType, } from 'react-reconciler/src/ReactInternalTypes'; +import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig'; import ErrorStackParser from 'error-stack-parser'; import assign from 'shared/assign'; @@ -48,6 +49,11 @@ type HookLogEntry = { value: mixed, debugInfo: ReactDebugInfo | null, dispatcherHookName: string, + /** + * A list of hook function names that may call this dispatcher method. + * If `null`, we assume that the wrapper name is equal to the dispatcher mthod name. + */ + wrapperNames: Array | null, }; let hookLog: Array = []; @@ -134,6 +140,11 @@ function getPrimitiveStackCache(): Map> { } Dispatcher.useId(); + + if (typeof Dispatcher.useHostTransitionStatus === 'function') { + // This type check is for Flow only. + Dispatcher.useHostTransitionStatus(); + } } finally { readHookLog = hookLog; hookLog = []; @@ -211,6 +222,7 @@ function use(usable: Usable): T { debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', + wrapperNames: null, }); return fulfilledValue; } @@ -229,6 +241,7 @@ function use(usable: Usable): T { debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', + wrapperNames: null, }); throw SuspenseException; } else if (usable.$$typeof === REACT_CONTEXT_TYPE) { @@ -242,6 +255,7 @@ function use(usable: Usable): T { value, debugInfo: null, dispatcherHookName: 'Use', + wrapperNames: null, }); return value; @@ -261,6 +275,7 @@ function useContext(context: ReactContext): T { value: value, debugInfo: null, dispatcherHookName: 'Context', + wrapperNames: null, }); return value; } @@ -283,6 +298,7 @@ function useState( value: state, debugInfo: null, dispatcherHookName: 'State', + wrapperNames: null, }); return [state, (action: BasicStateAction) => {}]; } @@ -306,6 +322,7 @@ function useReducer( value: state, debugInfo: null, dispatcherHookName: 'Reducer', + wrapperNames: null, }); return [state, (action: A) => {}]; } @@ -320,6 +337,7 @@ function useRef(initialValue: T): {current: T} { value: ref.current, debugInfo: null, dispatcherHookName: 'Ref', + wrapperNames: null, }); return ref; } @@ -333,6 +351,7 @@ function useCacheRefresh(): () => void { value: hook !== null ? hook.memoizedState : function refresh() {}, debugInfo: null, dispatcherHookName: 'CacheRefresh', + wrapperNames: null, }); return () => {}; } @@ -349,6 +368,7 @@ function useLayoutEffect( value: create, debugInfo: null, dispatcherHookName: 'LayoutEffect', + wrapperNames: null, }); } @@ -364,6 +384,7 @@ function useInsertionEffect( value: create, debugInfo: null, dispatcherHookName: 'InsertionEffect', + wrapperNames: null, }); } @@ -379,6 +400,7 @@ function useEffect( value: create, debugInfo: null, dispatcherHookName: 'Effect', + wrapperNames: null, }); } @@ -403,6 +425,7 @@ function useImperativeHandle( value: instance, debugInfo: null, dispatcherHookName: 'ImperativeHandle', + wrapperNames: null, }); } @@ -414,6 +437,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) { value: typeof formatterFn === 'function' ? formatterFn(value) : value, debugInfo: null, dispatcherHookName: 'DebugValue', + wrapperNames: null, }); } @@ -426,6 +450,7 @@ function useCallback(callback: T, inputs: Array | void | null): T { value: hook !== null ? hook.memoizedState[0] : callback, debugInfo: null, dispatcherHookName: 'Callback', + wrapperNames: null, }); return callback; } @@ -443,6 +468,7 @@ function useMemo( value, debugInfo: null, dispatcherHookName: 'Memo', + wrapperNames: null, }); return value; } @@ -465,6 +491,7 @@ function useSyncExternalStore( value, debugInfo: null, dispatcherHookName: 'SyncExternalStore', + wrapperNames: null, }); return value; } @@ -488,6 +515,7 @@ function useTransition(): [ value: isPending, debugInfo: null, dispatcherHookName: 'Transition', + wrapperNames: null, }); return [isPending, () => {}]; } @@ -502,6 +530,7 @@ function useDeferredValue(value: T, initialValue?: T): T { value: prevValue, debugInfo: null, dispatcherHookName: 'DeferredValue', + wrapperNames: null, }); return prevValue; } @@ -516,6 +545,7 @@ function useId(): string { value: id, debugInfo: null, dispatcherHookName: 'Id', + wrapperNames: null, }); return id; } @@ -567,6 +597,7 @@ function useOptimistic( value: state, debugInfo: null, dispatcherHookName: 'Optimistic', + wrapperNames: null, }); return [state, (action: A) => {}]; } @@ -627,6 +658,7 @@ function useFormState( value: value, debugInfo: debugInfo, dispatcherHookName: 'FormState', + wrapperNames: null, }); if (error !== null) { @@ -697,6 +729,7 @@ function useActionState( value: value, debugInfo: debugInfo, dispatcherHookName: 'ActionState', + wrapperNames: null, }); if (error !== null) { @@ -711,6 +744,31 @@ function useActionState( return [state, (payload: P) => {}, false]; } +function useHostTransitionStatus(): TransitionStatus { + const status = readContext( + // $FlowFixMe[prop-missing] `readContext` only needs _currentValue + ({ + // $FlowFixMe[incompatible-cast] TODO: Incorrect bottom value without access to Fiber config. + _currentValue: null, + }: ReactContext), + ); + + hookLog.push({ + displayName: null, + primitive: 'HostTransitionStatus', + stackError: new Error(), + value: status, + debugInfo: null, + dispatcherHookName: 'HostTransitionStatus', + wrapperNames: [ + // react-dom + 'FormStatus', + ], + }); + + return status; +} + const Dispatcher: DispatcherType = { use, readContext, @@ -734,6 +792,7 @@ const Dispatcher: DispatcherType = { useId, useFormState, useActionState, + useHostTransitionStatus, }; // create a proxy to throw a custom error @@ -855,11 +914,24 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { ) { i++; } - if ( - i < hookStack.length - 1 && - isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) - ) { - i++; + if (hook.wrapperNames !== null) { + for (let j = 0; j < hook.wrapperNames.length; j++) { + const wrapperName = hook.wrapperNames[j]; + if ( + i < hookStack.length - 1 && + isReactWrapper(hookStack[i].functionName, wrapperName) + ) { + i++; + } + } + } else { + const wrapperName = hook.dispatcherHookName; + if ( + i < hookStack.length - 1 && + isReactWrapper(hookStack[i].functionName, wrapperName) + ) { + i++; + } } return i; } @@ -997,7 +1069,8 @@ function buildTree( primitive === 'Context (use)' || primitive === 'DebugValue' || primitive === 'Promise' || - primitive === 'Unresolved' + primitive === 'Unresolved' || + primitive === 'HostTransitionStatus' ? null : nativeHookID++; diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js new file mode 100644 index 0000000000000..752d93fa30ab8 --- /dev/null +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js @@ -0,0 +1,154 @@ +/** + * 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. + * + * @emails react-core + * @jest-environment jsdom + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactDOMClient; +let ReactDebugTools; +let act; + +function normalizeSourceLoc(tree) { + tree.forEach(node => { + if (node.hookSource) { + node.hookSource.fileName = '**'; + node.hookSource.lineNumber = 0; + node.hookSource.columnNumber = 0; + } + normalizeSourceLoc(node.subHooks); + }); + return tree; +} + +describe('ReactHooksInspectionIntegration', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; + ReactDebugTools = require('react-debug-tools'); + }); + + it('should support useFormStatus hook', async () => { + function FormStatus() { + const status = ReactDOM.useFormStatus(); + React.useMemo(() => 'memo', []); + React.useMemo(() => 'not used', []); + + return JSON.stringify(status); + } + + const treeWithoutFiber = ReactDebugTools.inspectHooks(FormStatus); + expect(normalizeSourceLoc(treeWithoutFiber)).toEqual([ + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: null, + isStateEditable: false, + name: 'FormStatus', + subHooks: [], + value: null, + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 0, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'memo', + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 1, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'not used', + }, + ]); + + const root = ReactDOMClient.createRoot(document.createElement('div')); + + await act(() => { + root.render( +
+ + , + ); + }); + + // Implementation detail. Feel free to adjust the position of the Fiber in the tree. + const formStatusFiber = root._internalRoot.current.child.child; + const treeWithFiber = ReactDebugTools.inspectHooksOfFiber(formStatusFiber); + expect(normalizeSourceLoc(treeWithFiber)).toEqual([ + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: null, + isStateEditable: false, + name: 'FormStatus', + subHooks: [], + value: null, + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 0, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'memo', + }, + { + debugInfo: null, + hookSource: { + columnNumber: 0, + fileName: '**', + functionName: 'FormStatus', + lineNumber: 0, + }, + id: 1, + isStateEditable: false, + name: 'Memo', + subHooks: [], + value: 'not used', + }, + ]); + }); +}); diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js index c8fbca78a9dea..a05acf138d3d3 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js @@ -21,7 +21,7 @@ import { useState, use, } from 'react'; -import {useFormState} from 'react-dom'; +import {useFormState, useFormStatus} from 'react-dom'; const object = { string: 'abc', @@ -164,6 +164,12 @@ function incrementWithDelay(previousState: number, formData: FormData) { }); } +function FormStatus() { + const status = useFormStatus(); + + return
{JSON.stringify(status)}
; +} + function Forms() { const [state, formAction] = useFormState(incrementWithDelay, 0); return ( @@ -184,6 +190,7 @@ function Forms() { + ); } From 83e9d3c962bcce905d364fdd3bfefa4db8f18681 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 5 Mar 2024 19:59:40 +0100 Subject: [PATCH 2/7] Gate production, non-source builds to workaround unspecified Jest bug --- .../src/__tests__/ReactHooksInspectionIntegrationDOM-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js index 752d93fa30ab8..19511eb36adc1 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js @@ -38,6 +38,8 @@ describe('ReactHooksInspectionIntegration', () => { ReactDebugTools = require('react-debug-tools'); }); + // Gating production, non-source builds only for Jest. + // @gate source || build === "development" it('should support useFormStatus hook', async () => { function FormStatus() { const status = ReactDOM.useFormStatus(); From 8910c69f08edb839aafdcb6cd0c160f030da98dd Mon Sep 17 00:00:00 2001 From: eps1lon Date: Wed, 20 Mar 2024 13:00:41 +0100 Subject: [PATCH 3/7] Less forking, more allocations --- .../react-debug-tools/src/ReactDebugHooks.js | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 3d39d54cc4a9b..a2e2f6626cb91 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -49,11 +49,7 @@ type HookLogEntry = { value: mixed, debugInfo: ReactDebugInfo | null, dispatcherHookName: string, - /** - * A list of hook function names that may call this dispatcher method. - * If `null`, we assume that the wrapper name is equal to the dispatcher mthod name. - */ - wrapperNames: Array | null, + wrapperNames: Array, }; let hookLog: Array = []; @@ -222,7 +218,7 @@ function use(usable: Usable): T { debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', - wrapperNames: null, + wrapperNames: ['Use'], }); return fulfilledValue; } @@ -241,7 +237,7 @@ function use(usable: Usable): T { debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', - wrapperNames: null, + wrapperNames: ['Use'], }); throw SuspenseException; } else if (usable.$$typeof === REACT_CONTEXT_TYPE) { @@ -255,7 +251,7 @@ function use(usable: Usable): T { value, debugInfo: null, dispatcherHookName: 'Use', - wrapperNames: null, + wrapperNames: ['Use'], }); return value; @@ -275,7 +271,7 @@ function useContext(context: ReactContext): T { value: value, debugInfo: null, dispatcherHookName: 'Context', - wrapperNames: null, + wrapperNames: ['Context'], }); return value; } @@ -298,7 +294,7 @@ function useState( value: state, debugInfo: null, dispatcherHookName: 'State', - wrapperNames: null, + wrapperNames: ['State'], }); return [state, (action: BasicStateAction) => {}]; } @@ -322,7 +318,7 @@ function useReducer( value: state, debugInfo: null, dispatcherHookName: 'Reducer', - wrapperNames: null, + wrapperNames: ['Reducer'], }); return [state, (action: A) => {}]; } @@ -337,7 +333,7 @@ function useRef(initialValue: T): {current: T} { value: ref.current, debugInfo: null, dispatcherHookName: 'Ref', - wrapperNames: null, + wrapperNames: ['Ref'], }); return ref; } @@ -351,7 +347,7 @@ function useCacheRefresh(): () => void { value: hook !== null ? hook.memoizedState : function refresh() {}, debugInfo: null, dispatcherHookName: 'CacheRefresh', - wrapperNames: null, + wrapperNames: ['CacheRefresh'], }); return () => {}; } @@ -368,7 +364,7 @@ function useLayoutEffect( value: create, debugInfo: null, dispatcherHookName: 'LayoutEffect', - wrapperNames: null, + wrapperNames: ['LayoutEffect'], }); } @@ -384,7 +380,7 @@ function useInsertionEffect( value: create, debugInfo: null, dispatcherHookName: 'InsertionEffect', - wrapperNames: null, + wrapperNames: ['InsertionEffect'], }); } @@ -400,7 +396,7 @@ function useEffect( value: create, debugInfo: null, dispatcherHookName: 'Effect', - wrapperNames: null, + wrapperNames: ['Effect'], }); } @@ -425,7 +421,7 @@ function useImperativeHandle( value: instance, debugInfo: null, dispatcherHookName: 'ImperativeHandle', - wrapperNames: null, + wrapperNames: ['ImperativeHandle'], }); } @@ -437,7 +433,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) { value: typeof formatterFn === 'function' ? formatterFn(value) : value, debugInfo: null, dispatcherHookName: 'DebugValue', - wrapperNames: null, + wrapperNames: ['DebugValue'], }); } @@ -450,7 +446,7 @@ function useCallback(callback: T, inputs: Array | void | null): T { value: hook !== null ? hook.memoizedState[0] : callback, debugInfo: null, dispatcherHookName: 'Callback', - wrapperNames: null, + wrapperNames: ['Callback'], }); return callback; } @@ -468,7 +464,7 @@ function useMemo( value, debugInfo: null, dispatcherHookName: 'Memo', - wrapperNames: null, + wrapperNames: ['Memo'], }); return value; } @@ -491,7 +487,7 @@ function useSyncExternalStore( value, debugInfo: null, dispatcherHookName: 'SyncExternalStore', - wrapperNames: null, + wrapperNames: ['SyncExternalStore'], }); return value; } @@ -515,7 +511,7 @@ function useTransition(): [ value: isPending, debugInfo: null, dispatcherHookName: 'Transition', - wrapperNames: null, + wrapperNames: ['Transition'], }); return [isPending, () => {}]; } @@ -530,7 +526,7 @@ function useDeferredValue(value: T, initialValue?: T): T { value: prevValue, debugInfo: null, dispatcherHookName: 'DeferredValue', - wrapperNames: null, + wrapperNames: ['DeferredValue'], }); return prevValue; } @@ -545,7 +541,7 @@ function useId(): string { value: id, debugInfo: null, dispatcherHookName: 'Id', - wrapperNames: null, + wrapperNames: ['Id'], }); return id; } @@ -597,7 +593,7 @@ function useOptimistic( value: state, debugInfo: null, dispatcherHookName: 'Optimistic', - wrapperNames: null, + wrapperNames: ['Optimistic'], }); return [state, (action: A) => {}]; } @@ -658,7 +654,7 @@ function useFormState( value: value, debugInfo: debugInfo, dispatcherHookName: 'FormState', - wrapperNames: null, + wrapperNames: ['FormState'], }); if (error !== null) { @@ -729,7 +725,7 @@ function useActionState( value: value, debugInfo: debugInfo, dispatcherHookName: 'ActionState', - wrapperNames: null, + wrapperNames: ['ActionState'], }); if (error !== null) { @@ -914,18 +910,8 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { ) { i++; } - if (hook.wrapperNames !== null) { - for (let j = 0; j < hook.wrapperNames.length; j++) { - const wrapperName = hook.wrapperNames[j]; - if ( - i < hookStack.length - 1 && - isReactWrapper(hookStack[i].functionName, wrapperName) - ) { - i++; - } - } - } else { - const wrapperName = hook.dispatcherHookName; + for (let j = 0; j < hook.wrapperNames.length; j++) { + const wrapperName = hook.wrapperNames[j]; if ( i < hookStack.length - 1 && isReactWrapper(hookStack[i].functionName, wrapperName) From daa026a80dc228c09924599f377e6ce3dfd173a9 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Wed, 3 Apr 2024 20:51:17 +0200 Subject: [PATCH 4/7] Fix parsing for minified prod bundles --- .../src/__tests__/ReactHooksInspectionIntegrationDOM-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js index 19511eb36adc1..752d93fa30ab8 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegrationDOM-test.js @@ -38,8 +38,6 @@ describe('ReactHooksInspectionIntegration', () => { ReactDebugTools = require('react-debug-tools'); }); - // Gating production, non-source builds only for Jest. - // @gate source || build === "development" it('should support useFormStatus hook', async () => { function FormStatus() { const status = ReactDOM.useFormStatus(); From eeece9283e3f1b723d0671d3e850f25ac098226f Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 15 Apr 2024 22:39:02 +0200 Subject: [PATCH 5/7] Regression test to ensure we don't confuse custom hooks due to name collision --- .../__tests__/ReactHooksInspection-test.js | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js index 97aaeb8f4a48a..8d159a22105c0 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js @@ -426,6 +426,150 @@ describe('ReactHooksInspection', () => { `); }); + it('should not confuse built-in hooks with custom hooks that have the same name', () => { + function useState(value) { + React.useState(value); + React.useDebugValue('custom useState'); + } + function useFormStatus() { + React.useState('custom useState'); + React.useDebugValue('custom useFormStatus'); + } + function Foo(props) { + useFormStatus(); + useState('Hello, Dave!'); + return null; + } + const tree = ReactDebugTools.inspectHooks(Foo, {}); + if (__DEV__) { + expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` + [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "FormStatus", + "subHooks": [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "useFormStatus", + "lineNumber": 0, + }, + "id": 0, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": "custom useState", + }, + ], + "value": "custom useFormStatus", + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "State", + "subHooks": [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "useState", + "lineNumber": 0, + }, + "id": 1, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": "Hello, Dave!", + }, + ], + "value": "custom useState", + }, + ] + `); + } else { + expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` + [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "FormStatus", + "subHooks": [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "useFormStatus", + "lineNumber": 0, + }, + "id": 0, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": "custom useState", + }, + ], + "value": undefined, + }, + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "State", + "subHooks": [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "useState", + "lineNumber": 0, + }, + "id": 1, + "isStateEditable": true, + "name": "State", + "subHooks": [], + "value": "Hello, Dave!", + }, + ], + "value": undefined, + }, + ] + `); + } + }); + it('should inspect the default value using the useContext hook', () => { const MyContext = React.createContext('default'); function Foo(props) { From 274e1cca44a607852800712a44d5ddce04d4a1ce Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 15 Apr 2024 22:57:24 +0200 Subject: [PATCH 6/7] Remove unnecessary concept of `wrapperNames` just look one frame above --- .../react-debug-tools/src/ReactDebugHooks.js | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index a2e2f6626cb91..a5be893f1d7a5 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -49,7 +49,6 @@ type HookLogEntry = { value: mixed, debugInfo: ReactDebugInfo | null, dispatcherHookName: string, - wrapperNames: Array, }; let hookLog: Array = []; @@ -218,7 +217,6 @@ function use(usable: Usable): T { debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', - wrapperNames: ['Use'], }); return fulfilledValue; } @@ -237,7 +235,6 @@ function use(usable: Usable): T { debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', - wrapperNames: ['Use'], }); throw SuspenseException; } else if (usable.$$typeof === REACT_CONTEXT_TYPE) { @@ -251,7 +248,6 @@ function use(usable: Usable): T { value, debugInfo: null, dispatcherHookName: 'Use', - wrapperNames: ['Use'], }); return value; @@ -271,7 +267,6 @@ function useContext(context: ReactContext): T { value: value, debugInfo: null, dispatcherHookName: 'Context', - wrapperNames: ['Context'], }); return value; } @@ -294,7 +289,6 @@ function useState( value: state, debugInfo: null, dispatcherHookName: 'State', - wrapperNames: ['State'], }); return [state, (action: BasicStateAction) => {}]; } @@ -318,7 +312,6 @@ function useReducer( value: state, debugInfo: null, dispatcherHookName: 'Reducer', - wrapperNames: ['Reducer'], }); return [state, (action: A) => {}]; } @@ -333,7 +326,6 @@ function useRef(initialValue: T): {current: T} { value: ref.current, debugInfo: null, dispatcherHookName: 'Ref', - wrapperNames: ['Ref'], }); return ref; } @@ -347,7 +339,6 @@ function useCacheRefresh(): () => void { value: hook !== null ? hook.memoizedState : function refresh() {}, debugInfo: null, dispatcherHookName: 'CacheRefresh', - wrapperNames: ['CacheRefresh'], }); return () => {}; } @@ -364,7 +355,6 @@ function useLayoutEffect( value: create, debugInfo: null, dispatcherHookName: 'LayoutEffect', - wrapperNames: ['LayoutEffect'], }); } @@ -380,7 +370,6 @@ function useInsertionEffect( value: create, debugInfo: null, dispatcherHookName: 'InsertionEffect', - wrapperNames: ['InsertionEffect'], }); } @@ -396,7 +385,6 @@ function useEffect( value: create, debugInfo: null, dispatcherHookName: 'Effect', - wrapperNames: ['Effect'], }); } @@ -421,7 +409,6 @@ function useImperativeHandle( value: instance, debugInfo: null, dispatcherHookName: 'ImperativeHandle', - wrapperNames: ['ImperativeHandle'], }); } @@ -433,7 +420,6 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) { value: typeof formatterFn === 'function' ? formatterFn(value) : value, debugInfo: null, dispatcherHookName: 'DebugValue', - wrapperNames: ['DebugValue'], }); } @@ -446,7 +432,6 @@ function useCallback(callback: T, inputs: Array | void | null): T { value: hook !== null ? hook.memoizedState[0] : callback, debugInfo: null, dispatcherHookName: 'Callback', - wrapperNames: ['Callback'], }); return callback; } @@ -464,7 +449,6 @@ function useMemo( value, debugInfo: null, dispatcherHookName: 'Memo', - wrapperNames: ['Memo'], }); return value; } @@ -487,7 +471,6 @@ function useSyncExternalStore( value, debugInfo: null, dispatcherHookName: 'SyncExternalStore', - wrapperNames: ['SyncExternalStore'], }); return value; } @@ -511,7 +494,6 @@ function useTransition(): [ value: isPending, debugInfo: null, dispatcherHookName: 'Transition', - wrapperNames: ['Transition'], }); return [isPending, () => {}]; } @@ -526,7 +508,6 @@ function useDeferredValue(value: T, initialValue?: T): T { value: prevValue, debugInfo: null, dispatcherHookName: 'DeferredValue', - wrapperNames: ['DeferredValue'], }); return prevValue; } @@ -541,7 +522,6 @@ function useId(): string { value: id, debugInfo: null, dispatcherHookName: 'Id', - wrapperNames: ['Id'], }); return id; } @@ -593,7 +573,6 @@ function useOptimistic( value: state, debugInfo: null, dispatcherHookName: 'Optimistic', - wrapperNames: ['Optimistic'], }); return [state, (action: A) => {}]; } @@ -654,7 +633,6 @@ function useFormState( value: value, debugInfo: debugInfo, dispatcherHookName: 'FormState', - wrapperNames: ['FormState'], }); if (error !== null) { @@ -725,7 +703,6 @@ function useActionState( value: value, debugInfo: debugInfo, dispatcherHookName: 'ActionState', - wrapperNames: ['ActionState'], }); if (error !== null) { @@ -756,10 +733,6 @@ function useHostTransitionStatus(): TransitionStatus { value: status, debugInfo: null, dispatcherHookName: 'HostTransitionStatus', - wrapperNames: [ - // react-dom - 'FormStatus', - ], }); return status; @@ -909,15 +882,7 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) ) { i++; - } - for (let j = 0; j < hook.wrapperNames.length; j++) { - const wrapperName = hook.wrapperNames[j]; - if ( - i < hookStack.length - 1 && - isReactWrapper(hookStack[i].functionName, wrapperName) - ) { - i++; - } + i++; } return i; } From de678941df52a1b0622d0c56ef620c3c2a1de331 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Tue, 16 Apr 2024 10:17:09 +0200 Subject: [PATCH 7/7] Guard against inlined dispatcher calls --- packages/react-debug-tools/src/ReactDebugHooks.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index a5be893f1d7a5..e7f3fb0ee58ae 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -882,7 +882,11 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) ) { i++; - i++; + // Guard against the dispatcher call being inlined. + // At this point we wouldn't be able to recover the actual React Hook name. + if (i < hookStack.length - 1) { + i++; + } } return i; }