@@ -62,6 +62,7 @@ import {
62
62
} from './ReactFiberSuspenseContext.new' ;
63
63
import {
64
64
renderDidError ,
65
+ renderDidSuspendDelayIfPossible ,
65
66
onUncaughtError ,
66
67
markLegacyErrorBoundaryAsFailed ,
67
68
isAlreadyFailedLegacyErrorBoundary ,
@@ -78,6 +79,7 @@ import {
78
79
includesSomeLane ,
79
80
mergeLanes ,
80
81
pickArbitraryLane ,
82
+ includesOnlyTransitions ,
81
83
} from './ReactFiberLane.new' ;
82
84
import {
83
85
getIsHydrating ,
@@ -165,12 +167,7 @@ function createClassErrorUpdate(
165
167
return update ;
166
168
}
167
169
168
- function attachWakeableListeners (
169
- suspenseBoundary : Fiber ,
170
- root : FiberRoot ,
171
- wakeable : Wakeable ,
172
- lanes : Lanes ,
173
- ) {
170
+ function attachPingListener ( root : FiberRoot , wakeable : Wakeable , lanes : Lanes ) {
174
171
// Attach a ping listener
175
172
//
176
173
// The data might resolve before we have a chance to commit the fallback. Or,
@@ -183,34 +180,39 @@ function attachWakeableListeners(
183
180
//
184
181
// We only need to do this in concurrent mode. Legacy Suspense always
185
182
// commits fallbacks synchronously, so there are no pings.
186
- if ( suspenseBoundary . mode & ConcurrentMode ) {
187
- let pingCache = root . pingCache ;
188
- let threadIDs ;
189
- if ( pingCache === null ) {
190
- pingCache = root . pingCache = new PossiblyWeakMap ( ) ;
183
+ let pingCache = root . pingCache ;
184
+ let threadIDs ;
185
+ if ( pingCache === null ) {
186
+ pingCache = root . pingCache = new PossiblyWeakMap ( ) ;
187
+ threadIDs = new Set ( ) ;
188
+ pingCache . set ( wakeable , threadIDs ) ;
189
+ } else {
190
+ threadIDs = pingCache . get ( wakeable ) ;
191
+ if ( threadIDs === undefined ) {
191
192
threadIDs = new Set ( ) ;
192
193
pingCache . set ( wakeable , threadIDs ) ;
193
- } else {
194
- threadIDs = pingCache . get ( wakeable ) ;
195
- if ( threadIDs === undefined ) {
196
- threadIDs = new Set ( ) ;
197
- pingCache . set ( wakeable , threadIDs ) ;
198
- }
199
194
}
200
- if ( ! threadIDs . has ( lanes ) ) {
201
- // Memoize using the thread ID to prevent redundant listeners.
202
- threadIDs . add ( lanes ) ;
203
- const ping = pingSuspendedRoot . bind ( null , root , wakeable , lanes ) ;
204
- if ( enableUpdaterTracking ) {
205
- if ( isDevToolsPresent ) {
206
- // If we have pending work still, restore the original updaters
207
- restorePendingUpdaters ( root , lanes ) ;
208
- }
195
+ }
196
+ if ( ! threadIDs . has ( lanes ) ) {
197
+ // Memoize using the thread ID to prevent redundant listeners.
198
+ threadIDs . add ( lanes ) ;
199
+ const ping = pingSuspendedRoot . bind ( null , root , wakeable , lanes ) ;
200
+ if ( enableUpdaterTracking ) {
201
+ if ( isDevToolsPresent ) {
202
+ // If we have pending work still, restore the original updaters
203
+ restorePendingUpdaters ( root , lanes ) ;
209
204
}
210
- wakeable . then ( ping , ping ) ;
211
205
}
206
+ wakeable . then ( ping , ping ) ;
212
207
}
208
+ }
213
209
210
+ function attachRetryListener (
211
+ suspenseBoundary : Fiber ,
212
+ root : FiberRoot ,
213
+ wakeable : Wakeable ,
214
+ lanes : Lanes ,
215
+ ) {
214
216
// Retry listener
215
217
//
216
218
// If the fallback does commit, we need to attach a different type of
@@ -470,24 +472,47 @@ function throwException(
470
472
root ,
471
473
rootRenderLanes ,
472
474
) ;
473
- attachWakeableListeners (
474
- suspenseBoundary ,
475
- root ,
476
- wakeable ,
477
- rootRenderLanes ,
478
- ) ;
475
+ // We only attach ping listeners in concurrent mode. Legacy Suspense always
476
+ // commits fallbacks synchronously, so there are no pings.
477
+ if ( suspenseBoundary . mode & ConcurrentMode ) {
478
+ attachPingListener ( root , wakeable , rootRenderLanes ) ;
479
+ }
480
+ attachRetryListener ( suspenseBoundary , root , wakeable , rootRenderLanes ) ;
479
481
return ;
480
482
} else {
481
- // No boundary was found. Fallthrough to error mode.
483
+ // No boundary was found. If we're inside startTransition, this is OK.
484
+ // We can suspend and wait for more data to arrive.
485
+
486
+ if ( includesOnlyTransitions ( rootRenderLanes ) ) {
487
+ // This is a transition. Suspend. Since we're not activating a Suspense
488
+ // boundary, this will unwind all the way to the root without performing
489
+ // a second pass to render a fallback. (This is arguably how refresh
490
+ // transitions should work, too, since we're not going to commit the
491
+ // fallbacks anyway.)
492
+ attachPingListener ( root , wakeable , rootRenderLanes ) ;
493
+ renderDidSuspendDelayIfPossible ( ) ;
494
+ return ;
495
+ }
496
+
497
+ // We're not in a transition. We treat this case like an error because
498
+ // discrete renders are expected to finish synchronously to maintain
499
+ // consistency with external state.
500
+ // TODO: This will error during non-transition concurrent renders, too.
501
+ // But maybe it shouldn't?
502
+
482
503
// TODO: We should never call getComponentNameFromFiber in production.
483
504
// Log a warning or something to prevent us from accidentally bundling it.
484
- value = new Error (
505
+ const uncaughtSuspenseError = new Error (
485
506
( getComponentNameFromFiber ( sourceFiber ) || 'A React component' ) +
486
507
' suspended while rendering, but no fallback UI was specified.\n' +
487
508
'\n' +
488
509
'Add a <Suspense fallback=...> component higher in the tree to ' +
489
510
'provide a loading indicator or placeholder to display.' ,
490
511
) ;
512
+
513
+ // If we're outside a transition, fall through to the regular error path.
514
+ // The error will be caught by the nearest suspense boundary.
515
+ value = uncaughtSuspenseError ;
491
516
}
492
517
} else {
493
518
// This is a regular error, not a Suspense wakeable.
0 commit comments