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.