@@ -13,6 +13,8 @@ import type {
13
13
ReactProviderType ,
14
14
StartTransitionOptions ,
15
15
Usable ,
16
+ Thenable ,
17
+ ReactDebugInfo ,
16
18
} from 'shared/ReactTypes' ;
17
19
import type {
18
20
Fiber ,
@@ -41,6 +43,7 @@ type HookLogEntry = {
41
43
primitive : string ,
42
44
stackError : Error ,
43
45
value : mixed ,
46
+ debugInfo : ReactDebugInfo | null ,
44
47
} ;
45
48
46
49
let hookLog : Array < HookLogEntry > = [ ] ;
@@ -93,6 +96,27 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
93
96
// This type check is for Flow only.
94
97
Dispatcher . useFormState ( ( s : mixed , p : mixed ) => s , null ) ;
95
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
+ }
96
120
} finally {
97
121
readHookLog = hookLog ;
98
122
hookLog = [ ] ;
@@ -122,22 +146,57 @@ function readContext<T>(context: ReactContext<T>): T {
122
146
return context . _currentValue ;
123
147
}
124
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
+
125
159
function use< T > (usable: Usable< T > ): T {
126
160
if ( usable !== null && typeof usable === 'object' ) {
127
161
// $FlowFixMe[method-unbinding]
128
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;
133
191
} else if ( usable . $$typeof === REACT_CONTEXT_TYPE ) {
134
192
const context : ReactContext < T > = ( usable : any ) ;
135
193
const value = readContext ( context ) ;
136
194
137
195
hookLog . push ( {
138
- primitive : 'Use ' ,
196
+ primitive : 'Context (use) ' ,
139
197
stackError : new Error ( ) ,
140
198
value,
199
+ debugInfo : null ,
141
200
} ) ;
142
201
143
202
return value ;
@@ -153,6 +212,7 @@ function useContext<T>(context: ReactContext<T>): T {
153
212
primitive : 'Context' ,
154
213
stackError : new Error ( ) ,
155
214
value : context . _currentValue ,
215
+ debugInfo : null ,
156
216
} ) ;
157
217
return context . _currentValue ;
158
218
}
@@ -168,7 +228,12 @@ function useState<S>(
168
228
? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
169
229
initialState ( )
170
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
+ } ) ;
172
237
return [ state , ( action : BasicStateAction < S > ) => { } ] ;
173
238
}
174
239
@@ -188,6 +253,7 @@ function useReducer<S, I, A>(
188
253
primitive : 'Reducer' ,
189
254
stackError : new Error ( ) ,
190
255
value : state ,
256
+ debugInfo : null ,
191
257
} ) ;
192
258
return [ state , ( action : A ) => { } ] ;
193
259
}
@@ -199,6 +265,7 @@ function useRef<T>(initialValue: T): {current: T} {
199
265
primitive : 'Ref' ,
200
266
stackError : new Error ( ) ,
201
267
value : ref . current ,
268
+ debugInfo : null ,
202
269
} ) ;
203
270
return ref ;
204
271
}
@@ -209,6 +276,7 @@ function useCacheRefresh(): () => void {
209
276
primitive : 'CacheRefresh' ,
210
277
stackError : new Error ( ) ,
211
278
value : hook !== null ? hook . memoizedState : function refresh ( ) { } ,
279
+ debugInfo : null ,
212
280
} ) ;
213
281
return ( ) = > { } ;
214
282
}
@@ -222,6 +290,7 @@ function useLayoutEffect(
222
290
primitive : 'LayoutEffect' ,
223
291
stackError : new Error ( ) ,
224
292
value : create ,
293
+ debugInfo : null ,
225
294
} ) ;
226
295
}
227
296
@@ -234,6 +303,7 @@ function useInsertionEffect(
234
303
primitive : 'InsertionEffect' ,
235
304
stackError : new Error ( ) ,
236
305
value : create ,
306
+ debugInfo : null ,
237
307
} ) ;
238
308
}
239
309
@@ -242,7 +312,12 @@ function useEffect(
242
312
inputs : Array < mixed > | void | null,
243
313
): void {
244
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
+ } ) ;
246
321
}
247
322
248
323
function useImperativeHandle< T > (
@@ -263,6 +338,7 @@ function useImperativeHandle<T>(
263
338
primitive : 'ImperativeHandle' ,
264
339
stackError : new Error ( ) ,
265
340
value : instance ,
341
+ debugInfo : null ,
266
342
} );
267
343
}
268
344
@@ -271,6 +347,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
271
347
primitive : 'DebugValue' ,
272
348
stackError : new Error ( ) ,
273
349
value : typeof formatterFn === 'function' ? formatterFn ( value ) : value ,
350
+ debugInfo : null ,
274
351
} ) ;
275
352
}
276
353
@@ -280,6 +357,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
280
357
primitive : 'Callback' ,
281
358
stackError : new Error ( ) ,
282
359
value : hook !== null ? hook . memoizedState [ 0 ] : callback ,
360
+ debugInfo : null ,
283
361
} ) ;
284
362
return callback ;
285
363
}
@@ -290,7 +368,12 @@ function useMemo<T>(
290
368
): T {
291
369
const hook = nextHook ( ) ;
292
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
+ } ) ;
294
377
return value ;
295
378
}
296
379
@@ -309,6 +392,7 @@ function useSyncExternalStore<T>(
309
392
primitive : 'SyncExternalStore' ,
310
393
stackError : new Error ( ) ,
311
394
value,
395
+ debugInfo : null ,
312
396
} ) ;
313
397
return value ;
314
398
}
@@ -326,6 +410,7 @@ function useTransition(): [
326
410
primitive : 'Transition' ,
327
411
stackError : new Error ( ) ,
328
412
value : undefined ,
413
+ debugInfo : null ,
329
414
} ) ;
330
415
return [ false , callback => { } ] ;
331
416
}
@@ -336,6 +421,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
336
421
primitive : 'DeferredValue' ,
337
422
stackError : new Error ( ) ,
338
423
value : hook !== null ? hook . memoizedState : value ,
424
+ debugInfo : null ,
339
425
} ) ;
340
426
return value ;
341
427
}
@@ -347,6 +433,7 @@ function useId(): string {
347
433
primitive : 'Id' ,
348
434
stackError : new Error ( ) ,
349
435
value : id ,
436
+ debugInfo : null ,
350
437
} ) ;
351
438
return id ;
352
439
}
@@ -395,6 +482,7 @@ function useOptimistic<S, A>(
395
482
primitive : 'Optimistic' ,
396
483
stackError : new Error ( ) ,
397
484
value : state ,
485
+ debugInfo : null ,
398
486
} ) ;
399
487
return [ state , ( action : A ) => { } ] ;
400
488
}
@@ -416,6 +504,7 @@ function useFormState<S, P>(
416
504
primitive : 'FormState' ,
417
505
stackError : new Error ( ) ,
418
506
value : state ,
507
+ debugInfo : null ,
419
508
} );
420
509
return [state, (payload: P) => { } ];
421
510
}
@@ -480,6 +569,7 @@ export type HooksNode = {
480
569
name : string ,
481
570
value : mixed ,
482
571
subHooks : Array < HooksNode > ,
572
+ debugInfo : null | ReactDebugInfo ,
483
573
hookSource ? : HookSource ,
484
574
} ;
485
575
export type HooksTree = Array< HooksNode > ;
@@ -546,6 +636,15 @@ function isReactWrapper(functionName: any, primitiveName: string) {
546
636
if ( ! functionName ) {
547
637
return false ;
548
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
+ }
549
648
const expectedPrimitiveName = 'use' + primitiveName;
550
649
if (functionName.length < expectedPrimitiveName . length ) {
551
650
return false ;
@@ -661,6 +760,7 @@ function buildTree(
661
760
name : parseCustomHookName ( stack [ j - 1 ] . functionName ) ,
662
761
value : undefined ,
663
762
subHooks : children ,
763
+ debugInfo : null ,
664
764
} ;
665
765
666
766
if ( includeHooksSource ) {
@@ -678,25 +778,29 @@ function buildTree(
678
778
}
679
779
prevStack = stack ;
680
780
}
681
- const { primitive } = hook;
781
+ const { primitive , debugInfo } = hook ;
682
782
683
783
// For now, the "id" of stateful hooks is just the stateful hook index.
684
784
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
685
785
const id =
686
786
primitive === 'Context' ||
787
+ primitive === 'Context (use)' ||
687
788
primitive === 'DebugValue' ||
688
- primitive === 'Use'
789
+ primitive === 'Promise' ||
790
+ primitive === 'Unresolved'
689
791
? null
690
792
: nativeHookID ++ ;
691
793
692
794
// For the time being, only State and Reducer hooks support runtime overrides.
693
795
const isStateEditable = primitive === 'Reducer' || primitive === 'State' ;
796
+ const name = primitive === 'Context (use)' ? 'Context' : primitive ;
694
797
const levelChild : HooksNode = {
695
798
id,
696
799
isStateEditable,
697
- name : primitive ,
800
+ name : name ,
698
801
value : hook . value ,
699
802
subHooks : [ ] ,
803
+ debugInfo : debugInfo ,
700
804
} ;
701
805
702
806
if ( includeHooksSource ) {
@@ -762,6 +866,11 @@ function processDebugValues(
762
866
763
867
function handleRenderFunctionError ( error : any ) : void {
764
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
+ }
765
874
if (
766
875
error instanceof Error &&
767
876
error . name === 'ReactDebugToolsUnsupportedHookError'
0 commit comments