packages/react-debug-tools/src/ReactDebugHooks.js JAVASCRIPT 1,453 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */9import type {StackFrame as ParsedStackFrame} from 'error-stack-parser';10import type {11  Awaited,12  ReactContext,13  StartTransitionOptions,14  Usable,15  Thenable,16  ReactDebugInfo,17} from 'shared/ReactTypes';18import type {19  ContextDependency,20  Dependencies,21  Fiber,22  Dispatcher as DispatcherType,23} from 'react-reconciler/src/ReactInternalTypes';24import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig';2526import ErrorStackParser from 'error-stack-parser';27import assign from 'shared/assign';28import ReactSharedInternals from 'shared/ReactSharedInternals';29import {30  FunctionComponent,31  SimpleMemoComponent,32  ContextProvider,33  ForwardRef,34} from 'react-reconciler/src/ReactWorkTags';35import {36  REACT_MEMO_CACHE_SENTINEL,37  REACT_CONTEXT_TYPE,38  REACT_RECOVERABLE_TYPE,39} from 'shared/ReactSymbols';40import hasOwnProperty from 'shared/hasOwnProperty';4142type CurrentDispatcherRef = typeof ReactSharedInternals;4344// Used to track hooks called during a render4546type HookLogEntry = {47  displayName: string | null,48  primitive: string,49  stackError: Error,50  value: mixed,51  debugInfo: ReactDebugInfo | null,52  dispatcherHookName: string,53};5455let hookLog: Array<HookLogEntry> = [];5657// Primitives5859type BasicStateAction<S> = (S => S) | S;6061type Dispatch<A> = A => void;6263let primitiveStackCache: null | Map<string, Array<any>> = null;6465type Hook = {66  memoizedState: any,67  next: Hook | null,68};6970function getPrimitiveStackCache(): Map<string, Array<any>> {71  // This initializes a cache of all primitive hooks so that the top72  // most stack frames added by calling the primitive hook can be removed.73  if (primitiveStackCache === null) {74    const cache = new Map<string, Array<any>>();75    let readHookLog;76    try {77      // Use all hooks here to add them to the hook log.78      Dispatcher.useContext({_currentValue: null} as any);79      Dispatcher.useState(null);80      Dispatcher.useReducer((s: mixed, a: mixed) => s, null);81      Dispatcher.useRef(null);82      if (typeof Dispatcher.useCacheRefresh === 'function') {83        // This type check is for Flow only.84        Dispatcher.useCacheRefresh();85      }86      Dispatcher.useLayoutEffect(() => {});87      Dispatcher.useInsertionEffect(() => {});88      Dispatcher.useEffect(() => {});89      Dispatcher.useImperativeHandle(undefined, () => null);90      Dispatcher.useDebugValue(null);91      Dispatcher.useCallback(() => {});92      Dispatcher.useTransition();93      Dispatcher.useSyncExternalStore(94        () => () => {},95        () => null,96        () => null,97      );98      Dispatcher.useDeferredValue(null);99      Dispatcher.useMemo(() => null);100      Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s);101      Dispatcher.useFormState((s: mixed, p: mixed) => s, null);102      Dispatcher.useActionState((s: mixed, p: mixed) => s, null);103      Dispatcher.useHostTransitionStatus();104      if (typeof Dispatcher.useMemoCache === 'function') {105        // This type check is for Flow only.106        Dispatcher.useMemoCache(0);107      }108      if (typeof Dispatcher.use === 'function') {109        // This type check is for Flow only.110        Dispatcher.use({111          $$typeof: REACT_CONTEXT_TYPE,112          _currentValue: null,113        } as any);114        const recoverable = {115          $$typeof: REACT_RECOVERABLE_TYPE,116          _reason: undefined,117        };118        Dispatcher.use(recoverable as any);119        Dispatcher.use({120          then() {},121          status: 'fulfilled',122          value: null,123        });124        try {125          Dispatcher.use({126            then() {},127          } as any);128        } catch (x) {}129      }130131      Dispatcher.useId();132133      if (typeof Dispatcher.useEffectEvent === 'function') {134        Dispatcher.useEffectEvent((args: empty) => {});135      }136    } finally {137      readHookLog = hookLog;138      hookLog = [];139    }140    for (let i = 0; i < readHookLog.length; i++) {141      const hook = readHookLog[i];142      cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError));143    }144    primitiveStackCache = cache;145  }146  return primitiveStackCache;147}148149let currentFiber: null | Fiber = null;150let currentHook: null | Hook = null;151let currentContextDependency: null | ContextDependency<mixed> = null;152let currentThenableIndex: number = 0;153let currentThenableState: null | Array<Thenable<mixed>> = null;154155function nextHook(): null | Hook {156  const hook = currentHook;157  if (hook !== null) {158    currentHook = hook.next;159  }160  return hook;161}162163function readContext<T>(context: ReactContext<T>): T {164  if (currentFiber === null) {165    // Hook inspection without access to the Fiber tree166    // e.g. when warming up the primitive stack cache or during `ReactDebugTools.inspectHooks()`.167    return context._currentValue;168  } else {169    if (currentContextDependency === null) {170      throw new Error(171        'Context reads do not line up with context dependencies. This is a bug in React Debug Tools.',172      );173    }174175    let value: T;176    // For now we don't expose readContext usage in the hooks debugging info.177    if (hasOwnProperty.call(currentContextDependency, 'memoizedValue')) {178      // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`179      value = currentContextDependency.memoizedValue as any as T;180181      // $FlowFixMe[incompatible-use] Flow thinks `hasOwnProperty` mutates `currentContextDependency`182      currentContextDependency = currentContextDependency.next;183    } else {184      // Before React 18, we did not have `memoizedValue` so we rely on `setupContexts` in those versions.185      // Multiple reads of the same context were also only tracked as a single dependency.186      // We just give up on advancing context dependencies and solely rely on `setupContexts`.187      value = context._currentValue;188    }189190    return value;191  }192}193194const SuspenseException: mixed = new Error(195  "Suspense Exception: This is not a real error! It's an implementation " +196    'detail of `use` to interrupt the current render. You must either ' +197    'rethrow it immediately, or move the `use` call outside of the ' +198    '`try/catch` block. Capturing without rethrowing will lead to ' +199    'unexpected behavior.\n\n' +200    'To handle async errors, wrap your component in an error boundary, or ' +201    "call the promise's `.catch` method and pass the result to `use`.",202);203204function use<T>(usable: Usable<T>): T {205  // $FlowFixMe[invalid-compare]206  if (usable !== null && typeof usable === 'object') {207    // $FlowFixMe[method-unbinding]208    if (typeof usable.then === 'function') {209      const thenable: Thenable<any> =210        // If we have thenable state, then the actually used thenable will be the one211        // stashed in it. It's possible for uncached Promises to be new each render212        // and in that case the one we're inspecting is the in the thenable state.213        currentThenableState !== null &&214        currentThenableIndex < currentThenableState.length215          ? currentThenableState[currentThenableIndex++]216          : (usable as any);217218      switch (thenable.status) {219        case 'fulfilled': {220          const fulfilledValue: T = thenable.value;221          hookLog.push({222            displayName: null,223            primitive: 'Promise',224            stackError: new Error(),225            value: fulfilledValue,226            debugInfo:227              thenable._debugInfo === undefined ? null : thenable._debugInfo,228            dispatcherHookName: 'Use',229          });230          return fulfilledValue;231        }232        case 'rejected': {233          const rejectedError = thenable.reason;234          throw rejectedError;235        }236      }237      // If this was an uncached Promise we have to abandon this attempt238      // but we can still emit anything up until this point.239      hookLog.push({240        displayName: null,241        primitive: 'Unresolved',242        stackError: new Error(),243        value: thenable,244        debugInfo:245          thenable._debugInfo === undefined ? null : thenable._debugInfo,246        dispatcherHookName: 'Use',247      });248      throw SuspenseException;249    } else if (usable.$$typeof === REACT_RECOVERABLE_TYPE) {250      hookLog.push({251        displayName: null,252        primitive: 'Recoverable',253        stackError: new Error(),254        value: undefined,255        debugInfo: null,256        dispatcherHookName: 'Use',257      });258      return undefined as any;259    } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {260      const context: ReactContext<T> = usable as any;261      const value = readContext(context);262263      hookLog.push({264        displayName: context.displayName || 'Context',265        primitive: 'Context (use)',266        stackError: new Error(),267        value,268        debugInfo: null,269        dispatcherHookName: 'Use',270      });271272      return value;273    }274  }275276  // eslint-disable-next-line react-internal/safe-string-coercion277  throw new Error('An unsupported type was passed to use(): ' + String(usable));278}279280function useContext<T>(context: ReactContext<T>): T {281  const value = readContext(context);282  hookLog.push({283    displayName: context.displayName || null,284    primitive: 'Context',285    stackError: new Error(),286    value: value,287    debugInfo: null,288    dispatcherHookName: 'Context',289  });290  return value;291}292293function useState<S>(294  initialState: (() => S) | S,295): [S, Dispatch<BasicStateAction<S>>] {296  const hook = nextHook();297  const state: S =298    hook !== null299      ? hook.memoizedState300      : typeof initialState === 'function'301        ? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types302          initialState()303        : initialState;304  hookLog.push({305    displayName: null,306    primitive: 'State',307    stackError: new Error(),308    value: state,309    debugInfo: null,310    dispatcherHookName: 'State',311  });312  return [state, (action: BasicStateAction<S>) => {}];313}314315function useReducer<S, I, A>(316  reducer: (S, A) => S,317  initialArg: I,318  init?: I => S,319): [S, Dispatch<A>] {320  const hook = nextHook();321  let state;322  if (hook !== null) {323    state = hook.memoizedState;324  } else {325    state = init !== undefined ? init(initialArg) : (initialArg as any as S);326  }327  hookLog.push({328    displayName: null,329    primitive: 'Reducer',330    stackError: new Error(),331    value: state,332    debugInfo: null,333    dispatcherHookName: 'Reducer',334  });335  return [state, (action: A) => {}];336}337338function useRef<T>(initialValue: T): {current: T} {339  const hook = nextHook();340  const ref = hook !== null ? hook.memoizedState : {current: initialValue};341  hookLog.push({342    displayName: null,343    primitive: 'Ref',344    stackError: new Error(),345    value: ref.current,346    debugInfo: null,347    dispatcherHookName: 'Ref',348  });349  return ref;350}351352function useCacheRefresh(): () => void {353  const hook = nextHook();354  hookLog.push({355    displayName: null,356    primitive: 'CacheRefresh',357    stackError: new Error(),358    value: hook !== null ? hook.memoizedState : function refresh() {},359    debugInfo: null,360    dispatcherHookName: 'CacheRefresh',361  });362  return () => {};363}364365function useLayoutEffect(366  create: () => (() => void) | void,367  inputs: Array<mixed> | void | null,368): void {369  nextHook();370  hookLog.push({371    displayName: null,372    primitive: 'LayoutEffect',373    stackError: new Error(),374    value: create,375    debugInfo: null,376    dispatcherHookName: 'LayoutEffect',377  });378}379380function useInsertionEffect(381  create: () => mixed,382  inputs: Array<mixed> | void | null,383): void {384  nextHook();385  hookLog.push({386    displayName: null,387    primitive: 'InsertionEffect',388    stackError: new Error(),389    value: create,390    debugInfo: null,391    dispatcherHookName: 'InsertionEffect',392  });393}394395function useEffect(396  create: () => (() => void) | void,397  deps: Array<mixed> | void | null,398): void {399  nextHook();400  hookLog.push({401    displayName: null,402    primitive: 'Effect',403    stackError: new Error(),404    value: create,405    debugInfo: null,406    dispatcherHookName: 'Effect',407  });408}409410function useImperativeHandle<T>(411  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,412  create: () => T,413  inputs: Array<mixed> | void | null,414): void {415  nextHook();416  // We don't actually store the instance anywhere if there is no ref callback417  // and if there is a ref callback it might not store it but if it does we418  // have no way of knowing where. So let's only enable introspection of the419  // ref itself if it is using the object form.420  let instance: ?T = undefined;421  if (ref !== null && typeof ref === 'object') {422    instance = ref.current;423  }424  hookLog.push({425    displayName: null,426    primitive: 'ImperativeHandle',427    stackError: new Error(),428    value: instance,429    debugInfo: null,430    dispatcherHookName: 'ImperativeHandle',431  });432}433434function useDebugValue(value: any, formatterFn: ?(value: any) => any) {435  hookLog.push({436    displayName: null,437    primitive: 'DebugValue',438    stackError: new Error(),439    value: typeof formatterFn === 'function' ? formatterFn(value) : value,440    debugInfo: null,441    dispatcherHookName: 'DebugValue',442  });443}444445function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {446  const hook = nextHook();447  hookLog.push({448    displayName: null,449    primitive: 'Callback',450    stackError: new Error(),451    value: hook !== null ? hook.memoizedState[0] : callback,452    debugInfo: null,453    dispatcherHookName: 'Callback',454  });455  return callback;456}457458function useMemo<T>(459  nextCreate: () => T,460  inputs: Array<mixed> | void | null,461): T {462  const hook = nextHook();463  const value = hook !== null ? hook.memoizedState[0] : nextCreate();464  hookLog.push({465    displayName: null,466    primitive: 'Memo',467    stackError: new Error(),468    value,469    debugInfo: null,470    dispatcherHookName: 'Memo',471  });472  return value;473}474475function useSyncExternalStore<T>(476  subscribe: (() => void) => () => void,477  getSnapshot: () => T,478  getServerSnapshot?: () => T,479): T {480  // useSyncExternalStore() composes multiple hooks internally.481  // Advance the current hook index the same number of times482  // so that subsequent hooks have the right memoized state.483  const hook = nextHook(); // SyncExternalStore484  nextHook(); // Effect485  // Read from hook.memoizedState to get the value that was used during render,486  // not the current value from getSnapshot() which may have changed.487  const value = hook !== null ? hook.memoizedState : getSnapshot();488  hookLog.push({489    displayName: null,490    primitive: 'SyncExternalStore',491    stackError: new Error(),492    value,493    debugInfo: null,494    dispatcherHookName: 'SyncExternalStore',495  });496  return value;497}498499function useTransition(): [500  boolean,501  (callback: () => void, options?: StartTransitionOptions) => void,502] {503  // useTransition() composes multiple hooks internally.504  // Advance the current hook index the same number of times505  // so that subsequent hooks have the right memoized state.506  const stateHook = nextHook();507  nextHook(); // Callback508509  const isPending = stateHook !== null ? stateHook.memoizedState : false;510511  hookLog.push({512    displayName: null,513    primitive: 'Transition',514    stackError: new Error(),515    value: isPending,516    debugInfo: null,517    dispatcherHookName: 'Transition',518  });519  return [isPending, () => {}];520}521522function useDeferredValue<T>(value: T, initialValue?: T): T {523  const hook = nextHook();524  const prevValue = hook !== null ? hook.memoizedState : value;525  hookLog.push({526    displayName: null,527    primitive: 'DeferredValue',528    stackError: new Error(),529    value: prevValue,530    debugInfo: null,531    dispatcherHookName: 'DeferredValue',532  });533  return prevValue;534}535536function useId(): string {537  const hook = nextHook();538  const id = hook !== null ? hook.memoizedState : '';539  hookLog.push({540    displayName: null,541    primitive: 'Id',542    stackError: new Error(),543    value: id,544    debugInfo: null,545    dispatcherHookName: 'Id',546  });547  return id;548}549550// useMemoCache is an implementation detail of Forget's memoization551// it should not be called directly in user-generated code552function useMemoCache(size: number): Array<mixed> {553  const fiber = currentFiber;554  // Don't throw, in case this is called from getPrimitiveStackCache555  if (fiber == null) {556    return [];557  }558559  const memoCache =560    // $FlowFixMe[incompatible-use]: updateQueue is mixed561    fiber.updateQueue != null ? fiber.updateQueue.memoCache : null;562  if (memoCache == null) {563    return [];564  }565566  let data = memoCache.data[memoCache.index];567  if (data === undefined) {568    data = memoCache.data[memoCache.index] = new Array(size);569    for (let i = 0; i < size; i++) {570      data[i] = REACT_MEMO_CACHE_SENTINEL;571    }572  }573574  // We don't write anything to hookLog on purpose, so this hook remains invisible to users.575576  memoCache.index++;577  return data;578}579580function useOptimistic<S, A>(581  passthrough: S,582  reducer: ?(S, A) => S,583): [S, (A) => void] {584  const hook = nextHook();585  let state;586  if (hook !== null) {587    state = hook.memoizedState;588  } else {589    state = passthrough;590  }591  hookLog.push({592    displayName: null,593    primitive: 'Optimistic',594    stackError: new Error(),595    value: state,596    debugInfo: null,597    dispatcherHookName: 'Optimistic',598  });599  return [state, (action: A) => {}];600}601602function useFormState<S, P>(603  action: (Awaited<S>, P) => S,604  initialState: Awaited<S>,605  permalink?: string,606): [Awaited<S>, (P) => void, boolean] {607  const hook = nextHook(); // FormState608  nextHook(); // PendingState609  nextHook(); // ActionQueue610  const stackError = new Error();611  let value;612  let debugInfo = null;613  let error = null;614615  if (hook !== null) {616    const actionResult = hook.memoizedState;617    if (618      typeof actionResult === 'object' &&619      actionResult !== null &&620      // $FlowFixMe[method-unbinding]621      typeof actionResult.then === 'function'622    ) {623      const thenable: Thenable<Awaited<S>> = actionResult as any;624      switch (thenable.status) {625        case 'fulfilled': {626          value = thenable.value;627          debugInfo =628            thenable._debugInfo === undefined ? null : thenable._debugInfo;629          break;630        }631        case 'rejected': {632          const rejectedError = thenable.reason;633          error = rejectedError;634          break;635        }636        default:637          // If this was an uncached Promise we have to abandon this attempt638          // but we can still emit anything up until this point.639          error = SuspenseException;640          debugInfo =641            thenable._debugInfo === undefined ? null : thenable._debugInfo;642          value = thenable;643      }644    } else {645      value = actionResult as any;646    }647  } else {648    value = initialState;649  }650651  hookLog.push({652    displayName: null,653    primitive: 'FormState',654    stackError: stackError,655    value: value,656    debugInfo: debugInfo,657    dispatcherHookName: 'FormState',658  });659660  if (error !== null) {661    throw error;662  }663664  // value being a Thenable is equivalent to error being not null665  // i.e. we only reach this point with Awaited<S>666  const state = value as any as Awaited<S>;667668  // TODO: support displaying pending value669  return [state, (payload: P) => {}, false];670}671672function useActionState<S, P>(673  action: (Awaited<S>, P) => S,674  initialState: Awaited<S>,675  permalink?: string,676): [Awaited<S>, (P) => void, boolean] {677  const hook = nextHook(); // FormState678  nextHook(); // PendingState679  nextHook(); // ActionQueue680  const stackError = new Error();681  let value;682  let debugInfo = null;683  let error = null;684685  if (hook !== null) {686    const actionResult = hook.memoizedState;687    if (688      typeof actionResult === 'object' &&689      actionResult !== null &&690      // $FlowFixMe[method-unbinding]691      typeof actionResult.then === 'function'692    ) {693      const thenable: Thenable<Awaited<S>> = actionResult as any;694      switch (thenable.status) {695        case 'fulfilled': {696          value = thenable.value;697          debugInfo =698            thenable._debugInfo === undefined ? null : thenable._debugInfo;699          break;700        }701        case 'rejected': {702          const rejectedError = thenable.reason;703          error = rejectedError;704          break;705        }706        default:707          // If this was an uncached Promise we have to abandon this attempt708          // but we can still emit anything up until this point.709          error = SuspenseException;710          debugInfo =711            thenable._debugInfo === undefined ? null : thenable._debugInfo;712          value = thenable;713      }714    } else {715      value = actionResult as any;716    }717  } else {718    value = initialState;719  }720721  hookLog.push({722    displayName: null,723    primitive: 'ActionState',724    stackError: stackError,725    value: value,726    debugInfo: debugInfo,727    dispatcherHookName: 'ActionState',728  });729730  if (error !== null) {731    throw error;732  }733734  // value being a Thenable is equivalent to error being not null735  // i.e. we only reach this point with Awaited<S>736  const state = value as any as Awaited<S>;737738  // TODO: support displaying pending value739  return [state, (payload: P) => {}, false];740}741742function useHostTransitionStatus(): TransitionStatus {743  const status = readContext<TransitionStatus>(744    // $FlowFixMe[prop-missing] `readContext` only needs _currentValue745    // $FlowFixMe[incompatible-type]746    {747      // $FlowFixMe[incompatible-type] TODO: Incorrect bottom value without access to Fiber config.748      _currentValue: null,749    } as ReactContext<TransitionStatus>,750  );751752  hookLog.push({753    displayName: null,754    primitive: 'HostTransitionStatus',755    stackError: new Error(),756    value: status,757    debugInfo: null,758    dispatcherHookName: 'HostTransitionStatus',759  });760761  return status;762}763764function useEffectEvent<Args, F: (...Array<Args>) => mixed>(callback: F): F {765  nextHook();766  hookLog.push({767    displayName: null,768    primitive: 'EffectEvent',769    stackError: new Error(),770    value: callback,771    debugInfo: null,772    dispatcherHookName: 'EffectEvent',773  });774775  return callback;776}777778const Dispatcher: DispatcherType = {779  readContext,780781  use,782  useCallback,783  useContext,784  useEffect,785  useImperativeHandle,786  useLayoutEffect,787  useInsertionEffect,788  useMemo,789  useReducer,790  useRef,791  useState,792  useDebugValue,793  useDeferredValue,794  useTransition,795  useSyncExternalStore,796  useId,797  useHostTransitionStatus,798  useFormState,799  useActionState,800  useOptimistic,801  useMemoCache,802  useCacheRefresh,803  useEffectEvent,804};805806// create a proxy to throw a custom error807// in case future versions of React adds more hooks808const DispatcherProxyHandler: Proxy$traps<DispatcherType> = {809  get(target: DispatcherType, prop: string) {810    if (target.hasOwnProperty(prop)) {811      // $FlowFixMe[invalid-computed-prop]812      return target[prop];813    }814    const error = new Error('Missing method in Dispatcher: ' + prop);815    // Note: This error name needs to stay in sync with react-devtools-shared816    // TODO: refactor this if we ever combine the devtools and debug tools packages817    error.name = 'ReactDebugToolsUnsupportedHookError';818    throw error;819  },820};821822// `Proxy` may not exist on some platforms823const DispatcherProxy =824  typeof Proxy === 'undefined'825    ? Dispatcher826    : new Proxy(Dispatcher, DispatcherProxyHandler);827828// Inspect829830export type HookSource = {831  lineNumber: number | null,832  columnNumber: number | null,833  fileName: string | null,834  functionName: string | null,835};836837export type HooksNode = {838  id: number | null,839  isStateEditable: boolean,840  name: string,841  value: mixed,842  subHooks: Array<HooksNode>,843  debugInfo: null | ReactDebugInfo,844  hookSource: null | HookSource,845};846export type HooksTree = Array<HooksNode>;847848// Don't assume849//850// We can't assume that stack frames are nth steps away from anything.851// E.g. we can't assume that the root call shares all frames with the stack852// of a hook call. A simple way to demonstrate this is wrapping `new Error()`853// in a wrapper constructor like a polyfill. That'll add an extra frame.854// Similar things can happen with the call to the dispatcher. The top frame855// may not be the primitive.856//857// We also can't assume that the last frame of the root call is the same858// frame as the last frame of the hook call because long stack traces can be859// truncated to a stack trace limit.860861let mostLikelyAncestorIndex = 0;862863function findSharedIndex(864  hookStack: ParsedStackFrame[],865  rootStack: ParsedStackFrame[],866  rootIndex: number,867) {868  const source = rootStack[rootIndex].source;869  hookSearch: for (let i = 0; i < hookStack.length; i++) {870    if (hookStack[i].source === source) {871      // This looks like a match. Validate that the rest of both stack match up.872      for (873        let a = rootIndex + 1, b = i + 1;874        a < rootStack.length && b < hookStack.length;875        a++, b++876      ) {877        if (hookStack[b].source !== rootStack[a].source) {878          // If not, give up and try a different match.879          continue hookSearch;880        }881      }882      return i;883    }884  }885  return -1;886}887888function findCommonAncestorIndex(889  rootStack: ParsedStackFrame[],890  hookStack: ParsedStackFrame[],891) {892  let rootIndex = findSharedIndex(893    hookStack,894    rootStack,895    mostLikelyAncestorIndex,896  );897  if (rootIndex !== -1) {898    return rootIndex;899  }900  // If the most likely one wasn't a hit, try any other frame to see if it is shared.901  // If that takes more than 5 frames, something probably went wrong.902  for (let i = 0; i < rootStack.length && i < 5; i++) {903    rootIndex = findSharedIndex(hookStack, rootStack, i);904    if (rootIndex !== -1) {905      mostLikelyAncestorIndex = i;906      return rootIndex;907    }908  }909  return -1;910}911912function isReactWrapper(functionName: void | string, wrapperName: string) {913  const hookName = parseHookName(functionName);914  if (wrapperName === 'HostTransitionStatus') {915    return hookName === wrapperName || hookName === 'FormStatus';916  }917918  return hookName === wrapperName;919}920921function findPrimitiveIndex(hookStack: ParsedStackFrame[], hook: HookLogEntry) {922  const stackCache = getPrimitiveStackCache();923  const primitiveStack = stackCache.get(hook.primitive);924  if (primitiveStack === undefined) {925    return -1;926  }927  for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {928    // Note: there is no guarantee that we will find the top-most primitive frame in the stack929    // For React Native (uses Hermes), these source fields will be identical and skipped930    if (primitiveStack[i].source !== hookStack[i].source) {931      // If the next two frames are functions called `useX` then we assume that they're part of the932      // wrappers that the React package or other packages adds around the dispatcher.933      if (934        i < hookStack.length - 1 &&935        isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)936      ) {937        i++;938      }939      if (940        i < hookStack.length - 1 &&941        isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)942      ) {943        i++;944      }945946      return i;947    }948  }949  return -1;950}951952function parseTrimmedStack(rootStack: ParsedStackFrame[], hook: HookLogEntry) {953  // Get the stack trace between the primitive hook function and954  // the root function call. I.e. the stack frames of custom hooks.955  const hookStack = ErrorStackParser.parse(hook.stackError);956  const rootIndex = findCommonAncestorIndex(rootStack, hookStack);957  const primitiveIndex = findPrimitiveIndex(hookStack, hook);958  if (959    rootIndex === -1 ||960    primitiveIndex === -1 ||961    rootIndex - primitiveIndex < 2962  ) {963    if (primitiveIndex === -1) {964      // Something went wrong. Give up.965      return [null, null];966    } else {967      return [hookStack[primitiveIndex - 1], null];968    }969  }970  return [971    hookStack[primitiveIndex - 1],972    hookStack.slice(primitiveIndex, rootIndex - 1),973  ];974}975976function parseHookName(functionName: void | string): string {977  if (!functionName) {978    return '';979  }980  let startIndex = functionName.lastIndexOf('[as ');981982  if (startIndex !== -1) {983    // Workaround for sourcemaps in Jest and Chrome.984    // In `node --enable-source-maps`, we don't see "Object.useHostTransitionStatus [as useFormStatus]" but "Object.useFormStatus"985    // "Object.useHostTransitionStatus [as useFormStatus]" -> "useFormStatus"986    return parseHookName(functionName.slice(startIndex + '[as '.length, -1));987  }988  startIndex = functionName.lastIndexOf('.');989  if (startIndex === -1) {990    startIndex = 0;991  } else {992    startIndex += 1;993  }994995  if (functionName.slice(startIndex).startsWith('unstable_')) {996    startIndex += 'unstable_'.length;997  }998999  if (functionName.slice(startIndex).startsWith('experimental_')) {1000    startIndex += 'experimental_'.length;1001  }10021003  if (functionName.slice(startIndex, startIndex + 3) === 'use') {1004    if (functionName.length - startIndex === 3) {1005      return 'Use';1006    }1007    startIndex += 3;1008  }1009  return functionName.slice(startIndex);1010}10111012function buildTree(1013  rootStack: ParsedStackFrame[],1014  readHookLog: Array<HookLogEntry>,1015): HooksTree {1016  const rootChildren: Array<HooksNode> = [];1017  let prevStack = null;1018  let levelChildren = rootChildren;1019  let nativeHookID = 0;1020  const stackOfChildren = [];1021  for (let i = 0; i < readHookLog.length; i++) {1022    const hook = readHookLog[i];1023    const parseResult = parseTrimmedStack(rootStack, hook);1024    const primitiveFrame = parseResult[0];1025    const stack = parseResult[1];1026    let displayName = hook.displayName;1027    if (displayName === null && primitiveFrame !== null) {1028      displayName =1029        parseHookName(primitiveFrame.functionName) ||1030        // Older versions of React do not have sourcemaps.1031        // In those versions there was always a 1:1 mapping between wrapper and dispatcher method.1032        parseHookName(hook.dispatcherHookName);1033    }1034    if (stack !== null) {1035      // Note: The indices 0 <= n < length-1 will contain the names.1036      // The indices 1 <= n < length will contain the source locations.1037      // That's why we get the name from n - 1 and don't check the source1038      // of index 0.1039      let commonSteps = 0;1040      if (prevStack !== null) {1041        // Compare the current level's stack to the new stack.1042        while (commonSteps < stack.length && commonSteps < prevStack.length) {1043          const stackSource = stack[stack.length - commonSteps - 1].source;1044          const prevSource =1045            prevStack[prevStack.length - commonSteps - 1].source;1046          if (stackSource !== prevSource) {1047            break;1048          }1049          commonSteps++;1050        }1051        // Pop back the stack as many steps as were not common.1052        for (let j = prevStack.length - 1; j > commonSteps; j--) {1053          // $FlowFixMe[incompatible-type]1054          levelChildren = stackOfChildren.pop();1055        }1056      }1057      // The remaining part of the new stack are custom hooks. Push them1058      // to the tree.1059      for (let j = stack.length - commonSteps - 1; j >= 1; j--) {1060        const children: Array<HooksNode> = [];1061        const stackFrame = stack[j];1062        const levelChild: HooksNode = {1063          id: null,1064          isStateEditable: false,1065          name: parseHookName(stack[j - 1].functionName),1066          value: undefined,1067          subHooks: children,1068          debugInfo: null,1069          hookSource: {1070            lineNumber:1071              stackFrame.lineNumber === undefined1072                ? null1073                : stackFrame.lineNumber,1074            columnNumber:1075              stackFrame.columnNumber === undefined1076                ? null1077                : stackFrame.columnNumber,1078            functionName:1079              stackFrame.functionName === undefined1080                ? null1081                : stackFrame.functionName,1082            fileName:1083              stackFrame.fileName === undefined ? null : stackFrame.fileName,1084          },1085        };10861087        levelChildren.push(levelChild);1088        stackOfChildren.push(levelChildren);1089        levelChildren = children;1090      }1091      prevStack = stack;1092    }1093    const {primitive, debugInfo} = hook;10941095    // For now, the "id" of stateful hooks is just the stateful hook index.1096    // Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).1097    const id =1098      primitive === 'Context' ||1099      primitive === 'Context (use)' ||1100      primitive === 'DebugValue' ||1101      primitive === 'Promise' ||1102      primitive === 'Unresolved' ||1103      primitive === 'HostTransitionStatus'1104        ? null1105        : nativeHookID++;11061107    // For the time being, only State and Reducer hooks support runtime overrides.1108    const isStateEditable = primitive === 'Reducer' || primitive === 'State';1109    const name = displayName || primitive;1110    const levelChild: HooksNode = {1111      id,1112      isStateEditable,1113      name,1114      value: hook.value,1115      subHooks: [],1116      debugInfo: debugInfo,1117      hookSource: null,1118    };11191120    const hookSource: HookSource = {1121      lineNumber: null,1122      functionName: null,1123      fileName: null,1124      columnNumber: null,1125    };1126    if (stack && stack.length >= 1) {1127      const stackFrame = stack[0];1128      hookSource.lineNumber =1129        stackFrame.lineNumber === undefined ? null : stackFrame.lineNumber;1130      hookSource.functionName =1131        stackFrame.functionName === undefined ? null : stackFrame.functionName;1132      hookSource.fileName =1133        stackFrame.fileName === undefined ? null : stackFrame.fileName;1134      hookSource.columnNumber =1135        stackFrame.columnNumber === undefined ? null : stackFrame.columnNumber;1136    }11371138    levelChild.hookSource = hookSource;11391140    levelChildren.push(levelChild);1141  }11421143  // Associate custom hook values (useDebugValue() hook entries) with the correct hooks.1144  processDebugValues(rootChildren, null);11451146  return rootChildren;1147}11481149// Custom hooks support user-configurable labels (via the special useDebugValue() hook).1150// That hook adds user-provided values to the hooks tree,1151// but these values aren't intended to appear alongside of the other hooks.1152// Instead they should be attributed to their parent custom hook.1153// This method walks the tree and assigns debug values to their custom hook owners.1154function processDebugValues(1155  hooksTree: HooksTree,1156  parentHooksNode: HooksNode | null,1157): void {1158  const debugValueHooksNodes: Array<HooksNode> = [];11591160  for (let i = 0; i < hooksTree.length; i++) {1161    const hooksNode = hooksTree[i];1162    if (hooksNode.name === 'DebugValue' && hooksNode.subHooks.length === 0) {1163      hooksTree.splice(i, 1);1164      i--;1165      debugValueHooksNodes.push(hooksNode);1166    } else {1167      processDebugValues(hooksNode.subHooks, hooksNode);1168    }1169  }11701171  // Bubble debug value labels to their custom hook owner.1172  // If there is no parent hook, just ignore them for now.1173  // (We may warn about this in the future.)1174  if (parentHooksNode !== null) {1175    if (debugValueHooksNodes.length === 1) {1176      parentHooksNode.value = debugValueHooksNodes[0].value;1177    } else if (debugValueHooksNodes.length > 1) {1178      parentHooksNode.value = debugValueHooksNodes.map(({value}) => value);1179    }1180  }1181}11821183function handleRenderFunctionError(error: any): void {1184  // original error might be any type.1185  if (error === SuspenseException) {1186    // An uncached Promise was used. We can't synchronously resolve the rest of1187    // the Hooks but we can at least show what ever we got so far.1188    return;1189  }1190  if (1191    error instanceof Error &&1192    error.name === 'ReactDebugToolsUnsupportedHookError'1193  ) {1194    throw error;1195  }1196  // If the error is not caused by an unsupported feature, it means1197  // that the error is caused by user's code in renderFunction.1198  // In this case, we should wrap the original error inside a custom error1199  // so that devtools can give a clear message about it.1200  // $FlowFixMe[extra-arg]: Flow doesn't know about 2nd argument of Error constructor1201  const wrapperError = new Error('Error rendering inspected component', {1202    cause: error,1203  });1204  // Note: This error name needs to stay in sync with react-devtools-shared1205  // TODO: refactor this if we ever combine the devtools and debug tools packages1206  wrapperError.name = 'ReactDebugToolsRenderError';1207  // this stage-4 proposal is not supported by all environments yet.1208  // $FlowFixMe[prop-missing] Flow doesn't have this type yet.1209  wrapperError.cause = error;1210  throw wrapperError;1211}12121213// Shared implementation. Requires an explicit dispatcher and never references1214// ReactSharedInternals, so importing it does not pull React into the bundle.1215function inspectHooksImpl<Props>(1216  renderFunction: Props => React$Node,1217  props: Props,1218  currentDispatcher: CurrentDispatcherRef,1219): HooksTree {1220  const previousDispatcher = currentDispatcher.H;1221  currentDispatcher.H = DispatcherProxy;12221223  let readHookLog;1224  let ancestorStackError;12251226  try {1227    ancestorStackError = new Error();1228    renderFunction(props);1229  } catch (error) {1230    handleRenderFunctionError(error);1231  } finally {1232    readHookLog = hookLog;1233    hookLog = [];1234    // $FlowFixMe[incompatible-use] found when upgrading Flow1235    currentDispatcher.H = previousDispatcher;1236  }1237  const rootStack =1238    ancestorStackError === undefined1239      ? ([] as Array<ParsedStackFrame>)1240      : ErrorStackParser.parse(ancestorStackError);1241  return buildTree(rootStack, readHookLog);1242}12431244// DevTools will pass the current renderer's injected dispatcher. Other apps1245// might compile debug hooks as part of their app though, so default to the1246// running React's shared internals when no dispatcher is provided.1247export function inspectHooks<Props>(1248  renderFunction: Props => React$Node,1249  props: Props,1250  currentDispatcher: ?CurrentDispatcherRef,1251): HooksTree {1252  return inspectHooksImpl(1253    renderFunction,1254    props,1255    currentDispatcher ?? ReactSharedInternals,1256  );1257}12581259// Like inspectHooks but requires an explicit dispatcher and never references1260// ReactSharedInternals, so importing it does not pull React into the bundle.1261export function inspectHooksWithoutDefaultDispatcher<Props>(1262  renderFunction: Props => React$Node,1263  props: Props,1264  currentDispatcher: CurrentDispatcherRef,1265): HooksTree {1266  return inspectHooksImpl(renderFunction, props, currentDispatcher);1267}12681269function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {1270  let current: null | Fiber = fiber;1271  while (current) {1272    if (current.tag === ContextProvider) {1273      let context: ReactContext<any> = current.type;1274      if ((context as any)._context !== undefined) {1275        // Support inspection of pre-19+ providers.1276        context = (context as any)._context;1277      }1278      if (!contextMap.has(context)) {1279        // Store the current value that we're going to restore later.1280        contextMap.set(context, context._currentValue);1281        // Set the inner most provider value on the context.1282        context._currentValue = current.memoizedProps.value;1283      }1284    }1285    current = current.return;1286  }1287}12881289function restoreContexts(contextMap: Map<ReactContext<any>, any>) {1290  contextMap.forEach((value, context) => (context._currentValue = value));1291}12921293function inspectHooksOfForwardRef<Props, Ref>(1294  renderFunction: (Props, Ref) => React$Node,1295  props: Props,1296  ref: Ref,1297  currentDispatcher: CurrentDispatcherRef,1298): HooksTree {1299  const previousDispatcher = currentDispatcher.H;1300  let readHookLog;1301  currentDispatcher.H = DispatcherProxy;1302  let ancestorStackError;1303  try {1304    ancestorStackError = new Error();1305    renderFunction(props, ref);1306  } catch (error) {1307    handleRenderFunctionError(error);1308  } finally {1309    readHookLog = hookLog;1310    hookLog = [];1311    currentDispatcher.H = previousDispatcher;1312  }1313  const rootStack =1314    ancestorStackError === undefined1315      ? ([] as Array<ParsedStackFrame>)1316      : ErrorStackParser.parse(ancestorStackError);1317  return buildTree(rootStack, readHookLog);1318}13191320function resolveDefaultProps(Component: any, baseProps: any) {1321  if (Component && Component.defaultProps) {1322    // Resolve default props. Taken from ReactElement1323    const props = assign({}, baseProps);1324    const defaultProps = Component.defaultProps;1325    for (const propName in defaultProps) {1326      if (props[propName] === undefined) {1327        props[propName] = defaultProps[propName];1328      }1329    }1330    return props;1331  }1332  return baseProps;1333}13341335// Shared implementation. Requires an explicit dispatcher and never references1336// ReactSharedInternals (it delegates to inspectHooksImpl), so importing it does1337// not pull React into the bundle.1338function inspectHooksOfFiberImpl(1339  fiber: Fiber,1340  currentDispatcher: CurrentDispatcherRef,1341): HooksTree {1342  if (1343    fiber.tag !== FunctionComponent &&1344    fiber.tag !== SimpleMemoComponent &&1345    fiber.tag !== ForwardRef1346  ) {1347    throw new Error(1348      'Unknown Fiber. Needs to be a function component to inspect hooks.',1349    );1350  }13511352  // Warm up the cache so that it doesn't consume the currentHook.1353  getPrimitiveStackCache();13541355  // Set up the current hook so that we can step through and read the1356  // current state from them.1357  currentHook = fiber.memoizedState as Hook;1358  currentFiber = fiber;1359  const thenableState =1360    fiber.dependencies && fiber.dependencies._debugThenableState;1361  // In DEV the thenableState is an inner object.1362  const usedThenables: any = thenableState1363    ? thenableState.thenables || thenableState1364    : null;1365  currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;1366  currentThenableIndex = 0;13671368  if (hasOwnProperty.call(currentFiber, 'dependencies')) {1369    // $FlowFixMe[incompatible-use]: Flow thinks hasOwnProperty might have nulled `currentFiber`1370    const dependencies = currentFiber.dependencies;1371    currentContextDependency =1372      dependencies !== null ? dependencies.firstContext : null;1373  } else if (hasOwnProperty.call(currentFiber, 'dependencies_old')) {1374    const dependencies: Dependencies = (currentFiber as any).dependencies_old;1375    currentContextDependency =1376      // $FlowFixMe[invalid-compare]1377      dependencies !== null ? dependencies.firstContext : null;1378  } else if (hasOwnProperty.call(currentFiber, 'dependencies_new')) {1379    const dependencies: Dependencies = (currentFiber as any).dependencies_new;1380    currentContextDependency =1381      // $FlowFixMe[invalid-compare]1382      dependencies !== null ? dependencies.firstContext : null;1383  } else if (hasOwnProperty.call(currentFiber, 'contextDependencies')) {1384    const contextDependencies = (currentFiber as any).contextDependencies;1385    currentContextDependency =1386      contextDependencies !== null ? contextDependencies.first : null;1387  } else {1388    throw new Error(1389      'Unsupported React version. This is a bug in React Debug Tools.',1390    );1391  }13921393  const type = fiber.type;1394  let props = fiber.memoizedProps;1395  if (type !== fiber.elementType) {1396    props = resolveDefaultProps(type, props);1397  }13981399  // Only used for versions of React without memoized context value in context dependencies.1400  const contextMap = new Map<ReactContext<any>, any>();1401  try {1402    if (1403      currentContextDependency !== null &&1404      !hasOwnProperty.call(currentContextDependency, 'memoizedValue')1405    ) {1406      setupContexts(contextMap, fiber);1407    }14081409    if (fiber.tag === ForwardRef) {1410      return inspectHooksOfForwardRef(1411        type.render,1412        props,1413        fiber.ref,1414        currentDispatcher,1415      );1416    }14171418    return inspectHooksImpl(type, props, currentDispatcher);1419  } finally {1420    currentFiber = null;1421    currentHook = null;1422    currentContextDependency = null;1423    currentThenableState = null;1424    currentThenableIndex = 0;14251426    restoreContexts(contextMap);1427  }1428}14291430// DevTools will pass the current renderer's injected dispatcher. Other apps1431// might compile debug hooks as part of their app though, so default to the1432// running React's shared internals when no dispatcher is provided.1433export function inspectHooksOfFiber(1434  fiber: Fiber,1435  currentDispatcher: ?CurrentDispatcherRef,1436): HooksTree {1437  return inspectHooksOfFiberImpl(1438    fiber,1439    currentDispatcher ?? ReactSharedInternals,1440  );1441}14421443// Like inspectHooksOfFiber but requires an explicit dispatcher and never1444// references ReactSharedInternals. Callers that always have the renderer's1445// injected dispatcher (e.g. react-devtools-facade) can use this to avoid1446// pulling React into their bundle.1447export function inspectHooksOfFiberWithoutDefaultDispatcher(1448  fiber: Fiber,1449  currentDispatcher: CurrentDispatcherRef,1450): HooksTree {1451  return inspectHooksOfFiberImpl(fiber, currentDispatcher);1452}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.