DevTools: Rely on sourcemaps to compute hook name of built-in hooks in newer versions (#28593)
Sebastian Silbermann committed
Apr 11, 2024 at 22:00 UTC
4f5c812a3c4e52d9ea5d908a27a317ac0f26590a
2 files changed
+73
-37
packages/react-debug-tools/src/ReactDebugHooks.js
+71
-35
@@ -47,6 +47,7 @@ type HookLogEntry = {
47
stackError: Error,
48
value: mixed,
49
debugInfo: ReactDebugInfo | null,
50
+ dispatcherHookName: string,
51
};
52
53
let hookLog: Array<HookLogEntry> = [];
@@ -131,6 +132,8 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
132
);
133
} catch (x) {}
134
}
135
+
136
+ Dispatcher.useId();
137
} finally {
138
readHookLog = hookLog;
139
hookLog = [];
@@ -207,6 +210,7 @@ function use<T>(usable: Usable<T>): T {
210
value: fulfilledValue,
211
debugInfo:
212
thenable._debugInfo === undefined ? null : thenable._debugInfo,
213
+ dispatcherHookName: 'Use',
214
});
215
return fulfilledValue;
216
}
@@ -224,6 +228,7 @@ function use<T>(usable: Usable<T>): T {
228
value: thenable,
229
debugInfo:
230
thenable._debugInfo === undefined ? null : thenable._debugInfo,
231
+ dispatcherHookName: 'Use',
232
});
233
throw SuspenseException;
234
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
@@ -236,6 +241,7 @@ function use<T>(usable: Usable<T>): T {
241
stackError: new Error(),
242
value,
243
debugInfo: null,
244
+ dispatcherHookName: 'Use',
245
});
246
247
return value;
@@ -254,6 +260,7 @@ function useContext<T>(context: ReactContext<T>): T {
260
stackError: new Error(),
261
value: value,
262
debugInfo: null,
263
+ dispatcherHookName: 'Context',
264
});
265
return value;
266
}
@@ -275,6 +282,7 @@ function useState<S>(
282
stackError: new Error(),
283
value: state,
284
debugInfo: null,
285
+ dispatcherHookName: 'State',
286
});
287
return [state, (action: BasicStateAction<S>) => {}];
288
}
@@ -297,6 +305,7 @@ function useReducer<S, I, A>(
305
stackError: new Error(),
306
value: state,
307
debugInfo: null,
308
+ dispatcherHookName: 'Reducer',
309
});
310
return [state, (action: A) => {}];
311
}
@@ -310,6 +319,7 @@ function useRef<T>(initialValue: T): {current: T} {
319
stackError: new Error(),
320
value: ref.current,
321
debugInfo: null,
322
+ dispatcherHookName: 'Ref',
323
});
324
return ref;
325
}
@@ -322,6 +332,7 @@ function useCacheRefresh(): () => void {
332
stackError: new Error(),
333
value: hook !== null ? hook.memoizedState : function refresh() {},
334
debugInfo: null,
335
+ dispatcherHookName: 'CacheRefresh',
336
});
337
return () => {};
338
}
@@ -337,6 +348,7 @@ function useLayoutEffect(
348
stackError: new Error(),
349
value: create,
350
debugInfo: null,
351
+ dispatcherHookName: 'LayoutEffect',
352
});
353
}
354
@@ -351,6 +363,7 @@ function useInsertionEffect(
363
stackError: new Error(),
364
value: create,
365
debugInfo: null,
366
+ dispatcherHookName: 'InsertionEffect',
367
});
368
}
369
@@ -365,6 +378,7 @@ function useEffect(
378
stackError: new Error(),
379
value: create,
380
debugInfo: null,
381
+ dispatcherHookName: 'Effect',
382
});
383
}
384
@@ -388,6 +402,7 @@ function useImperativeHandle<T>(
402
stackError: new Error(),
403
value: instance,
404
debugInfo: null,
405
+ dispatcherHookName: 'ImperativeHandle',
406
});
407
}
408
@@ -398,6 +413,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
413
stackError: new Error(),
414
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
415
debugInfo: null,
416
+ dispatcherHookName: 'DebugValue',
417
});
418
}
419
@@ -409,6 +425,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
425
stackError: new Error(),
426
value: hook !== null ? hook.memoizedState[0] : callback,
427
debugInfo: null,
428
+ dispatcherHookName: 'Callback',
429
});
430
return callback;
431
}
@@ -425,6 +442,7 @@ function useMemo<T>(
442
stackError: new Error(),
443
value,
444
debugInfo: null,
445
+ dispatcherHookName: 'Memo',
446
});
447
return value;
448
}
@@ -446,6 +464,7 @@ function useSyncExternalStore<T>(
464
stackError: new Error(),
465
value,
466
debugInfo: null,
467
+ dispatcherHookName: 'SyncExternalStore',
468
});
469
return value;
470
}
@@ -468,6 +487,7 @@ function useTransition(): [
487
stackError: new Error(),
488
value: isPending,
489
debugInfo: null,
490
+ dispatcherHookName: 'Transition',
491
});
492
return [isPending, () => {}];
493
}
@@ -481,6 +501,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
501
stackError: new Error(),
502
value: prevValue,
503
debugInfo: null,
504
+ dispatcherHookName: 'DeferredValue',
505
});
506
return prevValue;
507
}
@@ -494,6 +515,7 @@ function useId(): string {
515
stackError: new Error(),
516
value: id,
517
debugInfo: null,
518
+ dispatcherHookName: 'Id',
519
});
520
return id;
521
}
@@ -544,6 +566,7 @@ function useOptimistic<S, A>(
566
stackError: new Error(),
567
value: state,
568
debugInfo: null,
569
+ dispatcherHookName: 'Optimistic',
570
});
571
return [state, (action: A) => {}];
572
}
@@ -603,6 +626,7 @@ function useFormState<S, P>(
626
stackError: stackError,
627
value: value,
628
debugInfo: debugInfo,
629
+ dispatcherHookName: 'FormState',
630
});
631
632
if (error !== null) {
@@ -672,6 +696,7 @@ function useActionState<S, P>(
696
stackError: stackError,
697
value: value,
698
debugInfo: debugInfo,
699
+ dispatcherHookName: 'ActionState',
700
});
701
702
if (error !== null) {
@@ -759,8 +784,7 @@ export type HooksTree = Array<HooksNode>;
784
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
785
// in a wrapper constructor like a polyfill. That'll add an extra frame.
786
// 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.
788
//
789
// We also can't assume that the last frame of the root call is the same
790
// 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) {
834
return -1;
835
}
836
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;
839
}
840
841
function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
@@ -841,17 +846,18 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
846
}
847
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
848
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.
852
if (
853
i < hookStack.length - 1 &&
848
- isReactWrapper(hookStack[i].functionName, hook.primitive)
854
+ isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
855
) {
856
i++;
857
}
858
if (
859
i < hookStack.length - 1 &&
854
- isReactWrapper(hookStack[i].functionName, hook.primitive)
860
+ isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
861
) {
862
i++;
863
}
@@ -872,21 +878,41 @@ function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
878
primitiveIndex === -1 ||
879
rootIndex - primitiveIndex < 2
880
) {
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
+ }
887
}
878
- return hookStack.slice(primitiveIndex, rootIndex - 1);
888
+ return [
889
+ hookStack[primitiveIndex - 1],
890
+ hookStack.slice(primitiveIndex, rootIndex - 1),
891
+ ];
892
}
893
881
-function parseCustomHookName(functionName: void | string): string {
894
+function parseHookName(functionName: void | string): string {
895
if (!functionName) {
896
return '';
897
}
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('.');
907
if (startIndex === -1) {
908
startIndex = 0;
909
+ } else {
910
+ startIndex += 1;
911
}
912
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
913
+ if (functionName.length - startIndex === 3) {
914
+ return 'Use';
915
+ }
916
startIndex += 3;
917
}
918
return functionName.slice(startIndex);
@@ -903,7 +929,17 @@ function buildTree(
929
const stackOfChildren = [];
930
for (let i = 0; i < readHookLog.length; i++) {
931
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
+ }
943
if (stack !== null) {
944
// Note: The indices 0 <= n < length-1 will contain the names.
945
// The indices 1 <= n < length will contain the source locations.
@@ -934,7 +970,7 @@ function buildTree(
970
const levelChild: HooksNode = {
971
id: null,
972
isStateEditable: false,
937
- name: parseCustomHookName(stack[j - 1].functionName),
973
+ name: parseHookName(stack[j - 1].functionName),
974
value: undefined,
975
subHooks: children,
976
debugInfo: null,
@@ -952,7 +988,7 @@ function buildTree(
988
}
989
prevStack = stack;
990
}
955
- const {displayName, primitive, debugInfo} = hook;
991
+ const {primitive, debugInfo} = hook;
992
993
// For now, the "id" of stateful hooks is just the stateful hook index.
994
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
+2
-2
@@ -518,7 +518,7 @@ describe('ReactHooksInspection', () => {
518
},
519
"id": null,
520
"isStateEditable": false,
521
- "name": "Promise",
521
+ "name": "Use",
522
"subHooks": [],
523
"value": "world",
524
},
@@ -568,7 +568,7 @@ describe('ReactHooksInspection', () => {
568
},
569
"id": null,
570
"isStateEditable": false,
571
- "name": "Unresolved",
571
+ "name": "Use",
572
"subHooks": [],
573
"value": Any<Promise>,
574
}