@@ -142,9 +142,9 @@ import {
142
142
includesExpiredLane ,
143
143
getNextLanes ,
144
144
getLanesToRetrySynchronouslyOnError ,
145
- markRootUpdated ,
146
- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
147
- markRootPinged ,
145
+ markRootSuspended as _markRootSuspended ,
146
+ markRootUpdated as _markRootUpdated ,
147
+ markRootPinged as _markRootPinged ,
148
148
markRootEntangled ,
149
149
markRootFinished ,
150
150
addFiberToLanesMap ,
@@ -373,6 +373,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
373
373
let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
374
374
null ;
375
375
376
+ // Tracks when an update occurs during the render phase.
377
+ let workInProgressRootDidIncludeRecursiveRenderUpdate : boolean = false ;
378
+ // Thacks when an update occurs during the commit phase. It's a separate
379
+ // variable from the one for renders because the commit phase may run
380
+ // concurrently to a render phase.
381
+ let didIncludeCommitPhaseUpdate : boolean = false ;
382
+
376
383
// The most recent time we either committed a fallback, or when a fallback was
377
384
// filled in with the resolved UI. This lets us throttle the appearance of new
378
385
// content as it streams in, to minimize jank.
@@ -1095,6 +1102,7 @@ function finishConcurrentRender(
1095
1102
root ,
1096
1103
workInProgressRootRecoverableErrors ,
1097
1104
workInProgressTransitions ,
1105
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1098
1106
) ;
1099
1107
} else {
1100
1108
if (
@@ -1129,6 +1137,7 @@ function finishConcurrentRender(
1129
1137
finishedWork ,
1130
1138
workInProgressRootRecoverableErrors ,
1131
1139
workInProgressTransitions ,
1140
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1132
1141
lanes ,
1133
1142
) ,
1134
1143
msUntilTimeout ,
@@ -1141,6 +1150,7 @@ function finishConcurrentRender(
1141
1150
finishedWork ,
1142
1151
workInProgressRootRecoverableErrors ,
1143
1152
workInProgressTransitions ,
1153
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1144
1154
lanes ,
1145
1155
) ;
1146
1156
}
@@ -1151,6 +1161,7 @@ function commitRootWhenReady(
1151
1161
finishedWork : Fiber ,
1152
1162
recoverableErrors : Array < CapturedValue < mixed >> | null ,
1153
1163
transitions : Array < Transition > | null ,
1164
+ didIncludeRenderPhaseUpdate : boolean ,
1154
1165
lanes : Lanes ,
1155
1166
) {
1156
1167
// TODO: Combine retry throttling with Suspensey commits. Right now they run
@@ -1177,15 +1188,21 @@ function commitRootWhenReady(
1177
1188
// us that it's ready. This will be canceled if we start work on the
1178
1189
// root again.
1179
1190
root . cancelPendingCommit = schedulePendingCommit (
1180
- commitRoot . bind ( null , root , recoverableErrors , transitions ) ,
1191
+ commitRoot . bind (
1192
+ null ,
1193
+ root ,
1194
+ recoverableErrors ,
1195
+ transitions ,
1196
+ didIncludeRenderPhaseUpdate ,
1197
+ ) ,
1181
1198
) ;
1182
1199
markRootSuspended ( root , lanes ) ;
1183
1200
return ;
1184
1201
}
1185
1202
}
1186
1203
1187
1204
// Otherwise, commit immediately.
1188
- commitRoot ( root , recoverableErrors , transitions ) ;
1205
+ commitRoot ( root , recoverableErrors , transitions , didIncludeRenderPhaseUpdate ) ;
1189
1206
}
1190
1207
1191
1208
function isRenderConsistentWithExternalStores ( finishedWork : Fiber ) : boolean {
@@ -1241,17 +1258,51 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1241
1258
return true ;
1242
1259
}
1243
1260
1261
+ // The extra indirections around markRootUpdated and markRootSuspended is
1262
+ // needed to avoid a circular dependency between this module and
1263
+ // ReactFiberLane. There's probably a better way to split up these modules and
1264
+ // avoid this problem. Perhaps all the root-marking functions should move into
1265
+ // the work loop.
1266
+
1267
+ function markRootUpdated ( root : FiberRoot , updatedLanes : Lanes ) {
1268
+ _markRootUpdated ( root , updatedLanes ) ;
1269
+
1270
+ // Check for recursive updates
1271
+ if ( executionContext & RenderContext ) {
1272
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1273
+ } else if ( executionContext & CommitContext ) {
1274
+ didIncludeCommitPhaseUpdate = true ;
1275
+ }
1276
+
1277
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1278
+ }
1279
+
1280
+ function markRootPinged ( root : FiberRoot , pingedLanes : Lanes ) {
1281
+ _markRootPinged ( root , pingedLanes ) ;
1282
+
1283
+ // Check for recursive pings. Pings are conceptually different from updates in
1284
+ // other contexts but we call it an "update" in this context because
1285
+ // repeatedly pinging a suspended render can cause a recursive render loop.
1286
+ // The relevant property is that it can result in a new render attempt
1287
+ // being scheduled.
1288
+ if ( executionContext & RenderContext ) {
1289
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1290
+ } else if ( executionContext & CommitContext ) {
1291
+ didIncludeCommitPhaseUpdate = true ;
1292
+ }
1293
+
1294
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1295
+ }
1296
+
1244
1297
function markRootSuspended ( root : FiberRoot , suspendedLanes : Lanes ) {
1245
1298
// When suspending, we should always exclude lanes that were pinged or (more
1246
1299
// rarely, since we try to avoid it) updated during the render phase.
1247
- // TODO: Lol maybe there's a better way to factor this besides this
1248
- // obnoxiously named function :)
1249
1300
suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
1250
1301
suspendedLanes = removeLanes (
1251
1302
suspendedLanes ,
1252
1303
workInProgressRootInterleavedUpdatedLanes ,
1253
1304
) ;
1254
- markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes ) ;
1305
+ _markRootSuspended ( root , suspendedLanes ) ;
1255
1306
}
1256
1307
1257
1308
// This is the entry point for synchronous tasks that don't go
@@ -1324,6 +1375,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1324
1375
root ,
1325
1376
workInProgressRootRecoverableErrors ,
1326
1377
workInProgressTransitions ,
1378
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1327
1379
) ;
1328
1380
1329
1381
// Before exiting, make sure there's a callback scheduled for the next
@@ -1538,6 +1590,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1538
1590
workInProgressRootPingedLanes = NoLanes ;
1539
1591
workInProgressRootConcurrentErrors = null ;
1540
1592
workInProgressRootRecoverableErrors = null ;
1593
+ workInProgressRootDidIncludeRecursiveRenderUpdate = false ;
1541
1594
1542
1595
finishQueueingConcurrentUpdates ( ) ;
1543
1596
@@ -2582,6 +2635,7 @@ function commitRoot(
2582
2635
root : FiberRoot ,
2583
2636
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2584
2637
transitions : Array < Transition > | null ,
2638
+ didIncludeRenderPhaseUpdate : boolean ,
2585
2639
) {
2586
2640
// TODO: This no longer makes any sense. We already wrap the mutation and
2587
2641
// layout phases. Should be able to remove.
@@ -2595,6 +2649,7 @@ function commitRoot(
2595
2649
root ,
2596
2650
recoverableErrors ,
2597
2651
transitions ,
2652
+ didIncludeRenderPhaseUpdate ,
2598
2653
previousUpdateLanePriority ,
2599
2654
) ;
2600
2655
} finally {
@@ -2609,6 +2664,7 @@ function commitRootImpl(
2609
2664
root : FiberRoot ,
2610
2665
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2611
2666
transitions : Array < Transition > | null ,
2667
+ didIncludeRenderPhaseUpdate : boolean ,
2612
2668
renderPriorityLevel : EventPriority ,
2613
2669
) {
2614
2670
do {
@@ -2688,6 +2744,9 @@ function commitRootImpl(
2688
2744
2689
2745
markRootFinished ( root , remainingLanes ) ;
2690
2746
2747
+ // Reset this before firing side effects so we can detect recursive updates.
2748
+ didIncludeCommitPhaseUpdate = false ;
2749
+
2691
2750
if ( root === workInProgressRoot ) {
2692
2751
// We can reset these now that they are finished.
2693
2752
workInProgressRoot = null ;
@@ -2940,6 +2999,16 @@ function commitRootImpl(
2940
2999
// hydration lanes in this check, because render triggered by selective
2941
3000
// hydration is conceptually not an update.
2942
3001
if (
3002
+ // Check if there was a recursive update spawned by this render, in either
3003
+ // the render phase or the commit phase. We track these explicitly because
3004
+ // we can't infer from the remaining lanes alone.
3005
+ didIncludeCommitPhaseUpdate ||
3006
+ didIncludeRenderPhaseUpdate ||
3007
+ // As an additional precaution, we also check if there's any remaining sync
3008
+ // work. Theoretically this should be unreachable but if there's a mistake
3009
+ // in React it helps to be overly defensive given how hard it is to debug
3010
+ // those scenarios otherwise. This won't catch recursive async updates,
3011
+ // though, which is why we check the flags above first.
2943
3012
// Was the finished render the result of an update (not hydration)?
2944
3013
includesSomeLane ( lanes , UpdateLanes ) &&
2945
3014
// Did it schedule a sync update?
@@ -3486,6 +3555,17 @@ export function throwIfInfiniteUpdateLoopDetected() {
3486
3555
rootWithNestedUpdates = null ;
3487
3556
rootWithPassiveNestedUpdates = null ;
3488
3557
3558
+ if ( executionContext & RenderContext && workInProgressRoot !== null ) {
3559
+ // We're in the render phase. Disable the concurrent error recovery
3560
+ // mechanism to ensure that the error we're about to throw gets handled.
3561
+ // We need it to trigger the nearest error boundary so that the infinite
3562
+ // update loop is broken.
3563
+ workInProgressRoot . errorRecoveryDisabledLanes = mergeLanes (
3564
+ workInProgressRoot . errorRecoveryDisabledLanes ,
3565
+ workInProgressRootRenderLanes ,
3566
+ ) ;
3567
+ }
3568
+
3489
3569
throw new Error (
3490
3570
'Maximum update depth exceeded. This can happen when a component ' +
3491
3571
'repeatedly calls setState inside componentWillUpdate or ' +
0 commit comments