Skip to content

Commit f65435a

Browse files
committed
Introspect use() Hooks with Debug Tools
1 parent 83d3c62 commit f65435a

File tree

4 files changed

+347
-16
lines changed

4 files changed

+347
-16
lines changed

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

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type {
1313
ReactProviderType,
1414
StartTransitionOptions,
1515
Usable,
16+
Thenable,
17+
ReactDebugInfo,
1618
} from 'shared/ReactTypes';
1719
import type {
1820
Fiber,
@@ -41,6 +43,7 @@ type HookLogEntry = {
4143
primitive: string,
4244
stackError: Error,
4345
value: mixed,
46+
debugInfo: ReactDebugInfo | null,
4447
};
4548

4649
let hookLog: Array<HookLogEntry> = [];
@@ -93,6 +96,27 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
9396
// This type check is for Flow only.
9497
Dispatcher.useFormState((s: mixed, p: mixed) => s, null);
9598
}
99+
if (typeof Dispatcher.use === 'function') {
100+
// This type check is for Flow only.
101+
Dispatcher.use(
102+
({
103+
$$typeof: REACT_CONTEXT_TYPE,
104+
_currentValue: null,
105+
}: any),
106+
);
107+
Dispatcher.use({
108+
then() {},
109+
status: 'fulfilled',
110+
value: null,
111+
});
112+
try {
113+
Dispatcher.use(
114+
({
115+
then() {},
116+
}: any),
117+
);
118+
} catch (x) {}
119+
}
96120
} finally {
97121
readHookLog = hookLog;
98122
hookLog = [];
@@ -122,22 +146,57 @@ function readContext<T>(context: ReactContext<T>): T {
122146
return context._currentValue;
123147
}
124148

149+
const SuspenseException: mixed = new Error(
150+
"Suspense Exception: This is not a real error! It's an implementation " +
151+
'detail of `use` to interrupt the current render. You must either ' +
152+
'rethrow it immediately, or move the `use` call outside of the ' +
153+
'`try/catch` block. Capturing without rethrowing will lead to ' +
154+
'unexpected behavior.\n\n' +
155+
'To handle async errors, wrap your component in an error boundary, or ' +
156+
"call the promise's `.catch` method and pass the result to `use`",
157+
);
158+
125159
function use<T>(usable: Usable<T>): T {
126160
if (usable !== null && typeof usable === 'object') {
127161
// $FlowFixMe[method-unbinding]
128162
if (typeof usable.then === 'function') {
129-
// TODO: What should this do if it receives an unresolved promise?
130-
throw new Error(
131-
'Support for `use(Promise)` not yet implemented in react-debug-tools.',
132-
);
163+
const thenable: Thenable<any> = (usable: any);
164+
switch (thenable.status) {
165+
case 'fulfilled': {
166+
const fulfilledValue: T = thenable.value;
167+
hookLog.push({
168+
primitive: 'Promise',
169+
stackError: new Error(),
170+
value: fulfilledValue,
171+
debugInfo:
172+
thenable._debugInfo === undefined ? null : thenable._debugInfo,
173+
});
174+
return fulfilledValue;
175+
}
176+
case 'rejected': {
177+
const rejectedError = thenable.reason;
178+
throw rejectedError;
179+
}
180+
}
181+
// If this was an uncached Promise we have to abandon this attempt
182+
// but we can still emit anything up until this point.
183+
hookLog.push({
184+
primitive: 'Unresolved',
185+
stackError: new Error(),
186+
value: thenable,
187+
debugInfo:
188+
thenable._debugInfo === undefined ? null : thenable._debugInfo,
189+
});
190+
throw SuspenseException;
133191
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
134192
const context: ReactContext<T> = (usable: any);
135193
const value = readContext(context);
136194

137195
hookLog.push({
138-
primitive: 'Use',
196+
primitive: 'Context (use)',
139197
stackError: new Error(),
140198
value,
199+
debugInfo: null,
141200
});
142201

143202
return value;
@@ -153,6 +212,7 @@ function useContext<T>(context: ReactContext<T>): T {
153212
primitive: 'Context',
154213
stackError: new Error(),
155214
value: context._currentValue,
215+
debugInfo: null,
156216
});
157217
return context._currentValue;
158218
}
@@ -168,7 +228,12 @@ function useState<S>(
168228
? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
169229
initialState()
170230
: initialState;
171-
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
231+
hookLog.push({
232+
primitive: 'State',
233+
stackError: new Error(),
234+
value: state,
235+
debugInfo: null,
236+
});
172237
return [state, (action: BasicStateAction<S>) => {}];
173238
}
174239

@@ -188,6 +253,7 @@ function useReducer<S, I, A>(
188253
primitive: 'Reducer',
189254
stackError: new Error(),
190255
value: state,
256+
debugInfo: null,
191257
});
192258
return [state, (action: A) => {}];
193259
}
@@ -199,6 +265,7 @@ function useRef<T>(initialValue: T): {current: T} {
199265
primitive: 'Ref',
200266
stackError: new Error(),
201267
value: ref.current,
268+
debugInfo: null,
202269
});
203270
return ref;
204271
}
@@ -209,6 +276,7 @@ function useCacheRefresh(): () => void {
209276
primitive: 'CacheRefresh',
210277
stackError: new Error(),
211278
value: hook !== null ? hook.memoizedState : function refresh() {},
279+
debugInfo: null,
212280
});
213281
return () => {};
214282
}
@@ -222,6 +290,7 @@ function useLayoutEffect(
222290
primitive: 'LayoutEffect',
223291
stackError: new Error(),
224292
value: create,
293+
debugInfo: null,
225294
});
226295
}
227296

@@ -234,6 +303,7 @@ function useInsertionEffect(
234303
primitive: 'InsertionEffect',
235304
stackError: new Error(),
236305
value: create,
306+
debugInfo: null,
237307
});
238308
}
239309

@@ -242,7 +312,12 @@ function useEffect(
242312
inputs: Array<mixed> | void | null,
243313
): void {
244314
nextHook();
245-
hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
315+
hookLog.push({
316+
primitive: 'Effect',
317+
stackError: new Error(),
318+
value: create,
319+
debugInfo: null,
320+
});
246321
}
247322

248323
function useImperativeHandle<T>(
@@ -263,6 +338,7 @@ function useImperativeHandle<T>(
263338
primitive: 'ImperativeHandle',
264339
stackError: new Error(),
265340
value: instance,
341+
debugInfo: null,
266342
});
267343
}
268344

@@ -271,6 +347,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
271347
primitive: 'DebugValue',
272348
stackError: new Error(),
273349
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
350+
debugInfo: null,
274351
});
275352
}
276353

@@ -280,6 +357,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
280357
primitive: 'Callback',
281358
stackError: new Error(),
282359
value: hook !== null ? hook.memoizedState[0] : callback,
360+
debugInfo: null,
283361
});
284362
return callback;
285363
}
@@ -290,7 +368,12 @@ function useMemo<T>(
290368
): T {
291369
const hook = nextHook();
292370
const value = hook !== null ? hook.memoizedState[0] : nextCreate();
293-
hookLog.push({primitive: 'Memo', stackError: new Error(), value});
371+
hookLog.push({
372+
primitive: 'Memo',
373+
stackError: new Error(),
374+
value,
375+
debugInfo: null,
376+
});
294377
return value;
295378
}
296379

@@ -309,6 +392,7 @@ function useSyncExternalStore<T>(
309392
primitive: 'SyncExternalStore',
310393
stackError: new Error(),
311394
value,
395+
debugInfo: null,
312396
});
313397
return value;
314398
}
@@ -326,6 +410,7 @@ function useTransition(): [
326410
primitive: 'Transition',
327411
stackError: new Error(),
328412
value: undefined,
413+
debugInfo: null,
329414
});
330415
return [false, callback => {}];
331416
}
@@ -336,6 +421,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
336421
primitive: 'DeferredValue',
337422
stackError: new Error(),
338423
value: hook !== null ? hook.memoizedState : value,
424+
debugInfo: null,
339425
});
340426
return value;
341427
}
@@ -347,6 +433,7 @@ function useId(): string {
347433
primitive: 'Id',
348434
stackError: new Error(),
349435
value: id,
436+
debugInfo: null,
350437
});
351438
return id;
352439
}
@@ -395,6 +482,7 @@ function useOptimistic<S, A>(
395482
primitive: 'Optimistic',
396483
stackError: new Error(),
397484
value: state,
485+
debugInfo: null,
398486
});
399487
return [state, (action: A) => {}];
400488
}
@@ -416,6 +504,7 @@ function useFormState<S, P>(
416504
primitive: 'FormState',
417505
stackError: new Error(),
418506
value: state,
507+
debugInfo: null,
419508
});
420509
return [state, (payload: P) => {}];
421510
}
@@ -480,6 +569,7 @@ export type HooksNode = {
480569
name: string,
481570
value: mixed,
482571
subHooks: Array<HooksNode>,
572+
debugInfo: null | ReactDebugInfo,
483573
hookSource?: HookSource,
484574
};
485575
export type HooksTree = Array<HooksNode>;
@@ -546,6 +636,15 @@ function isReactWrapper(functionName: any, primitiveName: string) {
546636
if (!functionName) {
547637
return false;
548638
}
639+
switch (primitiveName) {
640+
case 'Context':
641+
case 'Context (use)':
642+
case 'Promise':
643+
case 'Unresolved':
644+
if (functionName.endsWith('use')) {
645+
return true;
646+
}
647+
}
549648
const expectedPrimitiveName = 'use' + primitiveName;
550649
if (functionName.length < expectedPrimitiveName.length) {
551650
return false;
@@ -661,6 +760,7 @@ function buildTree(
661760
name: parseCustomHookName(stack[j - 1].functionName),
662761
value: undefined,
663762
subHooks: children,
763+
debugInfo: null,
664764
};
665765

666766
if (includeHooksSource) {
@@ -678,25 +778,29 @@ function buildTree(
678778
}
679779
prevStack = stack;
680780
}
681-
const {primitive} = hook;
781+
const {primitive, debugInfo} = hook;
682782

683783
// For now, the "id" of stateful hooks is just the stateful hook index.
684784
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
685785
const id =
686786
primitive === 'Context' ||
787+
primitive === 'Context (use)' ||
687788
primitive === 'DebugValue' ||
688-
primitive === 'Use'
789+
primitive === 'Promise' ||
790+
primitive === 'Unresolved'
689791
? null
690792
: nativeHookID++;
691793

692794
// For the time being, only State and Reducer hooks support runtime overrides.
693795
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
796+
const name = primitive === 'Context (use)' ? 'Context' : primitive;
694797
const levelChild: HooksNode = {
695798
id,
696799
isStateEditable,
697-
name: primitive,
800+
name: name,
698801
value: hook.value,
699802
subHooks: [],
803+
debugInfo: debugInfo,
700804
};
701805

702806
if (includeHooksSource) {
@@ -762,6 +866,11 @@ function processDebugValues(
762866

763867
function handleRenderFunctionError(error: any): void {
764868
// original error might be any type.
869+
if (error === SuspenseException) {
870+
// An uncached Promise was used. We can't synchronously resolve the rest of
871+
// the Hooks but we can at least show what ever we got so far.
872+
return;
873+
}
765874
if (
766875
error instanceof Error &&
767876
error.name === 'ReactDebugToolsUnsupportedHookError'

0 commit comments

Comments
 (0)