/** * 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. * * @flow */ import type { Awaited, ReactContext, StartTransitionOptions, Usable, Thenable, ReactDebugInfo, } from 'shared/ReactTypes'; import type { ContextDependency, Dependencies, Fiber, Dispatcher as DispatcherType, ContextDependencyWithSelect, } from 'react-reconciler/src/ReactInternalTypes'; import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig'; import ErrorStackParser from 'error-stack-parser'; import assign from 'shared/assign'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { FunctionComponent, SimpleMemoComponent, ContextProvider, ForwardRef, } from 'react-reconciler/src/ReactWorkTags'; import { REACT_MEMO_CACHE_SENTINEL, REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import hasOwnProperty from 'shared/hasOwnProperty'; type CurrentDispatcherRef = typeof ReactSharedInternals; // Used to track hooks called during a render type HookLogEntry = { displayName: string | null, primitive: string, stackError: Error, value: mixed, debugInfo: ReactDebugInfo | null, dispatcherHookName: string, }; let hookLog: Array = []; // Primitives type BasicStateAction = (S => S) | S; type Dispatch = A => void; let primitiveStackCache: null | Map> = null; type Hook = { memoizedState: any, next: Hook | null, }; function getPrimitiveStackCache(): Map> { // This initializes a cache of all primitive hooks so that the top // most stack frames added by calling the primitive hook can be removed. if (primitiveStackCache === null) { const cache = new Map>(); let readHookLog; try { // Use all hooks here to add them to the hook log. Dispatcher.useContext(({_currentValue: null}: any)); if (typeof Dispatcher.unstable_useContextWithBailout === 'function') { // This type check is for Flow only. Dispatcher.unstable_useContextWithBailout( ({_currentValue: null}: any), null, ); } Dispatcher.useState(null); Dispatcher.useReducer((s: mixed, a: mixed) => s, null); Dispatcher.useRef(null); if (typeof Dispatcher.useCacheRefresh === 'function') { // This type check is for Flow only. Dispatcher.useCacheRefresh(); } Dispatcher.useLayoutEffect(() => {}); Dispatcher.useInsertionEffect(() => {}); Dispatcher.useEffect(() => {}); Dispatcher.useImperativeHandle(undefined, () => null); Dispatcher.useDebugValue(null); Dispatcher.useCallback(() => {}); Dispatcher.useTransition(); Dispatcher.useSyncExternalStore( () => () => {}, () => null, () => null, ); Dispatcher.useDeferredValue(null); Dispatcher.useMemo(() => null); if (typeof Dispatcher.useMemoCache === 'function') { // This type check is for Flow only. Dispatcher.useMemoCache(0); } if (typeof Dispatcher.useOptimistic === 'function') { // This type check is for Flow only. Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); } if (typeof Dispatcher.useFormState === 'function') { // This type check is for Flow only. Dispatcher.useFormState((s: mixed, p: mixed) => s, null); } if (typeof Dispatcher.useActionState === 'function') { // This type check is for Flow only. Dispatcher.useActionState((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) {} } Dispatcher.useId(); if (typeof Dispatcher.useHostTransitionStatus === 'function') { // This type check is for Flow only. Dispatcher.useHostTransitionStatus(); } } finally { readHookLog = hookLog; hookLog = []; } for (let i = 0; i < readHookLog.length; i++) { const hook = readHookLog[i]; cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError)); } primitiveStackCache = cache; } return primitiveStackCache; } let currentFiber: null | Fiber = null; let currentHook: null | Hook = null; let currentContextDependency: | null | ContextDependency | ContextDependencyWithSelect = null; function nextHook(): null | Hook { const hook = currentHook; if (hook !== null) { currentHook = hook.next; } return hook; } function readContext(context: ReactContext): T { 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.', ); } let value: T; // For now we don't expose readContext usage in the hooks debugging info. 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; } } 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(usable: Usable): T { if (usable !== null && typeof usable === 'object') { // $FlowFixMe[method-unbinding] if (typeof usable.then === 'function') { const thenable: Thenable = (usable: any); switch (thenable.status) { case 'fulfilled': { const fulfilledValue: T = thenable.value; hookLog.push({ displayName: null, primitive: 'Promise', stackError: new Error(), value: fulfilledValue, debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', }); 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({ displayName: null, primitive: 'Unresolved', stackError: new Error(), value: thenable, debugInfo: thenable._debugInfo === undefined ? null : thenable._debugInfo, dispatcherHookName: 'Use', }); throw SuspenseException; } else if (usable.$$typeof === REACT_CONTEXT_TYPE) { const context: ReactContext = (usable: any); const value = readContext(context); hookLog.push({ displayName: context.displayName || 'Context', primitive: 'Context (use)', stackError: new Error(), value, debugInfo: null, dispatcherHookName: 'Use', }); return value; } } // eslint-disable-next-line react-internal/safe-string-coercion throw new Error('An unsupported type was passed to use(): ' + String(usable)); } function useContext(context: ReactContext): T { const value = readContext(context); hookLog.push({ displayName: context.displayName || null, primitive: 'Context', stackError: new Error(), value: value, debugInfo: null, dispatcherHookName: 'Context', }); return value; } function unstable_useContextWithBailout( context: ReactContext, select: (T => Array) | null, ): T { const value = readContext(context); hookLog.push({ displayName: context.displayName || null, primitive: 'ContextWithBailout', stackError: new Error(), value: value, debugInfo: null, dispatcherHookName: 'ContextWithBailout', }); return value; } function useState( initialState: (() => S) | S, ): [S, Dispatch>] { const hook = nextHook(); const state: S = hook !== null ? hook.memoizedState : typeof initialState === 'function' ? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types initialState() : initialState; hookLog.push({ displayName: null, primitive: 'State', stackError: new Error(), value: state, debugInfo: null, dispatcherHookName: 'State', }); return [state, (action: BasicStateAction) => {}]; } function useReducer( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch] { const hook = nextHook(); let state; if (hook !== null) { state = hook.memoizedState; } else { state = init !== undefined ? init(initialArg) : ((initialArg: any): S); } hookLog.push({ displayName: null, primitive: 'Reducer', stackError: new Error(), value: state, debugInfo: null, dispatcherHookName: 'Reducer', }); return [state, (action: A) => {}]; } function useRef(initialValue: T): {current: T} { const hook = nextHook(); const ref = hook !== null ? hook.memoizedState : {current: initialValue}; hookLog.push({ displayName: null, primitive: 'Ref', stackError: new Error(), value: ref.current, debugInfo: null, dispatcherHookName: 'Ref', }); return ref; } function useCacheRefresh(): () => void { const hook = nextHook(); hookLog.push({ displayName: null, primitive: 'CacheRefresh', stackError: new Error(), value: hook !== null ? hook.memoizedState : function refresh() {}, debugInfo: null, dispatcherHookName: 'CacheRefresh', }); return () => {}; } function useLayoutEffect( create: () => (() => void) | void, inputs: Array | void | null, ): void { nextHook(); hookLog.push({ displayName: null, primitive: 'LayoutEffect', stackError: new Error(), value: create, debugInfo: null, dispatcherHookName: 'LayoutEffect', }); } function useInsertionEffect( create: () => mixed, inputs: Array | void | null, ): void { nextHook(); hookLog.push({ displayName: null, primitive: 'InsertionEffect', stackError: new Error(), value: create, debugInfo: null, dispatcherHookName: 'InsertionEffect', }); } function useEffect( create: () => (() => void) | void, inputs: Array | void | null, ): void { nextHook(); hookLog.push({ displayName: null, primitive: 'Effect', stackError: new Error(), value: create, debugInfo: null, dispatcherHookName: 'Effect', }); } function useImperativeHandle( ref: {current: T | null} | ((inst: T | null) => mixed) | null | void, create: () => T, inputs: Array | void | null, ): void { nextHook(); // We don't actually store the instance anywhere if there is no ref callback // and if there is a ref callback it might not store it but if it does we // have no way of knowing where. So let's only enable introspection of the // ref itself if it is using the object form. let instance: ?T = undefined; if (ref !== null && typeof ref === 'object') { instance = ref.current; } hookLog.push({ displayName: null, primitive: 'ImperativeHandle', stackError: new Error(), value: instance, debugInfo: null, dispatcherHookName: 'ImperativeHandle', }); } function useDebugValue(value: any, formatterFn: ?(value: any) => any) { hookLog.push({ displayName: null, primitive: 'DebugValue', stackError: new Error(), value: typeof formatterFn === 'function' ? formatterFn(value) : value, debugInfo: null, dispatcherHookName: 'DebugValue', }); } function useCallback(callback: T, inputs: Array | void | null): T { const hook = nextHook(); hookLog.push({ displayName: null, primitive: 'Callback', stackError: new Error(), value: hook !== null ? hook.memoizedState[0] : callback, debugInfo: null, dispatcherHookName: 'Callback', }); return callback; } function useMemo( nextCreate: () => T, inputs: Array | void | null, ): T { const hook = nextHook(); const value = hook !== null ? hook.memoizedState[0] : nextCreate(); hookLog.push({ displayName: null, primitive: 'Memo', stackError: new Error(), value, debugInfo: null, dispatcherHookName: 'Memo', }); return value; } function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ): T { // useSyncExternalStore() composes multiple hooks internally. // Advance the current hook index the same number of times // so that subsequent hooks have the right memoized state. nextHook(); // SyncExternalStore nextHook(); // Effect const value = getSnapshot(); hookLog.push({ displayName: null, primitive: 'SyncExternalStore', stackError: new Error(), value, debugInfo: null, dispatcherHookName: 'SyncExternalStore', }); return value; } function useTransition(): [ boolean, (callback: () => void, options?: StartTransitionOptions) => void, ] { // useTransition() composes multiple hooks internally. // Advance the current hook index the same number of times // so that subsequent hooks have the right memoized state. const stateHook = nextHook(); nextHook(); // Callback const isPending = stateHook !== null ? stateHook.memoizedState : false; hookLog.push({ displayName: null, primitive: 'Transition', stackError: new Error(), value: isPending, debugInfo: null, dispatcherHookName: 'Transition', }); return [isPending, () => {}]; } function useDeferredValue(value: T, initialValue?: T): T { const hook = nextHook(); const prevValue = hook !== null ? hook.memoizedState : value; hookLog.push({ displayName: null, primitive: 'DeferredValue', stackError: new Error(), value: prevValue, debugInfo: null, dispatcherHookName: 'DeferredValue', }); return prevValue; } function useId(): string { const hook = nextHook(); const id = hook !== null ? hook.memoizedState : ''; hookLog.push({ displayName: null, primitive: 'Id', stackError: new Error(), value: id, debugInfo: null, dispatcherHookName: 'Id', }); return id; } // useMemoCache is an implementation detail of Forget's memoization // it should not be called directly in user-generated code function useMemoCache(size: number): Array { const fiber = currentFiber; // Don't throw, in case this is called from getPrimitiveStackCache if (fiber == null) { return []; } // $FlowFixMe[incompatible-use]: updateQueue is mixed const memoCache = fiber.updateQueue?.memoCache; if (memoCache == null) { return []; } let data = memoCache.data[memoCache.index]; if (data === undefined) { data = memoCache.data[memoCache.index] = new Array(size); for (let i = 0; i < size; i++) { data[i] = REACT_MEMO_CACHE_SENTINEL; } } // We don't write anything to hookLog on purpose, so this hook remains invisible to users. memoCache.index++; return data; } function useOptimistic( passthrough: S, reducer: ?(S, A) => S, ): [S, (A) => void] { const hook = nextHook(); let state; if (hook !== null) { state = hook.memoizedState; } else { state = passthrough; } hookLog.push({ displayName: null, primitive: 'Optimistic', stackError: new Error(), value: state, debugInfo: null, dispatcherHookName: 'Optimistic', }); return [state, (action: A) => {}]; } function useFormState( action: (Awaited, P) => S, initialState: Awaited, permalink?: string, ): [Awaited, (P) => void, boolean] { const hook = nextHook(); // FormState nextHook(); // PendingState nextHook(); // ActionQueue const stackError = new Error(); let value; let debugInfo = null; let error = null; if (hook !== null) { const actionResult = hook.memoizedState; if ( typeof actionResult === 'object' && actionResult !== null && // $FlowFixMe[method-unbinding] typeof actionResult.then === 'function' ) { const thenable: Thenable> = (actionResult: any); switch (thenable.status) { case 'fulfilled': { value = thenable.value; debugInfo = thenable._debugInfo === undefined ? null : thenable._debugInfo; break; } case 'rejected': { const rejectedError = thenable.reason; error = rejectedError; break; } default: // If this was an uncached Promise we have to abandon this attempt // but we can still emit anything up until this point. error = SuspenseException; debugInfo = thenable._debugInfo === undefined ? null : thenable._debugInfo; value = thenable; } } else { value = (actionResult: any); } } else { value = initialState; } hookLog.push({ displayName: null, primitive: 'FormState', stackError: stackError, value: value, debugInfo: debugInfo, dispatcherHookName: 'FormState', }); if (error !== null) { throw error; } // value being a Thenable is equivalent to error being not null // i.e. we only reach this point with Awaited const state = ((value: any): Awaited); // TODO: support displaying pending value return [state, (payload: P) => {}, false]; } function useActionState( action: (Awaited, P) => S, initialState: Awaited, permalink?: string, ): [Awaited, (P) => void, boolean] { const hook = nextHook(); // FormState nextHook(); // PendingState nextHook(); // ActionQueue const stackError = new Error(); let value; let debugInfo = null; let error = null; if (hook !== null) { const actionResult = hook.memoizedState; if ( typeof actionResult === 'object' && actionResult !== null && // $FlowFixMe[method-unbinding] typeof actionResult.then === 'function' ) { const thenable: Thenable> = (actionResult: any); switch (thenable.status) { case 'fulfilled': { value = thenable.value; debugInfo = thenable._debugInfo === undefined ? null : thenable._debugInfo; break; } case 'rejected': { const rejectedError = thenable.reason; error = rejectedError; break; } default: // If this was an uncached Promise we have to abandon this attempt // but we can still emit anything up until this point. error = SuspenseException; debugInfo = thenable._debugInfo === undefined ? null : thenable._debugInfo; value = thenable; } } else { value = (actionResult: any); } } else { value = initialState; } hookLog.push({ displayName: null, primitive: 'ActionState', stackError: stackError, value: value, debugInfo: debugInfo, dispatcherHookName: 'ActionState', }); if (error !== null) { throw error; } // value being a Thenable is equivalent to error being not null // i.e. we only reach this point with Awaited const state = ((value: any): Awaited); // TODO: support displaying pending value 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', }); return status; } const Dispatcher: DispatcherType = { use, readContext, useCacheRefresh, useCallback, useContext, unstable_useContextWithBailout, useEffect, useImperativeHandle, useDebugValue, useLayoutEffect, useInsertionEffect, useMemo, useMemoCache, useOptimistic, useReducer, useRef, useState, useTransition, useSyncExternalStore, useDeferredValue, useId, useFormState, useActionState, useHostTransitionStatus, }; // create a proxy to throw a custom error // in case future versions of React adds more hooks const DispatcherProxyHandler = { get(target: DispatcherType, prop: string) { if (target.hasOwnProperty(prop)) { // $FlowFixMe[invalid-computed-prop] return target[prop]; } const error = new Error('Missing method in Dispatcher: ' + prop); // Note: This error name needs to stay in sync with react-devtools-shared // TODO: refactor this if we ever combine the devtools and debug tools packages error.name = 'ReactDebugToolsUnsupportedHookError'; throw error; }, }; // `Proxy` may not exist on some platforms const DispatcherProxy = typeof Proxy === 'undefined' ? Dispatcher : new Proxy(Dispatcher, DispatcherProxyHandler); // Inspect export type HookSource = { lineNumber: number | null, columnNumber: number | null, fileName: string | null, functionName: string | null, }; export type HooksNode = { id: number | null, isStateEditable: boolean, name: string, value: mixed, subHooks: Array, debugInfo: null | ReactDebugInfo, hookSource: null | HookSource, }; export type HooksTree = Array; // Don't assume // // We can't assume that stack frames are nth steps away from anything. // E.g. we can't assume that the root call shares all frames with the stack // of a hook call. A simple way to demonstrate this is wrapping `new Error()` // in a wrapper constructor like a polyfill. That'll add an extra frame. // Similar things can happen with the call to the dispatcher. The top frame // may not be the primitive. // // We also can't assume that the last frame of the root call is the same // frame as the last frame of the hook call because long stack traces can be // truncated to a stack trace limit. let mostLikelyAncestorIndex = 0; function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) { const source = rootStack[rootIndex].source; hookSearch: for (let i = 0; i < hookStack.length; i++) { if (hookStack[i].source === source) { // This looks like a match. Validate that the rest of both stack match up. for ( let a = rootIndex + 1, b = i + 1; a < rootStack.length && b < hookStack.length; a++, b++ ) { if (hookStack[b].source !== rootStack[a].source) { // If not, give up and try a different match. continue hookSearch; } } return i; } } return -1; } function findCommonAncestorIndex(rootStack: any, hookStack: any) { let rootIndex = findSharedIndex( hookStack, rootStack, mostLikelyAncestorIndex, ); if (rootIndex !== -1) { return rootIndex; } // If the most likely one wasn't a hit, try any other frame to see if it is shared. // If that takes more than 5 frames, something probably went wrong. for (let i = 0; i < rootStack.length && i < 5; i++) { rootIndex = findSharedIndex(hookStack, rootStack, i); if (rootIndex !== -1) { mostLikelyAncestorIndex = i; return rootIndex; } } return -1; } function isReactWrapper(functionName: any, wrapperName: string) { const hookName = parseHookName(functionName); if (wrapperName === 'HostTransitionStatus') { return hookName === wrapperName || hookName === 'FormStatus'; } return hookName === wrapperName; } function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { const stackCache = getPrimitiveStackCache(); const primitiveStack = stackCache.get(hook.primitive); if (primitiveStack === undefined) { return -1; } for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) { // Note: there is no guarantee that we will find the top-most primitive frame in the stack // For React Native (uses Hermes), these source fields will be identical and skipped if (primitiveStack[i].source !== hookStack[i].source) { // If the next two frames are functions called `useX` then we assume that they're part of the // wrappers that the React package or other packages adds around the dispatcher. if ( i < hookStack.length - 1 && isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) ) { i++; } if ( i < hookStack.length - 1 && isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) ) { i++; } return i; } } return -1; } function parseTrimmedStack(rootStack: any, hook: HookLogEntry) { // Get the stack trace between the primitive hook function and // the root function call. I.e. the stack frames of custom hooks. const hookStack = ErrorStackParser.parse(hook.stackError); const rootIndex = findCommonAncestorIndex(rootStack, hookStack); const primitiveIndex = findPrimitiveIndex(hookStack, hook); if ( rootIndex === -1 || primitiveIndex === -1 || rootIndex - primitiveIndex < 2 ) { if (primitiveIndex === -1) { // Something went wrong. Give up. return [null, null]; } else { return [hookStack[primitiveIndex - 1], null]; } } return [ hookStack[primitiveIndex - 1], hookStack.slice(primitiveIndex, rootIndex - 1), ]; } function parseHookName(functionName: void | string): string { if (!functionName) { return ''; } let startIndex = functionName.lastIndexOf('[as '); if (startIndex !== -1) { // Workaround for sourcemaps in Jest and Chrome. // In `node --enable-source-maps`, we don't see "Object.useHostTransitionStatus [as useFormStatus]" but "Object.useFormStatus" // "Object.useHostTransitionStatus [as useFormStatus]" -> "useFormStatus" return parseHookName(functionName.slice(startIndex + '[as '.length, -1)); } startIndex = functionName.lastIndexOf('.'); if (startIndex === -1) { startIndex = 0; } else { startIndex += 1; } if (functionName.slice(startIndex).startsWith('unstable_')) { startIndex += 'unstable_'.length; } if (functionName.slice(startIndex, startIndex + 3) === 'use') { if (functionName.length - startIndex === 3) { return 'Use'; } startIndex += 3; } return functionName.slice(startIndex); } function buildTree( rootStack: any, readHookLog: Array, ): HooksTree { const rootChildren: Array = []; let prevStack = null; let levelChildren = rootChildren; let nativeHookID = 0; const stackOfChildren = []; for (let i = 0; i < readHookLog.length; i++) { const hook = readHookLog[i]; const parseResult = parseTrimmedStack(rootStack, hook); const primitiveFrame = parseResult[0]; const stack = parseResult[1]; let displayName = hook.displayName; if (displayName === null && primitiveFrame !== null) { displayName = parseHookName(primitiveFrame.functionName) || // Older versions of React do not have sourcemaps. // In those versions there was always a 1:1 mapping between wrapper and dispatcher method. parseHookName(hook.dispatcherHookName); } if (stack !== null) { // Note: The indices 0 <= n < length-1 will contain the names. // The indices 1 <= n < length will contain the source locations. // That's why we get the name from n - 1 and don't check the source // of index 0. let commonSteps = 0; if (prevStack !== null) { // Compare the current level's stack to the new stack. while (commonSteps < stack.length && commonSteps < prevStack.length) { const stackSource = stack[stack.length - commonSteps - 1].source; const prevSource = prevStack[prevStack.length - commonSteps - 1].source; if (stackSource !== prevSource) { break; } commonSteps++; } // Pop back the stack as many steps as were not common. for (let j = prevStack.length - 1; j > commonSteps; j--) { // $FlowFixMe[incompatible-type] levelChildren = stackOfChildren.pop(); } } // The remaining part of the new stack are custom hooks. Push them // to the tree. for (let j = stack.length - commonSteps - 1; j >= 1; j--) { const children: Array = []; const stackFrame = stack[j]; const levelChild: HooksNode = { id: null, isStateEditable: false, name: parseHookName(stack[j - 1].functionName), value: undefined, subHooks: children, debugInfo: null, hookSource: { lineNumber: stackFrame.lineNumber, columnNumber: stackFrame.columnNumber, functionName: stackFrame.functionName, fileName: stackFrame.fileName, }, }; levelChildren.push(levelChild); stackOfChildren.push(levelChildren); levelChildren = children; } prevStack = stack; } 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 === 'Promise' || primitive === 'Unresolved' || primitive === 'HostTransitionStatus' ? null : nativeHookID++; // For the time being, only State and Reducer hooks support runtime overrides. const isStateEditable = primitive === 'Reducer' || primitive === 'State'; const name = displayName || primitive; const levelChild: HooksNode = { id, isStateEditable, name, value: hook.value, subHooks: [], debugInfo: debugInfo, hookSource: null, }; const hookSource: HookSource = { lineNumber: null, functionName: null, fileName: null, columnNumber: null, }; if (stack && stack.length >= 1) { const stackFrame = stack[0]; hookSource.lineNumber = stackFrame.lineNumber; hookSource.functionName = stackFrame.functionName; hookSource.fileName = stackFrame.fileName; hookSource.columnNumber = stackFrame.columnNumber; } levelChild.hookSource = hookSource; levelChildren.push(levelChild); } // Associate custom hook values (useDebugValue() hook entries) with the correct hooks. processDebugValues(rootChildren, null); return rootChildren; } // Custom hooks support user-configurable labels (via the special useDebugValue() hook). // That hook adds user-provided values to the hooks tree, // but these values aren't intended to appear alongside of the other hooks. // Instead they should be attributed to their parent custom hook. // This method walks the tree and assigns debug values to their custom hook owners. function processDebugValues( hooksTree: HooksTree, parentHooksNode: HooksNode | null, ): void { const debugValueHooksNodes: Array = []; for (let i = 0; i < hooksTree.length; i++) { const hooksNode = hooksTree[i]; if (hooksNode.name === 'DebugValue' && hooksNode.subHooks.length === 0) { hooksTree.splice(i, 1); i--; debugValueHooksNodes.push(hooksNode); } else { processDebugValues(hooksNode.subHooks, hooksNode); } } // Bubble debug value labels to their custom hook owner. // If there is no parent hook, just ignore them for now. // (We may warn about this in the future.) if (parentHooksNode !== null) { if (debugValueHooksNodes.length === 1) { parentHooksNode.value = debugValueHooksNodes[0].value; } else if (debugValueHooksNodes.length > 1) { parentHooksNode.value = debugValueHooksNodes.map(({value}) => value); } } } 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' ) { throw error; } // If the error is not caused by an unsupported feature, it means // that the error is caused by user's code in renderFunction. // In this case, we should wrap the original error inside a custom error // so that devtools can give a clear message about it. // $FlowFixMe[extra-arg]: Flow doesn't know about 2nd argument of Error constructor const wrapperError = new Error('Error rendering inspected component', { cause: error, }); // Note: This error name needs to stay in sync with react-devtools-shared // TODO: refactor this if we ever combine the devtools and debug tools packages wrapperError.name = 'ReactDebugToolsRenderError'; // this stage-4 proposal is not supported by all environments yet. // $FlowFixMe[prop-missing] Flow doesn't have this type yet. wrapperError.cause = error; throw wrapperError; } export function inspectHooks( renderFunction: Props => React$Node, props: Props, currentDispatcher: ?CurrentDispatcherRef, ): HooksTree { // DevTools will pass the current renderer's injected dispatcher. // Other apps might compile debug hooks as part of their app though. if (currentDispatcher == null) { currentDispatcher = ReactSharedInternals; } const previousDispatcher = currentDispatcher.H; currentDispatcher.H = DispatcherProxy; let readHookLog; let ancestorStackError; try { ancestorStackError = new Error(); renderFunction(props); } catch (error) { handleRenderFunctionError(error); } finally { readHookLog = hookLog; hookLog = []; // $FlowFixMe[incompatible-use] found when upgrading Flow currentDispatcher.H = previousDispatcher; } const rootStack = ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); } function setupContexts(contextMap: Map, any>, fiber: Fiber) { let current: null | Fiber = fiber; while (current) { if (current.tag === ContextProvider) { let context: ReactContext = current.type; if ((context: any)._context !== undefined) { // Support inspection of pre-19+ providers. context = (context: any)._context; } if (!contextMap.has(context)) { // Store the current value that we're going to restore later. contextMap.set(context, context._currentValue); // Set the inner most provider value on the context. context._currentValue = current.memoizedProps.value; } } current = current.return; } } function restoreContexts(contextMap: Map, any>) { contextMap.forEach((value, context) => (context._currentValue = value)); } function inspectHooksOfForwardRef( renderFunction: (Props, Ref) => React$Node, props: Props, ref: Ref, currentDispatcher: CurrentDispatcherRef, ): HooksTree { const previousDispatcher = currentDispatcher.H; let readHookLog; currentDispatcher.H = DispatcherProxy; let ancestorStackError; try { ancestorStackError = new Error(); renderFunction(props, ref); } catch (error) { handleRenderFunctionError(error); } finally { readHookLog = hookLog; hookLog = []; currentDispatcher.H = previousDispatcher; } const rootStack = ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); } function resolveDefaultProps(Component: any, baseProps: any) { if (Component && Component.defaultProps) { // Resolve default props. Taken from ReactElement const props = assign({}, baseProps); const defaultProps = Component.defaultProps; for (const propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } return props; } return baseProps; } export function inspectHooksOfFiber( fiber: Fiber, currentDispatcher: ?CurrentDispatcherRef, ): HooksTree { // DevTools will pass the current renderer's injected dispatcher. // Other apps might compile debug hooks as part of their app though. if (currentDispatcher == null) { currentDispatcher = ReactSharedInternals; } if ( fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent && fiber.tag !== ForwardRef ) { throw new Error( 'Unknown Fiber. Needs to be a function component to inspect hooks.', ); } // Warm up the cache so that it doesn't consume the currentHook. getPrimitiveStackCache(); // Set up the current hook so that we can step through and read the // current state from them. 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, any>(); try { if ( currentContextDependency !== null && !hasOwnProperty.call(currentContextDependency, 'memoizedValue') ) { setupContexts(contextMap, fiber); } if (fiber.tag === ForwardRef) { return inspectHooksOfForwardRef( type.render, props, fiber.ref, currentDispatcher, ); } return inspectHooks(type, props, currentDispatcher); } finally { currentFiber = null; currentHook = null; currentContextDependency = null; restoreContexts(contextMap); } }