Skip to content

DevTools: Read context values from context dependencies #28467

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
66 changes: 61 additions & 5 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
ReactDebugInfo,
} from 'shared/ReactTypes';
import type {
ContextDependency,
Dependencies,
Fiber,
Dispatcher as DispatcherType,
} from 'react-reconciler/src/ReactInternalTypes';
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Copy link
Collaborator Author

@eps1lon eps1lon Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only for ReactDebugTools.inspectHooks where we don't have access to the current fiber (we have tests that would break if we throw just because currentContextDependency is null).

} 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(
Expand Down Expand Up @@ -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>(
Expand Down Expand Up @@ -1083,15 +1109,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(
Expand All @@ -1106,6 +1161,7 @@ export function inspectHooksOfFiber(
} finally {
currentFiber = null;
currentHook = null;
currentContextDependency = null;

restoreContexts(contextMap);
}
Expand Down