Repository: facebook/react
Pull request: [Debug Tools] Introspect Promises in use()

Alternative to #28295.

Instead of stashing all of the Usables eagerly, we can extract them by replaying the render when we need them like we do with any other hook. We already had an implementation of `use()` but it wasn't quite complete.

These can also include further DebugInfo on them such as what Server Component rendered the Promise or async debug info. This is nice just to see which use() calls were made in the side-panel but it can also be used to gather everything that might have suspended.

Together with https://github.com/facebook/react/pull/28286 we cover the case when a Promise was used a child and if it was unwrapped with use(). Notably we don't cover a Promise that was thrown (although we do support that in a Server Component which maybe we shouldn't). Throwing a Promise isn't officially supported though and that use case should move to the use() Hook.

The pattern of conditionally suspending based on cache also isn't really supported with the use() pattern. You should always call use() if you previously called use() with the same input. This also ensures that we can track what might have suspended rather than what actually did.

One limitation of this strategy is that it's hard to find all the places something might suspend in a tree without rerendering all the fibers again. So we might need to still add something to the tree to indicate which Fibers may have further debug info / thenables.

Changed files:
filename: packages/react-debug-tools/src/ReactDebugHooks.js
patch: @@ -13,6 +13,8 @@ import type {
   ReactProviderType,
   StartTransitionOptions,
   Usable,
+  Thenable,
+  ReactDebugInfo,
 } from 'shared/ReactTypes';
 import type {
   Fiber,
@@ -41,6 +43,7 @@ type HookLogEntry = {
   primitive: string,
   stackError: Error,
   value: mixed,
+  debugInfo: ReactDebugInfo | null,
 };
 
 let hookLog: Array<HookLogEntry> = [];
@@ -93,6 +96,27 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
         // This type check is for Flow only.
         Dispatcher.useFormState((s: mixed, p: mixed) => s, null);
       }
+      if (typeof Dispatcher.use === 'function') {
+        // This type check is for Flow only.
+        Dispatcher.use(
+          ({
+            $$typeof: REACT_CONTEXT_TYPE,
+            _currentValue: null,
+          }: any),
+        );
+        Dispatcher.use({
+          then() {},
+          status: 'fulfilled',
+          value: null,
+        });
+        try {
+          Dispatcher.use(
+            ({
+              then() {},
+            }: any),
+          );
+        } catch (x) {}
+      }
     } finally {
       readHookLog = hookLog;
       hookLog = [];
@@ -122,22 +146,57 @@ function readContext<T>(context: ReactContext<T>): T {
   return context._currentValue;
 }
 
+const SuspenseException: mixed = new Error(
+  "Suspense Exception: This is not a real error! It's an implementation " +
+    'detail of `use` to interrupt the current render. You must either ' +
+    'rethrow it immediately, or move the `use` call outside of the ' +
+    '`try/catch` block. Capturing without rethrowing will lead to ' +
+    'unexpected behavior.\n\n' +
+    'To handle async errors, wrap your component in an error boundary, or ' +
+    "call the promise's `.catch` method and pass the result to `use`",
+);
+
 function use<T>(usable: Usable<T>): T {
   if (usable !== null && typeof usable === 'object') {
     // $FlowFixMe[method-unbinding]
     if (typeof usable.then === 'function') {
-      // TODO: What should this do if it receives an unresolved promise?
-      throw new Error(
-        'Support for `use(Promise)` not yet implemented in react-debug-tools.',
-      );
+      const thenable: Thenable<any> = (usable: any);
+      switch (thenable.status) {
+        case 'fulfilled': {
+          const fulfilledValue: T = thenable.value;
+          hookLog.push({
+            primitive: 'Promise',
+            stackError: new Error(),
+            value: fulfilledValue,
+            debugInfo:
+              thenable._debugInfo === undefined ? null : thenable._debugInfo,
+          });
+          return fulfilledValue;
+        }
+        case 'rejected': {
+          const rejectedError = thenable.reason;
+          throw rejectedError;
+        }
+      }
+      // If this was an uncached Promise we have to abandon this attempt
+      // but we can still emit anything up until this point.
+      hookLog.push({
+        primitive: 'Unresolved',
+        stackError: new Error(),
+        value: thenable,
+        debugInfo:
+          thenable._debugInfo === undefined ? null : thenable._debugInfo,
+      });
+      throw SuspenseException;
     } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
       const context: ReactContext<T> = (usable: any);
       const value = readContext(context);
 
       hookLog.push({
-        primitive: 'Use',
+        primitive: 'Context (use)',
         stackError: new Error(),
         value,
+        debugInfo: null,
       });
 
       return value;
@@ -153,6 +212,7 @@ function useContext<T>(context: ReactContext<T>): T {
     primitive: 'Context',
     stackError: new Error(),
     value: context._currentValue,
+    debugInfo: null,
   });
   return context._currentValue;
 }
@@ -168,7 +228,12 @@ function useState<S>(
       ? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
         initialState()
       : initialState;
-  hookLog.push({primitive: 'State', stackError: new Error(), value: state});
+  hookLog.push({
+    primitive: 'State',
+    stackError: new Error(),
+    value: state,
+    debugInfo: null,
+  });
   return [state, (action: BasicStateAction<S>) => {}];
 }
 
@@ -188,6 +253,7 @@ function useReducer<S, I, A>(
     primitive: 'Reducer',
     stackError: new Error(),
     value: state,
+    debugInfo: null,
   });
   return [state, (action: A) => {}];
 }
@@ -199,6 +265,7 @@ function useRef<T>(initialValue: T): {current: T} {
     primitive: 'Ref',
     stackError: new Error(),
     value: ref.current,
+    debugInfo: null,
   });
   return ref;
 }
@@ -209,6 +276,7 @@ function useCacheRefresh(): () => void {
     primitive: 'CacheRefresh',
     stackError: new Error(),
     value: hook !== null ? hook.memoizedState : function refresh() {},
+    debugInfo: null,
   });
   return () => {};
 }
@@ -222,6 +290,7 @@ function useLayoutEffect(
     primitive: 'LayoutEffect',
     stackError: new Error(),
     value: create,
+    debugInfo: null,
   });
 }
 
@@ -234,6 +303,7 @@ function useInsertionEffect(
     primitive: 'InsertionEffect',
     stackError: new Error(),
     value: create,
+    debugInfo: null,
   });
 }
 
@@ -242,7 +312,12 @@ function useEffect(
   inputs: Array<mixed> | void | null,
 ): void {
   nextHook();
-  hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
+  hookLog.push({
+    primitive: 'Effect',
+    stackError: new Error(),
+    value: create,
+    debugInfo: null,
+  });
 }
 
 function useImperativeHandle<T>(
@@ -263,6 +338,7 @@ function useImperativeHandle<T>(
     primitive: 'ImperativeHandle',
     stackError: new Error(),
     value: instance,
+    debugInfo: null,
   });
 }
 
@@ -271,6 +347,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
     primitive: 'DebugValue',
     stackError: new Error(),
     value: typeof formatterFn === 'function' ? formatterFn(value) : value,
+    debugInfo: null,
   });
 }
 
@@ -280,6 +357,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
     primitive: 'Callback',
     stackError: new Error(),
     value: hook !== null ? hook.memoizedState[0] : callback,
+    debugInfo: null,
   });
   return callback;
 }
@@ -290,7 +368,12 @@ function useMemo<T>(
 ): T {
   const hook = nextHook();
   const value = hook !== null ? hook.memoizedState[0] : nextCreate();
-  hookLog.push({primitive: 'Memo', stackError: new Error(), value});
+  hookLog.push({
+    primitive: 'Memo',
+    stackError: new Error(),
+    value,
+    debugInfo: null,
+  });
   return value;
 }
 
@@ -309,6 +392,7 @@ function useSyncExternalStore<T>(
     primitive: 'SyncExternalStore',
     stackError: new Error(),
     value,
+    debugInfo: null,
   });
   return value;
 }
@@ -326,6 +410,7 @@ function useTransition(): [
     primitive: 'Transition',
     stackError: new Error(),
     value: undefined,
+    debugInfo: null,
   });
   return [false, callback => {}];
 }
@@ -336,6 +421,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
     primitive: 'DeferredValue',
     stackError: new Error(),
     value: hook !== null ? hook.memoizedState : value,
+    debugInfo: null,
   });
   return value;
 }
@@ -347,6 +433,7 @@ function useId(): string {
     primitive: 'Id',
     stackError: new Error(),
     value: id,
+    debugInfo: null,
   });
   return id;
 }
@@ -395,6 +482,7 @@ function useOptimistic<S, A>(
     primitive: 'Optimistic',
     stackError: new Error(),
     value: state,
+    debugInfo: null,
   });
   return [state, (action: A) => {}];
 }
@@ -416,6 +504,7 @@ function useFormState<S, P>(
     primitive: 'FormState',
     stackError: new Error(),
     value: state,
+    debugInfo: null,
   });
   return [state, (payload: P) => {}];
 }
@@ -480,6 +569,7 @@ export type HooksNode = {
   name: string,
   value: mixed,
   subHooks: Array<HooksNode>,
+  debugInfo: null | ReactDebugInfo,
   hookSource?: HookSource,
 };
 export type HooksTree = Array<HooksNode>;
@@ -546,6 +636,15 @@ function isReactWrapper(functionName: any, primitiveName: string) {
   if (!functionName) {
     return false;
   }
+  switch (primitiveName) {
+    case 'Context':
+    case 'Context (use)':
+    case 'Promise':
+    case 'Unresolved':
+      if (functionName.endsWith('use')) {
+        return true;
+      }
+  }
   const expectedPrimitiveName = 'use' + primitiveName;
   if (functionName.length < expectedPrimitiveName.length) {
     return false;
@@ -661,6 +760,7 @@ function buildTree(
           name: parseCustomHookName(stack[j - 1].functionName),
           value: undefined,
           subHooks: children,
+          debugInfo: null,
         };
 
         if (includeHooksSource) {
@@ -678,25 +778,29 @@ function buildTree(
       }
       prevStack = stack;
     }
-    const {primitive} = hook;
+    const {primitive, debugInfo} = hook;
 
     // For now, the "id" of stateful hooks is just the stateful hook index.
     // Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
     const id =
       primitive === 'Context' ||
+      primitive === 'Context (use)' ||
       primitive === 'DebugValue' ||
-      primitive === 'Use'
+      primitive === 'Promise' ||
+      primitive === 'Unresolved'
         ? null
         : nativeHookID++;
 
     // For the time being, only State and Reducer hooks support runtime overrides.
     const isStateEditable = primitive === 'Reducer' || primitive === 'State';
+    const name = primitive === 'Context (use)' ? 'Context' : primitive;
     const levelChild: HooksNode = {
       id,
       isStateEditable,
-      name: primitive,
+      name: name,
       value: hook.value,
       subHooks: [],
+      debugInfo: debugInfo,
     };
 
     if (includeHooksSource) {
@@ -762,6 +866,11 @@ function processDebugValues(
 
 function handleRenderFunctionError(error: any): void {
   // original error might be any type.
+  if (error === SuspenseException) {
+    // An uncached Promise was used. We can't synchronously resolve the rest of
+    // the Hooks but we can at least show what ever we got so far.
+    return;
+  }
   if (
     error instanceof Error &&
     error.name === 'ReactDebugToolsUnsupportedHookError'

filename: packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
patch: @@ -31,6 +31,7 @@ describe('ReactHooksInspection', () => {
         id: 0,
         name: 'State',
         value: 'hello world',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -53,12 +54,14 @@ describe('ReactHooksInspection', () => {
         id: null,
         name: 'Custom',
         value: __DEV__ ? 'custom hook label' : undefined,
+        debugInfo: null,
         subHooks: [
           {
             isStateEditable: true,
             id: 0,
             name: 'State',
             value: 'hello world',
+            debugInfo: null,
             subHooks: [],
           },
         ],
@@ -89,18 +92,21 @@ describe('ReactHooksInspection', () => {
         id: null,
         name: 'Custom',
         value: undefined,
+        debugInfo: null,
         subHooks: [
           {
             isStateEditable: true,
             id: 0,
             name: 'State',
+            debugInfo: null,
             subHooks: [],
             value: 'hello',
           },
           {
             isStateEditable: false,
             id: 1,
             name: 'Effect',
+            debugInfo: null,
             subHooks: [],
             value: effect,
           },
@@ -111,19 +117,22 @@ describe('ReactHooksInspection', () => {
         id: null,
         name: 'Custom',
         value: undefined,
+        debugInfo: null,
         subHooks: [
           {
             isStateEditable: true,
             id: 2,
             name: 'State',
             value: 'world',
+            debugInfo: null,
             subHooks: [],
           },
           {
             isStateEditable: false,
             id: 3,
             name: 'Effect',
             value: effect,
+            debugInfo: null,
             subHooks: [],
           },
         ],
@@ -164,25 +173,29 @@ describe('ReactHooksInspection', () => {
         id: null,
         name: 'Bar',
         value: undefined,
+        debugInfo: null,
         subHooks: [
           {
             isStateEditable: false,
             id: null,
             name: 'Custom',
             value: undefined,
+            debugInfo: null,
             subHooks: [
               {
                 isStateEditable: true,
                 id: 0,
                 name: 'Reducer',
                 value: 'hello',
+                debugInfo: null,
                 subHooks: [],
               },
               {
                 isStateEditable: false,
                 id: 1,
                 name: 'Effect',
                 value: effect,
+                debugInfo: null,
                 subHooks: [],
               },
             ],
@@ -192,6 +205,7 @@ describe('ReactHooksInspection', () => {
             id: 2,
             name: 'LayoutEffect',
             value: effect,
+            debugInfo: null,
             subHooks: [],
           },
         ],
@@ -201,30 +215,35 @@ describe('ReactHooksInspection', () => {
         id: null,
         name: 'Baz',
         value: undefined,
+        debugInfo: null,
         subHooks: [
           {
             isStateEditable: false,
             id: 3,
             name: 'LayoutEffect',
             value: effect,
+            debugInfo: null,
             subHooks: [],
           },
           {
             isStateEditable: false,
             id: null,
             name: 'Custom',
+            debugInfo: null,
             subHooks: [
               {
                 isStateEditable: true,
                 id: 4,
                 name: 'Reducer',
+                debugInfo: null,
                 subHooks: [],
                 value: 'world',
               },
               {
                 isStateEditable: false,
                 id: 5,
                 name: 'Effect',
+                debugInfo: null,
                 subHooks: [],
                 value: effect,
               },
@@ -249,6 +268,7 @@ describe('ReactHooksInspection', () => {
         id: null,
         name: 'Context',
         value: 'default',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -287,6 +307,86 @@ describe('ReactHooksInspection', () => {
     expect(setterCalls[1]).toBe(initial);
   });
 
+  it('should inspect use() calls for Promise and Context', async () => {
+    const MyContext = React.createContext('hi');
+    const promise = Promise.resolve('world');
+    await promise;
+    promise.status = 'fulfilled';
+    promise.value = 'world';
+    promise._debugInfo = [{name: 'Hello'}];
+
+    function useCustom() {
+      const value = React.use(promise);
+      const [state] = React.useState(value);
+      return state;
+    }
+    function Foo(props) {
+      const value1 = React.use(MyContext);
+      const value2 = useCustom();
+      return (
+        <div>
+          {value1} {value2}
+        </div>
+      );
+    }
+    const tree = ReactDebugTools.inspectHooks(Foo, {});
+    expect(tree).toEqual([
+      {
+        isStateEditable: false,
+        id: null,
+        name: 'Context',
+        value: 'hi',
+        debugInfo: null,
+        subHooks: [],
+      },
+      {
+        isStateEditable: false,
+        id: null,
+        name: 'Custom',
+        value: undefined,
+        debugInfo: null,
+        subHooks: [
+          {
+            isStateEditable: false,
+            id: null,
+            name: 'Promise',
+            value: 'world',
+            debugInfo: [{name: 'Hello'}],
+            subHooks: [],
+          },
+          {
+            isStateEditable: true,
+            id: 0,
+            name: 'State',
+            value: 'world',
+            debugInfo: null,
+            subHooks: [],
+          },
+        ],
+      },
+    ]);
+  });
+
+  it('should inspect use() calls for unresolved Promise', () => {
+    const promise = Promise.resolve('hi');
+
+    function Foo(props) {
+      const value = React.use(promise);
+      return <div>{value}</div>;
+    }
+    const tree = ReactDebugTools.inspectHooks(Foo, {});
+    expect(tree).toEqual([
+      {
+        isStateEditable: false,
+        id: null,
+        name: 'Unresolved',
+        value: promise,
+        debugInfo: null,
+        subHooks: [],
+      },
+    ]);
+  });
+
   describe('useDebugValue', () => {
     it('should be ignored when called outside of a custom hook', () => {
       function Foo(props) {
@@ -313,11 +413,13 @@ describe('ReactHooksInspection', () => {
           id: null,
           name: 'Custom',
           value: __DEV__ ? 'bar:123' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 0,
               name: 'State',
+              debugInfo: null,
               subHooks: [],
               value: 0,
             },

filename: packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
patch: @@ -48,13 +48,15 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'hello',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'State',
         value: 'world',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -73,13 +75,15 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'Hi',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'State',
         value: 'world',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -95,13 +99,15 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'Hi',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'State',
         value: 'world!',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -157,49 +163,63 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'a',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'Reducer',
         value: 'b',
+        debugInfo: null,
+        subHooks: [],
+      },
+      {
+        isStateEditable: false,
+        id: 2,
+        name: 'Ref',
+        value: 'c',
+        debugInfo: null,
         subHooks: [],
       },
-      {isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
       {
         isStateEditable: false,
         id: 3,
         name: 'LayoutEffect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 4,
         name: 'Effect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 5,
         name: 'ImperativeHandle',
         value: outsideRef.current,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 6,
         name: 'Memo',
         value: 'ab',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 7,
         name: 'Callback',
         value: updateStates,
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -217,49 +237,63 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'A',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'Reducer',
         value: 'B',
+        debugInfo: null,
+        subHooks: [],
+      },
+      {
+        isStateEditable: false,
+        id: 2,
+        name: 'Ref',
+        value: 'C',
+        debugInfo: null,
         subHooks: [],
       },
-      {isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
       {
         isStateEditable: false,
         id: 3,
         name: 'LayoutEffect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 4,
         name: 'Effect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 5,
         name: 'ImperativeHandle',
         value: outsideRef.current,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 6,
         name: 'Memo',
         value: 'Ab',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 7,
         name: 'Callback',
         value: updateStates,
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -317,56 +351,71 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'a',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'Reducer',
         value: 'b',
+        debugInfo: null,
+        subHooks: [],
+      },
+      {
+        isStateEditable: false,
+        id: 2,
+        name: 'Ref',
+        value: 'c',
+        debugInfo: null,
         subHooks: [],
       },
-      {isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
       {
         isStateEditable: false,
         id: 3,
         name: 'InsertionEffect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 4,
         name: 'LayoutEffect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 5,
         name: 'Effect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 6,
         name: 'ImperativeHandle',
         value: outsideRef.current,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 7,
         name: 'Memo',
         value: 'ab',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 8,
         name: 'Callback',
         value: updateStates,
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -384,56 +433,71 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'A',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 1,
         name: 'Reducer',
         value: 'B',
+        debugInfo: null,
+        subHooks: [],
+      },
+      {
+        isStateEditable: false,
+        id: 2,
+        name: 'Ref',
+        value: 'C',
+        debugInfo: null,
         subHooks: [],
       },
-      {isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
       {
         isStateEditable: false,
         id: 3,
         name: 'InsertionEffect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 4,
         name: 'LayoutEffect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 5,
         name: 'Effect',
         value: effect,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 6,
         name: 'ImperativeHandle',
         value: outsideRef.current,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 7,
         name: 'Memo',
         value: 'Ab',
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: false,
         id: 8,
         name: 'Callback',
         value: updateStates,
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -458,6 +522,7 @@ describe('ReactHooksInspectionIntegration', () => {
         id: null,
         name: 'Context',
         value: 'contextual',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -480,6 +545,7 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'ImperativeHandle',
         value: obj,
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -501,6 +567,7 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'hello',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -524,12 +591,14 @@ describe('ReactHooksInspectionIntegration', () => {
         id: null,
         name: 'Custom',
         value: undefined,
+        debugInfo: null,
         subHooks: [
           {
             isStateEditable: true,
             id: 0,
             name: 'State',
             value: 'hello',
+            debugInfo: null,
             subHooks: [],
           },
         ],
@@ -553,20 +622,23 @@ describe('ReactHooksInspectionIntegration', () => {
         isStateEditable: false,
         name: 'Transition',
         value: undefined,
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 1,
         isStateEditable: false,
         name: 'Memo',
         value: 'hello',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 2,
         isStateEditable: false,
         name: 'Memo',
         value: 'not used',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -588,20 +660,23 @@ describe('ReactHooksInspectionIntegration', () => {
         isStateEditable: false,
         name: 'DeferredValue',
         value: 'abc',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 1,
         isStateEditable: false,
         name: 'Memo',
         value: 1,
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 2,
         isStateEditable: false,
         name: 'Memo',
         value: 2,
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -630,6 +705,7 @@ describe('ReactHooksInspectionIntegration', () => {
       isStateEditable: true,
       name: 'State',
       value: 'hello',
+      debugInfo: null,
       subHooks: [],
     });
   });
@@ -721,12 +797,14 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'LabeledValue',
           value: __DEV__ ? 'custom label a' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 0,
               name: 'State',
               value: 'a',
+              debugInfo: null,
               subHooks: [],
             },
           ],
@@ -736,19 +814,22 @@ describe('ReactHooksInspectionIntegration', () => {
           id: 1,
           name: 'State',
           value: 'b',
+          debugInfo: null,
           subHooks: [],
         },
         {
           isStateEditable: false,
           id: null,
           name: 'Anonymous',
           value: undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 2,
               name: 'State',
               value: 'c',
+              debugInfo: null,
               subHooks: [],
             },
           ],
@@ -758,12 +839,14 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'LabeledValue',
           value: __DEV__ ? 'custom label d' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 3,
               name: 'State',
               value: 'd',
+              debugInfo: null,
               subHooks: [],
             },
           ],
@@ -793,18 +876,21 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'Outer',
           value: __DEV__ ? 'outer' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: false,
               id: null,
               name: 'Inner',
               value: __DEV__ ? 'inner' : undefined,
+              debugInfo: null,
               subHooks: [
                 {
                   isStateEditable: true,
                   id: 0,
                   name: 'State',
                   value: 0,
+                  debugInfo: null,
                   subHooks: [],
                 },
               ],
@@ -840,12 +926,14 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'SingleLabelCustom',
           value: __DEV__ ? 'single one' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 0,
               name: 'State',
               value: 0,
+              debugInfo: null,
               subHooks: [],
             },
           ],
@@ -855,12 +943,14 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'MultiLabelCustom',
           value: __DEV__ ? ['one', 'two', 'three'] : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 1,
               name: 'State',
               value: 0,
+              debugInfo: null,
               subHooks: [],
             },
           ],
@@ -870,12 +960,14 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'SingleLabelCustom',
           value: __DEV__ ? 'single two' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 2,
               name: 'State',
               value: 0,
+              debugInfo: null,
               subHooks: [],
             },
           ],
@@ -912,11 +1004,13 @@ describe('ReactHooksInspectionIntegration', () => {
           id: null,
           name: 'Custom',
           value: __DEV__ ? 'bar:123' : undefined,
+          debugInfo: null,
           subHooks: [
             {
               isStateEditable: true,
               id: 0,
               name: 'State',
+              debugInfo: null,
               subHooks: [],
               value: 0,
             },
@@ -963,6 +1057,7 @@ describe('ReactHooksInspectionIntegration', () => {
         id: 0,
         name: 'State',
         value: 'def',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -1056,13 +1151,15 @@ describe('ReactHooksInspectionIntegration', () => {
         id: null,
         name: 'Context',
         value: 1,
+        debugInfo: null,
         subHooks: [],
       },
       {
         isStateEditable: true,
         id: 0,
         name: 'State',
         value: {count: 2},
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -1089,20 +1186,23 @@ describe('ReactHooksInspectionIntegration', () => {
         isStateEditable: false,
         name: 'SyncExternalStore',
         value: 'snapshot',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 1,
         isStateEditable: false,
         name: 'Memo',
         value: 'memo',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 2,
         isStateEditable: false,
         name: 'Memo',
         value: 'not used',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -1125,22 +1225,25 @@ describe('ReactHooksInspectionIntegration', () => {
       {
         id: null,
         isStateEditable: false,
-        name: 'Use',
+        name: 'Context',
         value: 'default',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 0,
         isStateEditable: false,
         name: 'Memo',
         value: 'memo',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 1,
         isStateEditable: false,
         name: 'Memo',
         value: 'not used',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -1165,20 +1268,23 @@ describe('ReactHooksInspectionIntegration', () => {
         isStateEditable: false,
         name: 'Optimistic',
         value: 'abc',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 1,
         isStateEditable: false,
         name: 'Memo',
         value: 'memo',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 2,
         isStateEditable: false,
         name: 'Memo',
         value: 'not used',
+        debugInfo: null,
         subHooks: [],
       },
     ]);
@@ -1205,20 +1311,23 @@ describe('ReactHooksInspectionIntegration', () => {
         isStateEditable: false,
         name: 'FormState',
         value: 0,
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 1,
         isStateEditable: false,
         name: 'Memo',
         value: 'memo',
+        debugInfo: null,
         subHooks: [],
       },
       {
         id: 2,
         isStateEditable: false,
         name: 'Memo',
         value: 'not used',
+        debugInfo: null,
         subHooks: [],
       },
     ]);

filename: packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
patch: @@ -198,6 +198,7 @@ describe('InspectedElement', () => {
         "events": undefined,
         "hooks": [
           {
+            "debugInfo": null,
             "hookSource": {
               "columnNumber": "removed by Jest serializer",
               "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -240,6 +241,7 @@ describe('InspectedElement', () => {
         "events": undefined,
         "hooks": [
           {
+            "debugInfo": null,
             "hookSource": {
               "columnNumber": "removed by Jest serializer",
               "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1157,6 +1159,7 @@ describe('InspectedElement', () => {
     expect(inspectedElement.hooks).toMatchInlineSnapshot(`
       [
         {
+          "debugInfo": null,
           "hookSource": {
             "columnNumber": "removed by Jest serializer",
             "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1184,6 +1187,7 @@ describe('InspectedElement', () => {
     expect(inspectedElement.hooks).toMatchInlineSnapshot(`
       [
         {
+          "debugInfo": null,
           "hookSource": {
             "columnNumber": "removed by Jest serializer",
             "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1659,6 +1663,7 @@ describe('InspectedElement', () => {
         "events": undefined,
         "hooks": [
           {
+            "debugInfo": null,
             "hookSource": {
               "columnNumber": "removed by Jest serializer",
               "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1700,6 +1705,7 @@ describe('InspectedElement', () => {
         "events": undefined,
         "hooks": [
           {
+            "debugInfo": null,
             "hookSource": {
               "columnNumber": "removed by Jest serializer",
               "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1945,6 +1951,7 @@ describe('InspectedElement', () => {
     expect(hooks).toMatchInlineSnapshot(`
       [
         {
+          "debugInfo": null,
           "hookSource": {
             "columnNumber": "removed by Jest serializer",
             "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1956,6 +1963,7 @@ describe('InspectedElement', () => {
           "name": "DebuggableHook",
           "subHooks": [
             {
+              "debugInfo": null,
               "hookSource": {
                 "columnNumber": "removed by Jest serializer",
                 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -2239,6 +2247,7 @@ describe('InspectedElement', () => {
         {
           "hooks": [
             {
+              "debugInfo": null,
               "hookSource": {
                 "columnNumber": "removed by Jest serializer",
                 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -2275,6 +2284,7 @@ describe('InspectedElement', () => {
         {
           "hooks": [
             {
+              "debugInfo": null,
               "hookSource": {
                 "columnNumber": "removed by Jest serializer",
                 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -2311,6 +2321,7 @@ describe('InspectedElement', () => {
         {
           "hooks": [
             {
+              "debugInfo": null,
               "hookSource": {
                 "columnNumber": "removed by Jest serializer",
                 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",