13
ReactProviderType,
14
StartTransitionOptions,
15
Usable,
16
+ Thenable,
17
+ ReactDebugInfo,
18
} from 'shared/ReactTypes';
19
import type {
20
Fiber,
43
primitive: string,
44
stackError: Error,
45
value: mixed,
46
+ debugInfo: ReactDebugInfo | null,
47
};
48
49
let hookLog: Array<HookLogEntry> = [];
96
// This type check is for Flow only.
97
Dispatcher.useFormState((s: mixed, p: mixed) => s, null);
98
}
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
+ }
120
} finally {
121
readHookLog = hookLog;
122
hookLog = [];
146
return context._currentValue;
147
}
148
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
+
159
function use<T>(usable: Usable<T>): T {
160
if (usable !== null && typeof usable === 'object') {
161
// $FlowFixMe[method-unbinding]
162
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;
191
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
192
const context: ReactContext<T> = (usable: any);
193
const value = readContext(context);
194
195
hookLog.push({
138
- primitive: 'Use',
196
+ primitive: 'Context (use)',
197
stackError: new Error(),
198
value,
199
+ debugInfo: null,
200
});
201
202
return value;
212
primitive: 'Context',
213
stackError: new Error(),
214
value: context._currentValue,
215
+ debugInfo: null,
216
});
217
return context._currentValue;
218
}
228
? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
229
initialState()
230
: 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
+ });
237
return [state, (action: BasicStateAction<S>) => {}];
238
}
239
253
primitive: 'Reducer',
254
stackError: new Error(),
255
value: state,
256
+ debugInfo: null,
257
});
258
return [state, (action: A) => {}];
259
}
265
primitive: 'Ref',
266
stackError: new Error(),
267
value: ref.current,
268
+ debugInfo: null,
269
});
270
return ref;
271
}
276
primitive: 'CacheRefresh',
277
stackError: new Error(),
278
value: hook !== null ? hook.memoizedState : function refresh() {},
279
+ debugInfo: null,
280
});
281
return () => {};
282
}
290
primitive: 'LayoutEffect',
291
stackError: new Error(),
292
value: create,
293
+ debugInfo: null,
294
});
295
}
296
303
primitive: 'InsertionEffect',
304
stackError: new Error(),
305
value: create,
306
+ debugInfo: null,
307
});
308
}
309
312
inputs: Array<mixed> | void | null,
313
): void {
314
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
+ });
321
}
322
323
function useImperativeHandle<T>(
338
primitive: 'ImperativeHandle',
339
stackError: new Error(),
340
value: instance,
341
+ debugInfo: null,
342
});
343
}
344
347
primitive: 'DebugValue',
348
stackError: new Error(),
349
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
350
+ debugInfo: null,
351
});
352
}
353
357
primitive: 'Callback',
358
stackError: new Error(),
359
value: hook !== null ? hook.memoizedState[0] : callback,
360
+ debugInfo: null,
361
});
362
return callback;
363
}
368
): T {
369
const hook = nextHook();
370
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
+ });
377
return value;
378
}
379
392
primitive: 'SyncExternalStore',
393
stackError: new Error(),
394
value,
395
+ debugInfo: null,
396
});
397
return value;
398
}
410
primitive: 'Transition',
411
stackError: new Error(),
412
value: undefined,
413
+ debugInfo: null,
414
});
415
return [false, callback => {}];
416
}
421
primitive: 'DeferredValue',
422
stackError: new Error(),
423
value: hook !== null ? hook.memoizedState : value,
424
+ debugInfo: null,
425
});
426
return value;
427
}
433
primitive: 'Id',
434
stackError: new Error(),
435
value: id,
436
+ debugInfo: null,
437
});
438
return id;
439
}
482
primitive: 'Optimistic',
483
stackError: new Error(),
484
value: state,
485
+ debugInfo: null,
486
});
487
return [state, (action: A) => {}];
488
}
504
primitive: 'FormState',
505
stackError: new Error(),
506
value: state,
507
+ debugInfo: null,
508
});
509
return [state, (payload: P) => {}];
510
}
569
name: string,
570
value: mixed,
571
subHooks: Array<HooksNode>,
572
+ debugInfo: null | ReactDebugInfo,
573
hookSource?: HookSource,
574
};
575
export type HooksTree = Array<HooksNode>;
636
if (!functionName) {
637
return false;
638
}
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
+ }
648
const expectedPrimitiveName = 'use' + primitiveName;
649
if (functionName.length < expectedPrimitiveName.length) {
650
return false;
760
name: parseCustomHookName(stack[j - 1].functionName),
761
value: undefined,
762
subHooks: children,
763
+ debugInfo: null,
764
};
765
766
if (includeHooksSource) {
778
}
779
prevStack = stack;
780
}
681
- const {primitive} = hook;
781
+ const {primitive, debugInfo} = hook;
782
783
// For now, the "id" of stateful hooks is just the stateful hook index.
784
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
785
const id =
786
primitive === 'Context' ||
787
+ primitive === 'Context (use)' ||
788
primitive === 'DebugValue' ||
688
- primitive === 'Use'
789
+ primitive === 'Promise' ||
790
+ primitive === 'Unresolved'
791
? null
792
: nativeHookID++;
793
794
// For the time being, only State and Reducer hooks support runtime overrides.
795
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
796
+ const name = primitive === 'Context (use)' ? 'Context' : primitive;
797
const levelChild: HooksNode = {
798
id,
799
isStateEditable,
697
- name: primitive,
800
+ name: name,
801
value: hook.value,
802
subHooks: [],
803
+ debugInfo: debugInfo,
804
};
805
806
if (includeHooksSource) {
866
867
function handleRenderFunctionError(error: any): void {
868
// 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
+ }
874
if (
875
error instanceof Error &&
876
error.name === 'ReactDebugToolsUnsupportedHookError'