Skip to content

Commit 88ccaec

Browse files
committed
Rely on sourcemaps to compute hook name of built-in hooks
1 parent ec606a6 commit 88ccaec

File tree

3 files changed

+67
-80
lines changed

3 files changed

+67
-80
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type HookLogEntry = {
4545
stackError: Error,
4646
value: mixed,
4747
debugInfo: ReactDebugInfo | null,
48+
dispatcherMethodName: string,
4849
};
4950

5051
let hookLog: Array<HookLogEntry> = [];
@@ -130,6 +131,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
130131
// This type check is for Flow only.
131132
Dispatcher.useHostTransitionStatus();
132133
}
134+
Dispatcher.useId();
133135
} finally {
134136
readHookLog = hookLog;
135137
hookLog = [];
@@ -200,6 +202,7 @@ function use<T>(usable: Usable<T>): T {
200202
value: fulfilledValue,
201203
debugInfo:
202204
thenable._debugInfo === undefined ? null : thenable._debugInfo,
205+
dispatcherMethodName: 'use',
203206
});
204207
return fulfilledValue;
205208
}
@@ -217,6 +220,7 @@ function use<T>(usable: Usable<T>): T {
217220
value: thenable,
218221
debugInfo:
219222
thenable._debugInfo === undefined ? null : thenable._debugInfo,
223+
dispatcherMethodName: 'use',
220224
});
221225
throw SuspenseException;
222226
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
@@ -229,6 +233,7 @@ function use<T>(usable: Usable<T>): T {
229233
stackError: new Error(),
230234
value,
231235
debugInfo: null,
236+
dispatcherMethodName: 'use',
232237
});
233238

234239
return value;
@@ -247,6 +252,7 @@ function useContext<T>(context: ReactContext<T>): T {
247252
stackError: new Error(),
248253
value: value,
249254
debugInfo: null,
255+
dispatcherMethodName: 'useContext',
250256
});
251257
return value;
252258
}
@@ -268,6 +274,7 @@ function useState<S>(
268274
stackError: new Error(),
269275
value: state,
270276
debugInfo: null,
277+
dispatcherMethodName: 'useState',
271278
});
272279
return [state, (action: BasicStateAction<S>) => {}];
273280
}
@@ -290,6 +297,7 @@ function useReducer<S, I, A>(
290297
stackError: new Error(),
291298
value: state,
292299
debugInfo: null,
300+
dispatcherMethodName: 'useReducer',
293301
});
294302
return [state, (action: A) => {}];
295303
}
@@ -303,6 +311,7 @@ function useRef<T>(initialValue: T): {current: T} {
303311
stackError: new Error(),
304312
value: ref.current,
305313
debugInfo: null,
314+
dispatcherMethodName: 'useRef',
306315
});
307316
return ref;
308317
}
@@ -315,6 +324,7 @@ function useCacheRefresh(): () => void {
315324
stackError: new Error(),
316325
value: hook !== null ? hook.memoizedState : function refresh() {},
317326
debugInfo: null,
327+
dispatcherMethodName: 'useCacheRefresh',
318328
});
319329
return () => {};
320330
}
@@ -330,6 +340,7 @@ function useLayoutEffect(
330340
stackError: new Error(),
331341
value: create,
332342
debugInfo: null,
343+
dispatcherMethodName: 'useLayoutEffect',
333344
});
334345
}
335346

@@ -344,6 +355,7 @@ function useInsertionEffect(
344355
stackError: new Error(),
345356
value: create,
346357
debugInfo: null,
358+
dispatcherMethodName: 'useInsertionEffect',
347359
});
348360
}
349361

@@ -358,6 +370,7 @@ function useEffect(
358370
stackError: new Error(),
359371
value: create,
360372
debugInfo: null,
373+
dispatcherMethodName: 'useEffect',
361374
});
362375
}
363376

@@ -381,6 +394,7 @@ function useImperativeHandle<T>(
381394
stackError: new Error(),
382395
value: instance,
383396
debugInfo: null,
397+
dispatcherMethodName: 'useImperativeHandle',
384398
});
385399
}
386400

@@ -391,6 +405,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
391405
stackError: new Error(),
392406
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
393407
debugInfo: null,
408+
dispatcherMethodName: 'useDebugValue',
394409
});
395410
}
396411

@@ -402,6 +417,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
402417
stackError: new Error(),
403418
value: hook !== null ? hook.memoizedState[0] : callback,
404419
debugInfo: null,
420+
dispatcherMethodName: 'useCallback',
405421
});
406422
return callback;
407423
}
@@ -418,6 +434,7 @@ function useMemo<T>(
418434
stackError: new Error(),
419435
value,
420436
debugInfo: null,
437+
dispatcherMethodName: 'useMemo',
421438
});
422439
return value;
423440
}
@@ -439,6 +456,7 @@ function useSyncExternalStore<T>(
439456
stackError: new Error(),
440457
value,
441458
debugInfo: null,
459+
dispatcherMethodName: 'useSyncExternalStore',
442460
});
443461
return value;
444462
}
@@ -458,6 +476,7 @@ function useTransition(): [
458476
stackError: new Error(),
459477
value: undefined,
460478
debugInfo: null,
479+
dispatcherMethodName: 'useTransition',
461480
});
462481
return [false, callback => {}];
463482
}
@@ -470,6 +489,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
470489
stackError: new Error(),
471490
value: hook !== null ? hook.memoizedState : value,
472491
debugInfo: null,
492+
dispatcherMethodName: 'useDeferredValue',
473493
});
474494
return value;
475495
}
@@ -483,6 +503,7 @@ function useId(): string {
483503
stackError: new Error(),
484504
value: id,
485505
debugInfo: null,
506+
dispatcherMethodName: 'useId',
486507
});
487508
return id;
488509
}
@@ -533,6 +554,7 @@ function useOptimistic<S, A>(
533554
stackError: new Error(),
534555
value: state,
535556
debugInfo: null,
557+
dispatcherMethodName: 'useOptimistic',
536558
});
537559
return [state, (action: A) => {}];
538560
}
@@ -591,6 +613,7 @@ function useFormState<S, P>(
591613
stackError: stackError,
592614
value: value,
593615
debugInfo: debugInfo,
616+
dispatcherMethodName: 'useFormState',
594617
});
595618

596619
if (error !== null) {
@@ -618,6 +641,7 @@ function useHostTransitionStatus(): TransitionStatus {
618641
stackError: new Error(),
619642
value: status,
620643
debugInfo: null,
644+
dispatcherMethodName: 'HostTransitionStatus',
621645
});
622646

623647
return status;
@@ -696,8 +720,7 @@ export type HooksTree = Array<HooksNode>;
696720
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
697721
// in a wrapper constructor like a polyfill. That'll add an extra frame.
698722
// Similar things can happen with the call to the dispatcher. The top frame
699-
// may not be the primitive. Likewise the primitive can have fewer stack frames
700-
// such as when a call to useState got inlined to use dispatcher.useState.
723+
// may not be the primitive.
701724
//
702725
// We also can't assume that the last frame of the root call is the same
703726
// frame as the last frame of the hook call because long stack traces can be
@@ -747,26 +770,16 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
747770
return -1;
748771
}
749772

750-
function isReactWrapper(functionName: any, primitiveName: string) {
773+
function isReactWrapper(functionName: any, wrapperName: string) {
751774
if (!functionName) {
752775
return false;
753776
}
754-
switch (primitiveName) {
755-
case 'Context':
756-
case 'Context (use)':
757-
case 'Promise':
758-
case 'Unresolved':
759-
if (functionName.endsWith('use')) {
760-
return true;
761-
}
762-
}
763-
const expectedPrimitiveName = 'use' + primitiveName;
764-
if (functionName.length < expectedPrimitiveName.length) {
777+
if (functionName.length < wrapperName.length) {
765778
return false;
766779
}
767780
return (
768-
functionName.lastIndexOf(expectedPrimitiveName) ===
769-
functionName.length - expectedPrimitiveName.length
781+
functionName.lastIndexOf(wrapperName) ===
782+
functionName.length - wrapperName.length
770783
);
771784
}
772785

@@ -778,18 +791,14 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
778791
}
779792
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
780793
if (primitiveStack[i].source !== hookStack[i].source) {
781-
// If the next two frames are functions called `useX` then we assume that they're part of the
782-
// wrappers that the React packager or other packages adds around the dispatcher.
794+
// If the next frame is a method from the dispatcher, we
795+
// assume that the next frame after that is the actual public API call.
796+
// This prohibits nesting dispatcher calls in hooks.
783797
if (
784798
i < hookStack.length - 1 &&
785-
isReactWrapper(hookStack[i].functionName, hook.primitive)
799+
isReactWrapper(hookStack[i].functionName, hook.dispatcherMethodName)
786800
) {
787801
i++;
788-
}
789-
if (
790-
i < hookStack.length - 1 &&
791-
isReactWrapper(hookStack[i].functionName, hook.primitive)
792-
) {
793802
i++;
794803
}
795804
return i;
@@ -812,16 +821,21 @@ function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
812821
// Something went wrong. Give up.
813822
return null;
814823
}
815-
return hookStack.slice(primitiveIndex, rootIndex - 1);
824+
return [
825+
hookStack[primitiveIndex - 1],
826+
hookStack.slice(primitiveIndex, rootIndex - 1),
827+
];
816828
}
817829

818-
function parseCustomHookName(functionName: void | string): string {
830+
function parseHookName(functionName: void | string): string {
819831
if (!functionName) {
820832
return '';
821833
}
822834
let startIndex = functionName.lastIndexOf('.');
823835
if (startIndex === -1) {
824836
startIndex = 0;
837+
} else {
838+
startIndex += 1;
825839
}
826840
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
827841
startIndex += 3;
@@ -840,8 +854,14 @@ function buildTree(
840854
const stackOfChildren = [];
841855
for (let i = 0; i < readHookLog.length; i++) {
842856
const hook = readHookLog[i];
843-
const stack = parseTrimmedStack(rootStack, hook);
844-
if (stack !== null) {
857+
const parseResult = parseTrimmedStack(rootStack, hook);
858+
let displayName = hook.displayName;
859+
if (parseResult !== null) {
860+
const [primitiveFrame, stack] = parseResult;
861+
if (hook.displayName === null) {
862+
// TODO: Support older versions of React without sourcemaps by using the primitive name if primitiveFrame.functionName does not look like a hook.
863+
displayName = parseHookName(primitiveFrame.functionName);
864+
}
845865
// Note: The indices 0 <= n < length-1 will contain the names.
846866
// The indices 1 <= n < length will contain the source locations.
847867
// That's why we get the name from n - 1 and don't check the source
@@ -871,7 +891,7 @@ function buildTree(
871891
const levelChild: HooksNode = {
872892
id: null,
873893
isStateEditable: false,
874-
name: parseCustomHookName(stack[j - 1].functionName),
894+
name: parseHookName(stack[j - 1].functionName),
875895
value: undefined,
876896
subHooks: children,
877897
debugInfo: null,
@@ -889,7 +909,7 @@ function buildTree(
889909
}
890910
prevStack = stack;
891911
}
892-
const {displayName, primitive, debugInfo} = hook;
912+
const {primitive, debugInfo} = hook;
893913

894914
// For now, the "id" of stateful hooks is just the stateful hook index.
895915
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
@@ -905,11 +925,11 @@ function buildTree(
905925

906926
// For the time being, only State and Reducer hooks support runtime overrides.
907927
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
908-
const name = displayName || primitive;
928+
909929
const levelChild: HooksNode = {
910930
id,
911931
isStateEditable,
912-
name: name,
932+
name: displayName || 'Unknown',
913933
value: hook.value,
914934
subHooks: [],
915935
debugInfo: debugInfo,
@@ -922,6 +942,7 @@ function buildTree(
922942
fileName: null,
923943
columnNumber: null,
924944
};
945+
const stack = parseResult !== null ? parseResult[1] : null;
925946
if (stack && stack.length >= 1) {
926947
const stackFrame = stack[0];
927948
hookSource.lineNumber = stackFrame.lineNumber;

0 commit comments

Comments
 (0)