Skip to content

Commit 4f5c812

Browse files
authored
DevTools: Rely on sourcemaps to compute hook name of built-in hooks in newer versions (#28593)
1 parent 4354159 commit 4f5c812

File tree

2 files changed

+73
-37
lines changed

2 files changed

+73
-37
lines changed

Diff for: packages/react-debug-tools/src/ReactDebugHooks.js

+71-35
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+
dispatcherHookName: string,
5051
};
5152

5253
let hookLog: Array<HookLogEntry> = [];
@@ -131,6 +132,8 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
131132
);
132133
} catch (x) {}
133134
}
135+
136+
Dispatcher.useId();
134137
} finally {
135138
readHookLog = hookLog;
136139
hookLog = [];
@@ -207,6 +210,7 @@ function use<T>(usable: Usable<T>): T {
207210
value: fulfilledValue,
208211
debugInfo:
209212
thenable._debugInfo === undefined ? null : thenable._debugInfo,
213+
dispatcherHookName: 'Use',
210214
});
211215
return fulfilledValue;
212216
}
@@ -224,6 +228,7 @@ function use<T>(usable: Usable<T>): T {
224228
value: thenable,
225229
debugInfo:
226230
thenable._debugInfo === undefined ? null : thenable._debugInfo,
231+
dispatcherHookName: 'Use',
227232
});
228233
throw SuspenseException;
229234
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
@@ -236,6 +241,7 @@ function use<T>(usable: Usable<T>): T {
236241
stackError: new Error(),
237242
value,
238243
debugInfo: null,
244+
dispatcherHookName: 'Use',
239245
});
240246

241247
return value;
@@ -254,6 +260,7 @@ function useContext<T>(context: ReactContext<T>): T {
254260
stackError: new Error(),
255261
value: value,
256262
debugInfo: null,
263+
dispatcherHookName: 'Context',
257264
});
258265
return value;
259266
}
@@ -275,6 +282,7 @@ function useState<S>(
275282
stackError: new Error(),
276283
value: state,
277284
debugInfo: null,
285+
dispatcherHookName: 'State',
278286
});
279287
return [state, (action: BasicStateAction<S>) => {}];
280288
}
@@ -297,6 +305,7 @@ function useReducer<S, I, A>(
297305
stackError: new Error(),
298306
value: state,
299307
debugInfo: null,
308+
dispatcherHookName: 'Reducer',
300309
});
301310
return [state, (action: A) => {}];
302311
}
@@ -310,6 +319,7 @@ function useRef<T>(initialValue: T): {current: T} {
310319
stackError: new Error(),
311320
value: ref.current,
312321
debugInfo: null,
322+
dispatcherHookName: 'Ref',
313323
});
314324
return ref;
315325
}
@@ -322,6 +332,7 @@ function useCacheRefresh(): () => void {
322332
stackError: new Error(),
323333
value: hook !== null ? hook.memoizedState : function refresh() {},
324334
debugInfo: null,
335+
dispatcherHookName: 'CacheRefresh',
325336
});
326337
return () => {};
327338
}
@@ -337,6 +348,7 @@ function useLayoutEffect(
337348
stackError: new Error(),
338349
value: create,
339350
debugInfo: null,
351+
dispatcherHookName: 'LayoutEffect',
340352
});
341353
}
342354

@@ -351,6 +363,7 @@ function useInsertionEffect(
351363
stackError: new Error(),
352364
value: create,
353365
debugInfo: null,
366+
dispatcherHookName: 'InsertionEffect',
354367
});
355368
}
356369

@@ -365,6 +378,7 @@ function useEffect(
365378
stackError: new Error(),
366379
value: create,
367380
debugInfo: null,
381+
dispatcherHookName: 'Effect',
368382
});
369383
}
370384

@@ -388,6 +402,7 @@ function useImperativeHandle<T>(
388402
stackError: new Error(),
389403
value: instance,
390404
debugInfo: null,
405+
dispatcherHookName: 'ImperativeHandle',
391406
});
392407
}
393408

@@ -398,6 +413,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
398413
stackError: new Error(),
399414
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
400415
debugInfo: null,
416+
dispatcherHookName: 'DebugValue',
401417
});
402418
}
403419

@@ -409,6 +425,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
409425
stackError: new Error(),
410426
value: hook !== null ? hook.memoizedState[0] : callback,
411427
debugInfo: null,
428+
dispatcherHookName: 'Callback',
412429
});
413430
return callback;
414431
}
@@ -425,6 +442,7 @@ function useMemo<T>(
425442
stackError: new Error(),
426443
value,
427444
debugInfo: null,
445+
dispatcherHookName: 'Memo',
428446
});
429447
return value;
430448
}
@@ -446,6 +464,7 @@ function useSyncExternalStore<T>(
446464
stackError: new Error(),
447465
value,
448466
debugInfo: null,
467+
dispatcherHookName: 'SyncExternalStore',
449468
});
450469
return value;
451470
}
@@ -468,6 +487,7 @@ function useTransition(): [
468487
stackError: new Error(),
469488
value: isPending,
470489
debugInfo: null,
490+
dispatcherHookName: 'Transition',
471491
});
472492
return [isPending, () => {}];
473493
}
@@ -481,6 +501,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
481501
stackError: new Error(),
482502
value: prevValue,
483503
debugInfo: null,
504+
dispatcherHookName: 'DeferredValue',
484505
});
485506
return prevValue;
486507
}
@@ -494,6 +515,7 @@ function useId(): string {
494515
stackError: new Error(),
495516
value: id,
496517
debugInfo: null,
518+
dispatcherHookName: 'Id',
497519
});
498520
return id;
499521
}
@@ -544,6 +566,7 @@ function useOptimistic<S, A>(
544566
stackError: new Error(),
545567
value: state,
546568
debugInfo: null,
569+
dispatcherHookName: 'Optimistic',
547570
});
548571
return [state, (action: A) => {}];
549572
}
@@ -603,6 +626,7 @@ function useFormState<S, P>(
603626
stackError: stackError,
604627
value: value,
605628
debugInfo: debugInfo,
629+
dispatcherHookName: 'FormState',
606630
});
607631

608632
if (error !== null) {
@@ -672,6 +696,7 @@ function useActionState<S, P>(
672696
stackError: stackError,
673697
value: value,
674698
debugInfo: debugInfo,
699+
dispatcherHookName: 'ActionState',
675700
});
676701

677702
if (error !== null) {
@@ -759,8 +784,7 @@ export type HooksTree = Array<HooksNode>;
759784
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
760785
// in a wrapper constructor like a polyfill. That'll add an extra frame.
761786
// Similar things can happen with the call to the dispatcher. The top frame
762-
// may not be the primitive. Likewise the primitive can have fewer stack frames
763-
// such as when a call to useState got inlined to use dispatcher.useState.
787+
// may not be the primitive.
764788
//
765789
// We also can't assume that the last frame of the root call is the same
766790
// frame as the last frame of the hook call because long stack traces can be
@@ -810,27 +834,8 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
810834
return -1;
811835
}
812836

813-
function isReactWrapper(functionName: any, primitiveName: string) {
814-
if (!functionName) {
815-
return false;
816-
}
817-
switch (primitiveName) {
818-
case 'Context':
819-
case 'Context (use)':
820-
case 'Promise':
821-
case 'Unresolved':
822-
if (functionName.endsWith('use')) {
823-
return true;
824-
}
825-
}
826-
const expectedPrimitiveName = 'use' + primitiveName;
827-
if (functionName.length < expectedPrimitiveName.length) {
828-
return false;
829-
}
830-
return (
831-
functionName.lastIndexOf(expectedPrimitiveName) ===
832-
functionName.length - expectedPrimitiveName.length
833-
);
837+
function isReactWrapper(functionName: any, wrapperName: string) {
838+
return parseHookName(functionName) === wrapperName;
834839
}
835840

836841
function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
@@ -841,17 +846,18 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
841846
}
842847
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
843848
if (primitiveStack[i].source !== hookStack[i].source) {
844-
// If the next two frames are functions called `useX` then we assume that they're part of the
845-
// wrappers that the React packager or other packages adds around the dispatcher.
849+
// If the next frame is a method from the dispatcher, we
850+
// assume that the next frame after that is the actual public API call.
851+
// This prohibits nesting dispatcher calls in hooks.
846852
if (
847853
i < hookStack.length - 1 &&
848-
isReactWrapper(hookStack[i].functionName, hook.primitive)
854+
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
849855
) {
850856
i++;
851857
}
852858
if (
853859
i < hookStack.length - 1 &&
854-
isReactWrapper(hookStack[i].functionName, hook.primitive)
860+
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
855861
) {
856862
i++;
857863
}
@@ -872,21 +878,41 @@ function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
872878
primitiveIndex === -1 ||
873879
rootIndex - primitiveIndex < 2
874880
) {
875-
// Something went wrong. Give up.
876-
return null;
881+
if (primitiveIndex === -1) {
882+
// Something went wrong. Give up.
883+
return [null, null];
884+
} else {
885+
return [hookStack[primitiveIndex - 1], null];
886+
}
877887
}
878-
return hookStack.slice(primitiveIndex, rootIndex - 1);
888+
return [
889+
hookStack[primitiveIndex - 1],
890+
hookStack.slice(primitiveIndex, rootIndex - 1),
891+
];
879892
}
880893

881-
function parseCustomHookName(functionName: void | string): string {
894+
function parseHookName(functionName: void | string): string {
882895
if (!functionName) {
883896
return '';
884897
}
885-
let startIndex = functionName.lastIndexOf('.');
898+
let startIndex = functionName.lastIndexOf('[as ');
899+
900+
if (startIndex !== -1) {
901+
// Workaround for sourcemaps in Jest and Chrome.
902+
// In `node --enable-source-maps`, we don't see "Object.useHostTransitionStatus [as useFormStatus]" but "Object.useFormStatus"
903+
// "Object.useHostTransitionStatus [as useFormStatus]" -> "useFormStatus"
904+
return parseHookName(functionName.slice(startIndex + '[as '.length, -1));
905+
}
906+
startIndex = functionName.lastIndexOf('.');
886907
if (startIndex === -1) {
887908
startIndex = 0;
909+
} else {
910+
startIndex += 1;
888911
}
889912
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
913+
if (functionName.length - startIndex === 3) {
914+
return 'Use';
915+
}
890916
startIndex += 3;
891917
}
892918
return functionName.slice(startIndex);
@@ -903,7 +929,17 @@ function buildTree(
903929
const stackOfChildren = [];
904930
for (let i = 0; i < readHookLog.length; i++) {
905931
const hook = readHookLog[i];
906-
const stack = parseTrimmedStack(rootStack, hook);
932+
const parseResult = parseTrimmedStack(rootStack, hook);
933+
const primitiveFrame = parseResult[0];
934+
const stack = parseResult[1];
935+
let displayName = hook.displayName;
936+
if (displayName === null && primitiveFrame !== null) {
937+
displayName =
938+
parseHookName(primitiveFrame.functionName) ||
939+
// Older versions of React do not have sourcemaps.
940+
// In those versions there was always a 1:1 mapping between wrapper and dispatcher method.
941+
parseHookName(hook.dispatcherHookName);
942+
}
907943
if (stack !== null) {
908944
// Note: The indices 0 <= n < length-1 will contain the names.
909945
// The indices 1 <= n < length will contain the source locations.
@@ -934,7 +970,7 @@ function buildTree(
934970
const levelChild: HooksNode = {
935971
id: null,
936972
isStateEditable: false,
937-
name: parseCustomHookName(stack[j - 1].functionName),
973+
name: parseHookName(stack[j - 1].functionName),
938974
value: undefined,
939975
subHooks: children,
940976
debugInfo: null,
@@ -952,7 +988,7 @@ function buildTree(
952988
}
953989
prevStack = stack;
954990
}
955-
const {displayName, primitive, debugInfo} = hook;
991+
const {primitive, debugInfo} = hook;
956992

957993
// For now, the "id" of stateful hooks is just the stateful hook index.
958994
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ describe('ReactHooksInspection', () => {
518518
},
519519
"id": null,
520520
"isStateEditable": false,
521-
"name": "Promise",
521+
"name": "Use",
522522
"subHooks": [],
523523
"value": "world",
524524
},
@@ -568,7 +568,7 @@ describe('ReactHooksInspection', () => {
568568
},
569569
"id": null,
570570
"isStateEditable": false,
571-
"name": "Unresolved",
571+
"name": "Use",
572572
"subHooks": [],
573573
"value": Any<Promise>,
574574
}

0 commit comments

Comments
 (0)