@@ -13,6 +13,8 @@ import type {
1313 ReactProviderType ,
1414 StartTransitionOptions ,
1515 Usable ,
16+ Thenable ,
17+ ReactDebugInfo ,
1618} from 'shared/ReactTypes' ;
1719import type {
1820 Fiber ,
@@ -41,6 +43,7 @@ type HookLogEntry = {
4143 primitive : string ,
4244 stackError : Error ,
4345 value : mixed ,
46+ debugInfo : ReactDebugInfo | null ,
4447} ;
4548
4649let 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+
125159function 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
248323function 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} ;
485575export 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
763867function 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