@@ -19,7 +19,24 @@ import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
1919import ReactSharedInternals from 'shared/ReactSharedInternals' ;
2020const { ReactCurrentActQueue} = ReactSharedInternals ;
2121
22- export opaque type ThenableState = Array < Thenable < any >> ;
22+ opaque type ThenableStateDev = {
23+ didWarnAboutUncachedPromise : boolean ,
24+ thenables : Array < Thenable < any >> ,
25+ } ;
26+
27+ opaque type ThenableStateProd = Array < Thenable < any >> ;
28+
29+ export opaque type ThenableState = ThenableStateDev | ThenableStateProd ;
30+
31+ function getThenablesFromState ( state : ThenableState ) : Array < Thenable < any >> {
32+ if ( __DEV__ ) {
33+ const devState : ThenableStateDev = ( state : any ) ;
34+ return devState . thenables ;
35+ } else {
36+ const prodState = ( state : any ) ;
37+ return prodState ;
38+ }
39+ }
2340
2441// An error that is thrown (e.g. by `use`) to trigger Suspense. If we
2542// detect this is caught by userspace, we'll log a warning in development.
@@ -56,7 +73,14 @@ export const noopSuspenseyCommitThenable = {
5673export function createThenableState ( ) : ThenableState {
5774 // The ThenableState is created the first time a component suspends. If it
5875 // suspends again, we'll reuse the same state.
59- return [ ] ;
76+ if ( __DEV__ ) {
77+ return {
78+ didWarnAboutUncachedPromise : false ,
79+ thenables : [ ] ,
80+ } ;
81+ } else {
82+ return [ ] ;
83+ }
6084}
6185
6286export function isThenableResolved ( thenable : Thenable < mixed > ) : boolean {
@@ -74,15 +98,44 @@ export function trackUsedThenable<T>(
7498 if ( __DEV__ && ReactCurrentActQueue . current !== null ) {
7599 ReactCurrentActQueue . didUsePromise = true ;
76100 }
77-
78- const previous = thenableState [ index ] ;
101+ const trackedThenables = getThenablesFromState ( thenableState ) ;
102+ const previous = trackedThenables [ index ] ;
79103 if ( previous === undefined ) {
80- thenableState . push ( thenable ) ;
104+ trackedThenables . push ( thenable ) ;
81105 } else {
82106 if ( previous !== thenable ) {
83107 // Reuse the previous thenable, and drop the new one. We can assume
84108 // they represent the same value, because components are idempotent.
85109
110+ if ( __DEV__ ) {
111+ const thenableStateDev : ThenableStateDev = ( thenableState : any ) ;
112+ if ( ! thenableStateDev . didWarnAboutUncachedPromise ) {
113+ // We should only warn the first time an uncached thenable is
114+ // discovered per component, because if there are multiple, the
115+ // subsequent ones are likely derived from the first.
116+ //
117+ // We track this on the thenableState instead of deduping using the
118+ // component name like we usually do, because in the case of a
119+ // promise-as-React-node, the owner component is likely different from
120+ // the parent that's currently being reconciled. We'd have to track
121+ // the owner using state, which we're trying to move away from. Though
122+ // since this is dev-only, maybe that'd be OK.
123+ //
124+ // However, another benefit of doing it this way is we might
125+ // eventually have a thenableState per memo/Forget boundary instead
126+ // of per component, so this would allow us to have more
127+ // granular warnings.
128+ thenableStateDev . didWarnAboutUncachedPromise = true ;
129+
130+ // TODO: This warning should link to a corresponding docs page.
131+ console . error (
132+ 'A component was suspended by an uncached promise. Creating ' +
133+ 'promises inside a Client Component or hook is not yet ' +
134+ 'supported, except via a Suspense-compatible library or framework.' ,
135+ ) ;
136+ }
137+ }
138+
86139 // Avoid an unhandled rejection errors for the Promises that we'll
87140 // intentionally ignore.
88141 thenable . then ( noop , noop ) ;
0 commit comments