main
js 1,452 lines 43.4 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9 import type {StackFrame as ParsedStackFrame} from 'error-stack-parser';
10 import type {
11 Awaited,
12 ReactContext,
13 StartTransitionOptions,
14 Usable,
15 Thenable,
16 ReactDebugInfo,
17 } from 'shared/ReactTypes';
18 import type {
19 ContextDependency,
20 Dependencies,
21 Fiber,
22 Dispatcher as DispatcherType,
23 } from 'react-reconciler/src/ReactInternalTypes';
24 import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig';
25
26 import ErrorStackParser from 'error-stack-parser';
27 import assign from 'shared/assign';
28 import ReactSharedInternals from 'shared/ReactSharedInternals';
29 import {
30 FunctionComponent,
31 SimpleMemoComponent,
32 ContextProvider,
33 ForwardRef,
34 } from 'react-reconciler/src/ReactWorkTags';
35 import {
36 REACT_MEMO_CACHE_SENTINEL,
37 REACT_CONTEXT_TYPE,
38 REACT_RECOVERABLE_TYPE,
39 } from 'shared/ReactSymbols';
40 import hasOwnProperty from 'shared/hasOwnProperty';
41
42 type CurrentDispatcherRef = typeof ReactSharedInternals;
43
44 // Used to track hooks called during a render
45
46 type HookLogEntry = {
47 displayName: string | null,
48 primitive: string,
49 stackError: Error,
50 value: mixed,
51 debugInfo: ReactDebugInfo | null,
52 dispatcherHookName: string,
53 };
54
55 let hookLog: Array<HookLogEntry> = [];
56
57 // Primitives
58
59 type BasicStateAction<S> = (S => S) | S;
60
61 type Dispatch<A> = A => void;
62
63 let primitiveStackCache: null | Map<string, Array<any>> = null;
64
65 type Hook = {
66 memoizedState: any,
67 next: Hook | null,
68 };
69
70 function getPrimitiveStackCache(): Map<string, Array<any>> {
71 // This initializes a cache of all primitive hooks so that the top
72 // 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 }
130
131 Dispatcher.useId();
132
133 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 }
148
149 let currentFiber: null | Fiber = null;
150 let currentHook: null | Hook = null;
151 let currentContextDependency: null | ContextDependency<mixed> = null;
152 let currentThenableIndex: number = 0;
153 let currentThenableState: null | Array<Thenable<mixed>> = null;
154
155 function nextHook(): null | Hook {
156 const hook = currentHook;
157 if (hook !== null) {
158 currentHook = hook.next;
159 }
160 return hook;
161 }
162
163 function readContext<T>(context: ReactContext<T>): T {
164 if (currentFiber === null) {
165 // Hook inspection without access to the Fiber tree
166 // 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 }
174
175 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;
180
181 // $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 }
189
190 return value;
191 }
192 }
193
194 const 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 );
203
204 function 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 one
211 // stashed in it. It's possible for uncached Promises to be new each render
212 // and in that case the one we're inspecting is the in the thenable state.
213 currentThenableState !== null &&
214 currentThenableIndex < currentThenableState.length
215 ? currentThenableState[currentThenableIndex++]
216 : (usable as any);
217
218 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 attempt
238 // 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);
262
263 hookLog.push({
264 displayName: context.displayName || 'Context',
265 primitive: 'Context (use)',
266 stackError: new Error(),
267 value,
268 debugInfo: null,
269 dispatcherHookName: 'Use',
270 });
271
272 return value;
273 }
274 }
275
276 // eslint-disable-next-line react-internal/safe-string-coercion
277 throw new Error('An unsupported type was passed to use(): ' + String(usable));
278 }
279
280 function 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 }
292
293 function useState<S>(
294 initialState: (() => S) | S,
295 ): [S, Dispatch<BasicStateAction<S>>] {
296 const hook = nextHook();
297 const state: S =
298 hook !== null
299 ? hook.memoizedState
300 : typeof initialState === 'function'
301 ? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
302 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 }
314
315 function 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 }
337
338 function 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 }
351
352 function 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 }
364
365 function 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 }
379
380 function 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 }
394
395 function 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 }
409
410 function 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 callback
417 // and if there is a ref callback it might not store it but if it does we
418 // have no way of knowing where. So let's only enable introspection of the
419 // 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 }
433
434 function 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 }
444
445 function 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 }
457
458 function 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 }
474
475 function 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 times
482 // so that subsequent hooks have the right memoized state.
483 const hook = nextHook(); // SyncExternalStore
484 nextHook(); // Effect
485 // 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 }
498
499 function 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 times
505 // so that subsequent hooks have the right memoized state.
506 const stateHook = nextHook();
507 nextHook(); // Callback
508
509 const isPending = stateHook !== null ? stateHook.memoizedState : false;
510
511 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 }
521
522 function 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 }
535
536 function 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 }
549
550 // useMemoCache is an implementation detail of Forget's memoization
551 // it should not be called directly in user-generated code
552 function useMemoCache(size: number): Array<mixed> {
553 const fiber = currentFiber;
554 // Don't throw, in case this is called from getPrimitiveStackCache
555 if (fiber == null) {
556 return [];
557 }
558
559 const memoCache =
560 // $FlowFixMe[incompatible-use]: updateQueue is mixed
561 fiber.updateQueue != null ? fiber.updateQueue.memoCache : null;
562 if (memoCache == null) {
563 return [];
564 }
565
566 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 }
573
574 // We don't write anything to hookLog on purpose, so this hook remains invisible to users.
575
576 memoCache.index++;
577 return data;
578 }
579
580 function 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 }
601
602 function 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(); // FormState
608 nextHook(); // PendingState
609 nextHook(); // ActionQueue
610 const stackError = new Error();
611 let value;
612 let debugInfo = null;
613 let error = null;
614
615 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 attempt
638 // 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 }
650
651 hookLog.push({
652 displayName: null,
653 primitive: 'FormState',
654 stackError: stackError,
655 value: value,
656 debugInfo: debugInfo,
657 dispatcherHookName: 'FormState',
658 });
659
660 if (error !== null) {
661 throw error;
662 }
663
664 // value being a Thenable is equivalent to error being not null
665 // i.e. we only reach this point with Awaited<S>
666 const state = value as any as Awaited<S>;
667
668 // TODO: support displaying pending value
669 return [state, (payload: P) => {}, false];
670 }
671
672 function 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(); // FormState
678 nextHook(); // PendingState
679 nextHook(); // ActionQueue
680 const stackError = new Error();
681 let value;
682 let debugInfo = null;
683 let error = null;
684
685 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 attempt
708 // 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 }
720
721 hookLog.push({
722 displayName: null,
723 primitive: 'ActionState',
724 stackError: stackError,
725 value: value,
726 debugInfo: debugInfo,
727 dispatcherHookName: 'ActionState',
728 });
729
730 if (error !== null) {
731 throw error;
732 }
733
734 // value being a Thenable is equivalent to error being not null
735 // i.e. we only reach this point with Awaited<S>
736 const state = value as any as Awaited<S>;
737
738 // TODO: support displaying pending value
739 return [state, (payload: P) => {}, false];
740 }
741
742 function useHostTransitionStatus(): TransitionStatus {
743 const status = readContext<TransitionStatus>(
744 // $FlowFixMe[prop-missing] `readContext` only needs _currentValue
745 // $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 );
751
752 hookLog.push({
753 displayName: null,
754 primitive: 'HostTransitionStatus',
755 stackError: new Error(),
756 value: status,
757 debugInfo: null,
758 dispatcherHookName: 'HostTransitionStatus',
759 });
760
761 return status;
762 }
763
764 function 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 });
774
775 return callback;
776 }
777
778 const Dispatcher: DispatcherType = {
779 readContext,
780
781 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 };
805
806 // create a proxy to throw a custom error
807 // in case future versions of React adds more hooks
808 const 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-shared
816 // TODO: refactor this if we ever combine the devtools and debug tools packages
817 error.name = 'ReactDebugToolsUnsupportedHookError';
818 throw error;
819 },
820 };
821
822 // `Proxy` may not exist on some platforms
823 const DispatcherProxy =
824 typeof Proxy === 'undefined'
825 ? Dispatcher
826 : new Proxy(Dispatcher, DispatcherProxyHandler);
827
828 // Inspect
829
830 export type HookSource = {
831 lineNumber: number | null,
832 columnNumber: number | null,
833 fileName: string | null,
834 functionName: string | null,
835 };
836
837 export 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 };
846 export type HooksTree = Array<HooksNode>;
847
848 // Don't assume
849 //
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 stack
852 // 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 frame
855 // may not be the primitive.
856 //
857 // We also can't assume that the last frame of the root call is the same
858 // frame as the last frame of the hook call because long stack traces can be
859 // truncated to a stack trace limit.
860
861 let mostLikelyAncestorIndex = 0;
862
863 function 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 }
887
888 function 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 }
911
912 function isReactWrapper(functionName: void | string, wrapperName: string) {
913 const hookName = parseHookName(functionName);
914 if (wrapperName === 'HostTransitionStatus') {
915 return hookName === wrapperName || hookName === 'FormStatus';
916 }
917
918 return hookName === wrapperName;
919 }
920
921 function 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 stack
929 // For React Native (uses Hermes), these source fields will be identical and skipped
930 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 the
932 // 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 }
945
946 return i;
947 }
948 }
949 return -1;
950 }
951
952 function parseTrimmedStack(rootStack: ParsedStackFrame[], hook: HookLogEntry) {
953 // Get the stack trace between the primitive hook function and
954 // 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 < 2
962 ) {
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 }
975
976 function parseHookName(functionName: void | string): string {
977 if (!functionName) {
978 return '';
979 }
980 let startIndex = functionName.lastIndexOf('[as ');
981
982 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 }
994
995 if (functionName.slice(startIndex).startsWith('unstable_')) {
996 startIndex += 'unstable_'.length;
997 }
998
999 if (functionName.slice(startIndex).startsWith('experimental_')) {
1000 startIndex += 'experimental_'.length;
1001 }
1002
1003 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 }
1011
1012 function 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 source
1038 // 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 them
1058 // 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 === undefined
1072 ? null
1073 : stackFrame.lineNumber,
1074 columnNumber:
1075 stackFrame.columnNumber === undefined
1076 ? null
1077 : stackFrame.columnNumber,
1078 functionName:
1079 stackFrame.functionName === undefined
1080 ? null
1081 : stackFrame.functionName,
1082 fileName:
1083 stackFrame.fileName === undefined ? null : stackFrame.fileName,
1084 },
1085 };
1086
1087 levelChildren.push(levelChild);
1088 stackOfChildren.push(levelChildren);
1089 levelChildren = children;
1090 }
1091 prevStack = stack;
1092 }
1093 const {primitive, debugInfo} = hook;
1094
1095 // 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 ? null
1105 : nativeHookID++;
1106
1107 // 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 };
1119
1120 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 }
1137
1138 levelChild.hookSource = hookSource;
1139
1140 levelChildren.push(levelChild);
1141 }
1142
1143 // Associate custom hook values (useDebugValue() hook entries) with the correct hooks.
1144 processDebugValues(rootChildren, null);
1145
1146 return rootChildren;
1147 }
1148
1149 // 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.
1154 function processDebugValues(
1155 hooksTree: HooksTree,
1156 parentHooksNode: HooksNode | null,
1157 ): void {
1158 const debugValueHooksNodes: Array<HooksNode> = [];
1159
1160 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 }
1170
1171 // 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 }
1182
1183 function 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 of
1187 // 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 means
1197 // that the error is caused by user's code in renderFunction.
1198 // In this case, we should wrap the original error inside a custom error
1199 // so that devtools can give a clear message about it.
1200 // $FlowFixMe[extra-arg]: Flow doesn't know about 2nd argument of Error constructor
1201 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-shared
1205 // TODO: refactor this if we ever combine the devtools and debug tools packages
1206 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 }
1212
1213 // Shared implementation. Requires an explicit dispatcher and never references
1214 // ReactSharedInternals, so importing it does not pull React into the bundle.
1215 function inspectHooksImpl<Props>(
1216 renderFunction: Props => React$Node,
1217 props: Props,
1218 currentDispatcher: CurrentDispatcherRef,
1219 ): HooksTree {
1220 const previousDispatcher = currentDispatcher.H;
1221 currentDispatcher.H = DispatcherProxy;
1222
1223 let readHookLog;
1224 let ancestorStackError;
1225
1226 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 Flow
1235 currentDispatcher.H = previousDispatcher;
1236 }
1237 const rootStack =
1238 ancestorStackError === undefined
1239 ? ([] as Array<ParsedStackFrame>)
1240 : ErrorStackParser.parse(ancestorStackError);
1241 return buildTree(rootStack, readHookLog);
1242 }
1243
1244 // DevTools will pass the current renderer's injected dispatcher. Other apps
1245 // might compile debug hooks as part of their app though, so default to the
1246 // running React's shared internals when no dispatcher is provided.
1247 export 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 }
1258
1259 // Like inspectHooks but requires an explicit dispatcher and never references
1260 // ReactSharedInternals, so importing it does not pull React into the bundle.
1261 export function inspectHooksWithoutDefaultDispatcher<Props>(
1262 renderFunction: Props => React$Node,
1263 props: Props,
1264 currentDispatcher: CurrentDispatcherRef,
1265 ): HooksTree {
1266 return inspectHooksImpl(renderFunction, props, currentDispatcher);
1267 }
1268
1269 function 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 }
1288
1289 function restoreContexts(contextMap: Map<ReactContext<any>, any>) {
1290 contextMap.forEach((value, context) => (context._currentValue = value));
1291 }
1292
1293 function 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 === undefined
1315 ? ([] as Array<ParsedStackFrame>)
1316 : ErrorStackParser.parse(ancestorStackError);
1317 return buildTree(rootStack, readHookLog);
1318 }
1319
1320 function resolveDefaultProps(Component: any, baseProps: any) {
1321 if (Component && Component.defaultProps) {
1322 // Resolve default props. Taken from ReactElement
1323 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 }
1334
1335 // Shared implementation. Requires an explicit dispatcher and never references
1336 // ReactSharedInternals (it delegates to inspectHooksImpl), so importing it does
1337 // not pull React into the bundle.
1338 function inspectHooksOfFiberImpl(
1339 fiber: Fiber,
1340 currentDispatcher: CurrentDispatcherRef,
1341 ): HooksTree {
1342 if (
1343 fiber.tag !== FunctionComponent &&
1344 fiber.tag !== SimpleMemoComponent &&
1345 fiber.tag !== ForwardRef
1346 ) {
1347 throw new Error(
1348 'Unknown Fiber. Needs to be a function component to inspect hooks.',
1349 );
1350 }
1351
1352 // Warm up the cache so that it doesn't consume the currentHook.
1353 getPrimitiveStackCache();
1354
1355 // Set up the current hook so that we can step through and read the
1356 // 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 = thenableState
1363 ? thenableState.thenables || thenableState
1364 : null;
1365 currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
1366 currentThenableIndex = 0;
1367
1368 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 }
1392
1393 const type = fiber.type;
1394 let props = fiber.memoizedProps;
1395 if (type !== fiber.elementType) {
1396 props = resolveDefaultProps(type, props);
1397 }
1398
1399 // 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 }
1408
1409 if (fiber.tag === ForwardRef) {
1410 return inspectHooksOfForwardRef(
1411 type.render,
1412 props,
1413 fiber.ref,
1414 currentDispatcher,
1415 );
1416 }
1417
1418 return inspectHooksImpl(type, props, currentDispatcher);
1419 } finally {
1420 currentFiber = null;
1421 currentHook = null;
1422 currentContextDependency = null;
1423 currentThenableState = null;
1424 currentThenableIndex = 0;
1425
1426 restoreContexts(contextMap);
1427 }
1428 }
1429
1430 // DevTools will pass the current renderer's injected dispatcher. Other apps
1431 // might compile debug hooks as part of their app though, so default to the
1432 // running React's shared internals when no dispatcher is provided.
1433 export function inspectHooksOfFiber(
1434 fiber: Fiber,
1435 currentDispatcher: ?CurrentDispatcherRef,
1436 ): HooksTree {
1437 return inspectHooksOfFiberImpl(
1438 fiber,
1439 currentDispatcher ?? ReactSharedInternals,
1440 );
1441 }
1442
1443 // Like inspectHooksOfFiber but requires an explicit dispatcher and never
1444 // references ReactSharedInternals. Callers that always have the renderer's
1445 // injected dispatcher (e.g. react-devtools-facade) can use this to avoid
1446 // pulling React into their bundle.
1447 export function inspectHooksOfFiberWithoutDefaultDispatcher(
1448 fiber: Fiber,
1449 currentDispatcher: CurrentDispatcherRef,
1450 ): HooksTree {
1451 return inspectHooksOfFiberImpl(fiber, currentDispatcher);
1452 }