Skip to content

Commit dc19b37

Browse files
committed
DevTools: Rely on sourcemaps to compute hook name of built-in hooks
As a fallback, we assume the dispatcher method name is the hook name. This holds for currently released React versions. As a follow-up, we introduce a separate field that holds the list of possible wrappers we try to match against e.g. for `useFormStatus` -> `dispatcher.useHostTransitionStatus`.
1 parent 5cec48e commit dc19b37

File tree

2 files changed

+62
-30
lines changed

2 files changed

+62
-30
lines changed

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

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type HookLogEntry = {
4747
stackError: Error,
4848
value: mixed,
4949
debugInfo: ReactDebugInfo | null,
50+
dispatcherMethodName: string,
5051
};
5152

5253
let hookLog: Array<HookLogEntry> = [];
@@ -127,6 +128,8 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
127128
);
128129
} catch (x) {}
129130
}
131+
132+
Dispatcher.useId();
130133
} finally {
131134
readHookLog = hookLog;
132135
hookLog = [];
@@ -203,6 +206,7 @@ function use<T>(usable: Usable<T>): T {
203206
value: fulfilledValue,
204207
debugInfo:
205208
thenable._debugInfo === undefined ? null : thenable._debugInfo,
209+
dispatcherMethodName: 'use',
206210
});
207211
return fulfilledValue;
208212
}
@@ -220,6 +224,7 @@ function use<T>(usable: Usable<T>): T {
220224
value: thenable,
221225
debugInfo:
222226
thenable._debugInfo === undefined ? null : thenable._debugInfo,
227+
dispatcherMethodName: 'use',
223228
});
224229
throw SuspenseException;
225230
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
@@ -232,6 +237,7 @@ function use<T>(usable: Usable<T>): T {
232237
stackError: new Error(),
233238
value,
234239
debugInfo: null,
240+
dispatcherMethodName: 'use',
235241
});
236242

237243
return value;
@@ -250,6 +256,7 @@ function useContext<T>(context: ReactContext<T>): T {
250256
stackError: new Error(),
251257
value: value,
252258
debugInfo: null,
259+
dispatcherMethodName: 'useContext',
253260
});
254261
return value;
255262
}
@@ -271,6 +278,7 @@ function useState<S>(
271278
stackError: new Error(),
272279
value: state,
273280
debugInfo: null,
281+
dispatcherMethodName: 'useState',
274282
});
275283
return [state, (action: BasicStateAction<S>) => {}];
276284
}
@@ -293,6 +301,7 @@ function useReducer<S, I, A>(
293301
stackError: new Error(),
294302
value: state,
295303
debugInfo: null,
304+
dispatcherMethodName: 'useReducer',
296305
});
297306
return [state, (action: A) => {}];
298307
}
@@ -306,6 +315,7 @@ function useRef<T>(initialValue: T): {current: T} {
306315
stackError: new Error(),
307316
value: ref.current,
308317
debugInfo: null,
318+
dispatcherMethodName: 'useRef',
309319
});
310320
return ref;
311321
}
@@ -318,6 +328,7 @@ function useCacheRefresh(): () => void {
318328
stackError: new Error(),
319329
value: hook !== null ? hook.memoizedState : function refresh() {},
320330
debugInfo: null,
331+
dispatcherMethodName: 'useCacheRefresh',
321332
});
322333
return () => {};
323334
}
@@ -333,6 +344,7 @@ function useLayoutEffect(
333344
stackError: new Error(),
334345
value: create,
335346
debugInfo: null,
347+
dispatcherMethodName: 'useLayoutEffect',
336348
});
337349
}
338350

@@ -347,6 +359,7 @@ function useInsertionEffect(
347359
stackError: new Error(),
348360
value: create,
349361
debugInfo: null,
362+
dispatcherMethodName: 'useInsertionEffect',
350363
});
351364
}
352365

@@ -361,6 +374,7 @@ function useEffect(
361374
stackError: new Error(),
362375
value: create,
363376
debugInfo: null,
377+
dispatcherMethodName: 'useEffect',
364378
});
365379
}
366380

@@ -384,6 +398,7 @@ function useImperativeHandle<T>(
384398
stackError: new Error(),
385399
value: instance,
386400
debugInfo: null,
401+
dispatcherMethodName: 'useImperativeHandle',
387402
});
388403
}
389404

@@ -394,6 +409,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
394409
stackError: new Error(),
395410
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
396411
debugInfo: null,
412+
dispatcherMethodName: 'useDebugValue',
397413
});
398414
}
399415

@@ -405,6 +421,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
405421
stackError: new Error(),
406422
value: hook !== null ? hook.memoizedState[0] : callback,
407423
debugInfo: null,
424+
dispatcherMethodName: 'useCallback',
408425
});
409426
return callback;
410427
}
@@ -421,6 +438,7 @@ function useMemo<T>(
421438
stackError: new Error(),
422439
value,
423440
debugInfo: null,
441+
dispatcherMethodName: 'useMemo',
424442
});
425443
return value;
426444
}
@@ -442,6 +460,7 @@ function useSyncExternalStore<T>(
442460
stackError: new Error(),
443461
value,
444462
debugInfo: null,
463+
dispatcherMethodName: 'useSyncExternalStore',
445464
});
446465
return value;
447466
}
@@ -464,6 +483,7 @@ function useTransition(): [
464483
stackError: new Error(),
465484
value: isPending,
466485
debugInfo: null,
486+
dispatcherMethodName: 'useTransition',
467487
});
468488
return [isPending, () => {}];
469489
}
@@ -477,6 +497,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
477497
stackError: new Error(),
478498
value: prevValue,
479499
debugInfo: null,
500+
dispatcherMethodName: 'useDeferredValue',
480501
});
481502
return prevValue;
482503
}
@@ -490,6 +511,7 @@ function useId(): string {
490511
stackError: new Error(),
491512
value: id,
492513
debugInfo: null,
514+
dispatcherMethodName: 'useId',
493515
});
494516
return id;
495517
}
@@ -540,6 +562,7 @@ function useOptimistic<S, A>(
540562
stackError: new Error(),
541563
value: state,
542564
debugInfo: null,
565+
dispatcherMethodName: 'useOptimistic',
543566
});
544567
return [state, (action: A) => {}];
545568
}
@@ -599,6 +622,7 @@ function useFormState<S, P>(
599622
stackError: stackError,
600623
value: value,
601624
debugInfo: debugInfo,
625+
dispatcherMethodName: 'useFormState',
602626
});
603627

604628
if (error !== null) {
@@ -685,8 +709,7 @@ export type HooksTree = Array<HooksNode>;
685709
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
686710
// in a wrapper constructor like a polyfill. That'll add an extra frame.
687711
// Similar things can happen with the call to the dispatcher. The top frame
688-
// may not be the primitive. Likewise the primitive can have fewer stack frames
689-
// such as when a call to useState got inlined to use dispatcher.useState.
712+
// may not be the primitive.
690713
//
691714
// We also can't assume that the last frame of the root call is the same
692715
// frame as the last frame of the hook call because long stack traces can be
@@ -736,26 +759,16 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
736759
return -1;
737760
}
738761

739-
function isReactWrapper(functionName: any, primitiveName: string) {
762+
function isReactWrapper(functionName: any, wrapperName: string) {
740763
if (!functionName) {
741764
return false;
742765
}
743-
switch (primitiveName) {
744-
case 'Context':
745-
case 'Context (use)':
746-
case 'Promise':
747-
case 'Unresolved':
748-
if (functionName.endsWith('use')) {
749-
return true;
750-
}
751-
}
752-
const expectedPrimitiveName = 'use' + primitiveName;
753-
if (functionName.length < expectedPrimitiveName.length) {
766+
if (functionName.length < wrapperName.length) {
754767
return false;
755768
}
756769
return (
757-
functionName.lastIndexOf(expectedPrimitiveName) ===
758-
functionName.length - expectedPrimitiveName.length
770+
functionName.lastIndexOf(wrapperName) ===
771+
functionName.length - wrapperName.length
759772
);
760773
}
761774

@@ -767,17 +780,18 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
767780
}
768781
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
769782
if (primitiveStack[i].source !== hookStack[i].source) {
770-
// If the next two frames are functions called `useX` then we assume that they're part of the
771-
// wrappers that the React packager or other packages adds around the dispatcher.
783+
// If the next frame is a method from the dispatcher, we
784+
// assume that the next frame after that is the actual public API call.
785+
// This prohibits nesting dispatcher calls in hooks.
772786
if (
773787
i < hookStack.length - 1 &&
774-
isReactWrapper(hookStack[i].functionName, hook.primitive)
788+
isReactWrapper(hookStack[i].functionName, hook.dispatcherMethodName)
775789
) {
776790
i++;
777791
}
778792
if (
779793
i < hookStack.length - 1 &&
780-
isReactWrapper(hookStack[i].functionName, hook.primitive)
794+
isReactWrapper(hookStack[i].functionName, hook.dispatcherMethodName)
781795
) {
782796
i++;
783797
}
@@ -801,18 +815,26 @@ function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
801815
// Something went wrong. Give up.
802816
return null;
803817
}
804-
return hookStack.slice(primitiveIndex, rootIndex - 1);
818+
return [
819+
hookStack[primitiveIndex - 1],
820+
hookStack.slice(primitiveIndex, rootIndex - 1),
821+
];
805822
}
806823

807-
function parseCustomHookName(functionName: void | string): string {
824+
function parseHookName(functionName: void | string): string {
808825
if (!functionName) {
809826
return '';
810827
}
811828
let startIndex = functionName.lastIndexOf('.');
812829
if (startIndex === -1) {
813830
startIndex = 0;
831+
} else {
832+
startIndex += 1;
814833
}
815834
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
835+
if (functionName.length - startIndex === 3) {
836+
return 'Use';
837+
}
816838
startIndex += 3;
817839
}
818840
return functionName.slice(startIndex);
@@ -829,8 +851,17 @@ function buildTree(
829851
const stackOfChildren = [];
830852
for (let i = 0; i < readHookLog.length; i++) {
831853
const hook = readHookLog[i];
832-
const stack = parseTrimmedStack(rootStack, hook);
833-
if (stack !== null) {
854+
const parseResult = parseTrimmedStack(rootStack, hook);
855+
let displayName = hook.displayName;
856+
if (parseResult !== null) {
857+
const [primitiveFrame, stack] = parseResult;
858+
if (hook.displayName === null) {
859+
displayName =
860+
parseHookName(primitiveFrame.functionName) ||
861+
// Older versions of React do not have sourcemaps.
862+
// In those versions there was always a 1:1 mapping between wrapper and dispatcher method.
863+
parseHookName(hook.dispatcherMethodName);
864+
}
834865
// Note: The indices 0 <= n < length-1 will contain the names.
835866
// The indices 1 <= n < length will contain the source locations.
836867
// That's why we get the name from n - 1 and don't check the source
@@ -860,7 +891,7 @@ function buildTree(
860891
const levelChild: HooksNode = {
861892
id: null,
862893
isStateEditable: false,
863-
name: parseCustomHookName(stack[j - 1].functionName),
894+
name: parseHookName(stack[j - 1].functionName),
864895
value: undefined,
865896
subHooks: children,
866897
debugInfo: null,
@@ -878,7 +909,7 @@ function buildTree(
878909
}
879910
prevStack = stack;
880911
}
881-
const {displayName, primitive, debugInfo} = hook;
912+
const {primitive, debugInfo} = hook;
882913

883914
// For now, the "id" of stateful hooks is just the stateful hook index.
884915
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
@@ -893,11 +924,11 @@ function buildTree(
893924

894925
// For the time being, only State and Reducer hooks support runtime overrides.
895926
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
896-
const name = displayName || primitive;
927+
897928
const levelChild: HooksNode = {
898929
id,
899930
isStateEditable,
900-
name: name,
931+
name: displayName || 'Unknown',
901932
value: hook.value,
902933
subHooks: [],
903934
debugInfo: debugInfo,
@@ -910,6 +941,7 @@ function buildTree(
910941
fileName: null,
911942
columnNumber: null,
912943
};
944+
const stack = parseResult !== null ? parseResult[1] : null;
913945
if (stack && stack.length >= 1) {
914946
const stackFrame = stack[0];
915947
hookSource.lineNumber = stackFrame.lineNumber;

packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ describe('ReactHooksInspection', () => {
551551
},
552552
"id": null,
553553
"isStateEditable": false,
554-
"name": "Promise",
554+
"name": "Use",
555555
"subHooks": [],
556556
"value": "world",
557557
},
@@ -601,7 +601,7 @@ describe('ReactHooksInspection', () => {
601601
},
602602
"id": null,
603603
"isStateEditable": false,
604-
"name": "Unresolved",
604+
"name": "Use",
605605
"subHooks": [],
606606
"value": Any<Promise>,
607607
}

0 commit comments

Comments
 (0)