@@ -19,7 +19,24 @@ import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
19
19
import ReactSharedInternals from 'shared/ReactSharedInternals' ;
20
20
const { ReactCurrentActQueue} = ReactSharedInternals ;
21
21
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
+ }
23
40
24
41
// An error that is thrown (e.g. by `use`) to trigger Suspense. If we
25
42
// detect this is caught by userspace, we'll log a warning in development.
@@ -56,7 +73,14 @@ export const noopSuspenseyCommitThenable = {
56
73
export function createThenableState ( ) : ThenableState {
57
74
// The ThenableState is created the first time a component suspends. If it
58
75
// 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
+ }
60
84
}
61
85
62
86
export function isThenableResolved ( thenable : Thenable < mixed > ) : boolean {
@@ -74,15 +98,44 @@ export function trackUsedThenable<T>(
74
98
if ( __DEV__ && ReactCurrentActQueue . current !== null ) {
75
99
ReactCurrentActQueue . didUsePromise = true ;
76
100
}
77
-
78
- const previous = thenableState [ index ] ;
101
+ const trackedThenables = getThenablesFromState ( thenableState ) ;
102
+ const previous = trackedThenables [ index ] ;
79
103
if ( previous === undefined ) {
80
- thenableState . push ( thenable ) ;
104
+ trackedThenables . push ( thenable ) ;
81
105
} else {
82
106
if ( previous !== thenable ) {
83
107
// Reuse the previous thenable, and drop the new one. We can assume
84
108
// they represent the same value, because components are idempotent.
85
109
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
+
86
139
// Avoid an unhandled rejection errors for the Promises that we'll
87
140
// intentionally ignore.
88
141
thenable . then ( noop , noop ) ;
0 commit comments