Repository: facebook/react
Pull request: DevTools: Read context values from context dependencies

## Summary

Instead of walking up the fiber tree to fill context values from the provider values we now move a cursor along the context dependencies whenever we read context.

This will allow us to read from host specific contexts where we don't have access to the context object e.g. `useFormStatus()`. 

For React versions prior to https://github.com/facebook/react/pull/20890 we still need to walk the fiber tree up to visit context providers since we don't have `memoizedValue` on the context dependency in this case.

## How did you test this change?

- [x] react-debug-tools tests
- [x] inspected components with context hooks (e.g. FunctionalContextConsumerWithUpdates) in the shell
- [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 Canary: https://codesandbox.io/p/sandbox/canary-react-devtools-context-inspection-9y5gkc


Changed files:
filename: packages/react-debug-tools/src/ReactDebugHooks.js
patch: @@ -16,6 +16,8 @@ import type {
   ReactDebugInfo,
 } from 'shared/ReactTypes';
 import type {
+  ContextDependency,
+  Dependencies,
   Fiber,
   Dispatcher as DispatcherType,
 } from 'react-reconciler/src/ReactInternalTypes';
@@ -33,6 +35,7 @@ import {
   REACT_MEMO_CACHE_SENTINEL,
   REACT_CONTEXT_TYPE,
 } from 'shared/ReactSymbols';
+import hasOwnProperty from 'shared/hasOwnProperty';
 
 type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher;
 
@@ -139,6 +142,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
 
 let currentFiber: null | Fiber = null;
 let currentHook: null | Hook = null;
+let currentContextDependency: null | ContextDependency<mixed> = null;
 
 function nextHook(): null | Hook {
   const hook = currentHook;
@@ -149,8 +153,29 @@ function nextHook(): null | Hook {
 }
 
 function readContext<T>(context: ReactContext<T>): T {
-  // For now we don't expose readContext usage in the hooks debugging info.
-  return context._currentValue;
+  if (currentFiber === null) {
+    // Hook inspection without access to the Fiber tree
+    // e.g. when warming up the primitive stack cache or during `ReactDebugTools.inspectHooks()`.
+    return context._currentValue;
+  } else {
+    if (currentContextDependency === null) {
+      throw new Error(
+        'Context reads do not line up with context dependencies. This is a bug in React Debug Tools.',
+      );
+    }
+
+    // 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;
+
+    return value;
+  }
 }
 
 const SuspenseException: mixed = new Error(
@@ -218,14 +243,15 @@ function use<T>(usable: Usable<T>): T {
 }
 
 function useContext<T>(context: ReactContext<T>): T {
+  const value = readContext(context);
   hookLog.push({
     displayName: context.displayName || null,
     primitive: 'Context',
     stackError: new Error(),
-    value: context._currentValue,
+    value: value,
     debugInfo: null,
   });
-  return context._currentValue;
+  return value;
 }
 
 function useState<S>(
@@ -1090,15 +1116,44 @@ export function inspectHooksOfFiber(
   currentHook = (fiber.memoizedState: Hook);
   currentFiber = fiber;
 
+  if (hasOwnProperty.call(currentFiber, 'dependencies')) {
+    // $FlowFixMe[incompatible-use]: Flow thinks hasOwnProperty might have nulled `currentFiber`
+    const dependencies = currentFiber.dependencies;
+    currentContextDependency =
+      dependencies !== null ? dependencies.firstContext : null;
+  } else if (hasOwnProperty.call(currentFiber, 'dependencies_old')) {
+    const dependencies: Dependencies = (currentFiber: any).dependencies_old;
+    currentContextDependency =
+      dependencies !== null ? dependencies.firstContext : null;
+  } else if (hasOwnProperty.call(currentFiber, 'dependencies_new')) {
+    const dependencies: Dependencies = (currentFiber: any).dependencies_new;
+    currentContextDependency =
+      dependencies !== null ? dependencies.firstContext : null;
+  } else if (hasOwnProperty.call(currentFiber, 'contextDependencies')) {
+    const contextDependencies = (currentFiber: any).contextDependencies;
+    currentContextDependency =
+      contextDependencies !== null ? contextDependencies.first : null;
+  } else {
+    throw new Error(
+      'Unsupported React version. This is a bug in React Debug Tools.',
+    );
+  }
+
   const type = fiber.type;
   let props = fiber.memoizedProps;
   if (type !== fiber.elementType) {
     props = resolveDefaultProps(type, props);
   }
 
+  // Only used for versions of React without memoized context value in context dependencies.
   const contextMap = new Map<ReactContext<any>, any>();
   try {
-    setupContexts(contextMap, fiber);
+    if (
+      currentContextDependency !== null &&
+      !hasOwnProperty.call(currentContextDependency, 'memoizedValue')
+    ) {
+      setupContexts(contextMap, fiber);
+    }
 
     if (fiber.tag === ForwardRef) {
       return inspectHooksOfForwardRef(
@@ -1113,6 +1168,7 @@ export function inspectHooksOfFiber(
   } finally {
     currentFiber = null;
     currentHook = null;
+    currentContextDependency = null;
 
     restoreContexts(contextMap);
   }