Repository: facebook/react
Pull request: DevTools: Fix inspecting components with multiple reads of the same Context in React 17

Stack:
1. https://github.com/facebook/react/pull/28975
1. https://github.com/facebook/react/pull/28976
1. https://github.com/facebook/react/pull/28974 <--- You're here

## Summary

Closes https://github.com/facebook/react/issues/28960

In https://github.com/facebook/react/pull/28467, we started reading context values by advancing a cursor in the context dependency list. However, in React 17, multiple reads of the same context are only tracked as a single dependency so we don't know if we should advance or not. Keep in mind that the passed `context` should be ignored in later versions of React where the real context object may not be available (e.g. in `useFormStatus`).

So for React 17 and below we just completely fallback to the old approach of reading the current value from the context object. In those versions `useFormStatus` doesn't exist.

## How did you test this change?


- [x] react-debug-tools tests
- [x] Backwards compat with 16.8: https://codesandbox.io/p/sandbox/serverless-snowflake-ttrp8v 
- [x] Backwards compat with 17.0: https://codesandbox.io/p/sandbox/17-0-react-devtools-context-inspection-forked-rslrsq
- [x] Backwards compat with 18.0: https://codesandbox.io/p/sandbox/18-0-react-devtools-context-inspection-p8ntzj
- [x] Compat with Beta: https://codesandbox.io/p/sandbox/canary-react-devtools-context-inspection-9y5gkc

Changed files:
filename: packages/react-debug-tools/src/ReactDebugHooks.js
patch: @@ -177,15 +177,20 @@ function readContext<T>(context: ReactContext<T>): T {
       );
     }
 
+    let value: T;
     // For now we don't expose readContext usage in the hooks debugging info.
-    const value = hasOwnProperty.call(currentContextDependency, 'memoizedValue')
-      ? // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`
-        ((currentContextDependency.memoizedValue: any): T)
-      : // Before React 18, we did not have `memoizedValue` so we rely on `setupContexts` in those versions.
-        // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`
-        ((currentContextDependency.context._currentValue: any): T);
-    // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`
-    currentContextDependency = currentContextDependency.next;
+    if (hasOwnProperty.call(currentContextDependency, 'memoizedValue')) {
+      // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`
+      value = ((currentContextDependency.memoizedValue: any): T);
+
+      // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`
+      currentContextDependency = currentContextDependency.next;
+    } else {
+      // Before React 18, we did not have `memoizedValue` so we rely on `setupContexts` in those versions.
+      // Multiple reads of the same context were also only tracked as a single dependency.
+      // We just give up on advancing context dependencies and solely rely on `setupContexts`.
+      value = context._currentValue;
+    }
 
     return value;
   }

filename: packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
patch: @@ -833,6 +833,45 @@ describe('ReactHooksInspectionIntegration', () => {
     `);
   });
 
+  // @reactVersion >= 16.8
+  it('should inspect the value of the current provider in useContext reading the same context multiple times', async () => {
+    const ContextA = React.createContext('default A');
+    const ContextB = React.createContext('default B');
+    function Foo(props) {
+      React.useContext(ContextA);
+      React.useContext(ContextA);
+      React.useContext(ContextB);
+      React.useContext(ContextB);
+      React.useContext(ContextA);
+      React.useContext(ContextB);
+      React.useContext(ContextB);
+      React.useContext(ContextB);
+      return null;
+    }
+    let renderer;
+    await act(() => {
+      renderer = ReactTestRenderer.create(
+        <ContextA.Provider value="contextual A">
+          <Foo prop="prop" />
+        </ContextA.Provider>,
+        {unstable_isConcurrent: true},
+      );
+    });
+    const childFiber = renderer.root.findByType(Foo)._currentFiber();
+    const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
+
+    expect(normalizeSourceLoc(tree)).toEqual([
+      expect.objectContaining({value: 'contextual A'}),
+      expect.objectContaining({value: 'contextual A'}),
+      expect.objectContaining({value: 'default B'}),
+      expect.objectContaining({value: 'default B'}),
+      expect.objectContaining({value: 'contextual A'}),
+      expect.objectContaining({value: 'default B'}),
+      expect.objectContaining({value: 'default B'}),
+      expect.objectContaining({value: 'default B'}),
+    ]);
+  });
+
   it('should inspect forwardRef', async () => {
     const obj = function () {};
     const Foo = React.forwardRef(function (props, ref) {