@@ -147,10 +147,10 @@ import {
147
147
getNextLanes ,
148
148
getEntangledLanes ,
149
149
getLanesToRetrySynchronouslyOnError ,
150
- markRootUpdated ,
151
- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
152
- markRootPinged ,
153
150
upgradePendingLanesToSync ,
151
+ markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
152
+ markRootUpdated as _markRootUpdated ,
153
+ markRootPinged as _markRootPinged ,
154
154
markRootFinished ,
155
155
addFiberToLanesMap ,
156
156
movePendingFibersToMemoized ,
@@ -381,6 +381,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
381
381
let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
382
382
null ;
383
383
384
+ // Tracks when an update occurs during the render phase.
385
+ let workInProgressRootDidIncludeRecursiveRenderUpdate : boolean = false ;
386
+ // Thacks when an update occurs during the commit phase. It's a separate
387
+ // variable from the one for renders because the commit phase may run
388
+ // concurrently to a render phase.
389
+ let didIncludeCommitPhaseUpdate : boolean = false ;
390
+
384
391
// The most recent time we either committed a fallback, or when a fallback was
385
392
// filled in with the resolved UI. This lets us throttle the appearance of new
386
393
// content as it streams in, to minimize jank.
@@ -1155,6 +1162,7 @@ function finishConcurrentRender(
1155
1162
workInProgressRootRecoverableErrors ,
1156
1163
workInProgressTransitions ,
1157
1164
workInProgressDeferredLane ,
1165
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1158
1166
) ;
1159
1167
} else {
1160
1168
if (
@@ -1189,6 +1197,7 @@ function finishConcurrentRender(
1189
1197
finishedWork ,
1190
1198
workInProgressRootRecoverableErrors ,
1191
1199
workInProgressTransitions ,
1200
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1192
1201
lanes ,
1193
1202
workInProgressDeferredLane ,
1194
1203
) ,
@@ -1202,6 +1211,7 @@ function finishConcurrentRender(
1202
1211
finishedWork ,
1203
1212
workInProgressRootRecoverableErrors ,
1204
1213
workInProgressTransitions ,
1214
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1205
1215
lanes ,
1206
1216
workInProgressDeferredLane ,
1207
1217
) ;
@@ -1213,6 +1223,7 @@ function commitRootWhenReady(
1213
1223
finishedWork : Fiber ,
1214
1224
recoverableErrors : Array < CapturedValue < mixed >> | null ,
1215
1225
transitions : Array < Transition > | null ,
1226
+ didIncludeRenderPhaseUpdate : boolean ,
1216
1227
lanes : Lanes ,
1217
1228
spawnedLane : Lane ,
1218
1229
) {
@@ -1240,15 +1251,27 @@ function commitRootWhenReady(
1240
1251
// us that it's ready. This will be canceled if we start work on the
1241
1252
// root again.
1242
1253
root . cancelPendingCommit = schedulePendingCommit (
1243
- commitRoot . bind ( null , root , recoverableErrors , transitions ) ,
1254
+ commitRoot . bind (
1255
+ null ,
1256
+ root ,
1257
+ recoverableErrors ,
1258
+ transitions ,
1259
+ didIncludeRenderPhaseUpdate ,
1260
+ ) ,
1244
1261
) ;
1245
1262
markRootSuspended ( root , lanes , spawnedLane ) ;
1246
1263
return ;
1247
1264
}
1248
1265
}
1249
1266
1250
1267
// Otherwise, commit immediately.
1251
- commitRoot ( root , recoverableErrors , transitions , spawnedLane ) ;
1268
+ commitRoot (
1269
+ root ,
1270
+ recoverableErrors ,
1271
+ transitions ,
1272
+ spawnedLane ,
1273
+ didIncludeRenderPhaseUpdate ,
1274
+ ) ;
1252
1275
}
1253
1276
1254
1277
function isRenderConsistentWithExternalStores ( finishedWork : Fiber ) : boolean {
@@ -1304,21 +1327,55 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1304
1327
return true ;
1305
1328
}
1306
1329
1330
+ // The extra indirections around markRootUpdated and markRootSuspended is
1331
+ // needed to avoid a circular dependency between this module and
1332
+ // ReactFiberLane. There's probably a better way to split up these modules and
1333
+ // avoid this problem. Perhaps all the root-marking functions should move into
1334
+ // the work loop.
1335
+
1336
+ function markRootUpdated ( root : FiberRoot , updatedLanes : Lanes ) {
1337
+ _markRootUpdated ( root , updatedLanes ) ;
1338
+
1339
+ // Check for recursive updates
1340
+ if ( executionContext & RenderContext ) {
1341
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1342
+ } else if ( executionContext & CommitContext ) {
1343
+ didIncludeCommitPhaseUpdate = true ;
1344
+ }
1345
+
1346
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1347
+ }
1348
+
1349
+ function markRootPinged ( root : FiberRoot , pingedLanes : Lanes ) {
1350
+ _markRootPinged ( root , pingedLanes ) ;
1351
+
1352
+ // Check for recursive pings. Pings are conceptually different from updates in
1353
+ // other contexts but we call it an "update" in this context because
1354
+ // repeatedly pinging a suspended render can cause a recursive render loop.
1355
+ // The relevant property is that it can result in a new render attempt
1356
+ // being scheduled.
1357
+ if ( executionContext & RenderContext ) {
1358
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1359
+ } else if ( executionContext & CommitContext ) {
1360
+ didIncludeCommitPhaseUpdate = true ;
1361
+ }
1362
+
1363
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1364
+ }
1365
+
1307
1366
function markRootSuspended (
1308
1367
root : FiberRoot ,
1309
1368
suspendedLanes : Lanes ,
1310
1369
spawnedLane : Lane ,
1311
1370
) {
1312
1371
// When suspending, we should always exclude lanes that were pinged or (more
1313
1372
// rarely, since we try to avoid it) updated during the render phase.
1314
- // TODO: Lol maybe there's a better way to factor this besides this
1315
- // obnoxiously named function :)
1316
1373
suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
1317
1374
suspendedLanes = removeLanes (
1318
1375
suspendedLanes ,
1319
1376
workInProgressRootInterleavedUpdatedLanes ,
1320
1377
) ;
1321
- markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes , spawnedLane ) ;
1378
+ _markRootSuspended ( root , suspendedLanes , spawnedLane ) ;
1322
1379
}
1323
1380
1324
1381
// This is the entry point for synchronous tasks that don't go
@@ -1392,6 +1449,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1392
1449
workInProgressRootRecoverableErrors ,
1393
1450
workInProgressTransitions ,
1394
1451
workInProgressDeferredLane ,
1452
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1395
1453
) ;
1396
1454
1397
1455
// Before exiting, make sure there's a callback scheduled for the next
@@ -1607,6 +1665,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1607
1665
workInProgressDeferredLane = NoLane ;
1608
1666
workInProgressRootConcurrentErrors = null ;
1609
1667
workInProgressRootRecoverableErrors = null ;
1668
+ workInProgressRootDidIncludeRecursiveRenderUpdate = false ;
1610
1669
1611
1670
// Get the lanes that are entangled with whatever we're about to render. We
1612
1671
// track these separately so we can distinguish the priority of the render
@@ -2676,6 +2735,7 @@ function commitRoot(
2676
2735
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2677
2736
transitions : Array < Transition > | null ,
2678
2737
spawnedLane : Lane ,
2738
+ didIncludeRenderPhaseUpdate : boolean ,
2679
2739
) {
2680
2740
// TODO: This no longer makes any sense. We already wrap the mutation and
2681
2741
// layout phases. Should be able to remove.
@@ -2689,6 +2749,7 @@ function commitRoot(
2689
2749
root ,
2690
2750
recoverableErrors ,
2691
2751
transitions ,
2752
+ didIncludeRenderPhaseUpdate ,
2692
2753
previousUpdateLanePriority ,
2693
2754
spawnedLane ,
2694
2755
) ;
@@ -2704,6 +2765,7 @@ function commitRootImpl(
2704
2765
root : FiberRoot ,
2705
2766
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2706
2767
transitions : Array < Transition > | null ,
2768
+ didIncludeRenderPhaseUpdate : boolean ,
2707
2769
renderPriorityLevel : EventPriority ,
2708
2770
spawnedLane : Lane ,
2709
2771
) {
@@ -2784,6 +2846,9 @@ function commitRootImpl(
2784
2846
2785
2847
markRootFinished ( root , remainingLanes , spawnedLane ) ;
2786
2848
2849
+ // Reset this before firing side effects so we can detect recursive updates.
2850
+ didIncludeCommitPhaseUpdate = false ;
2851
+
2787
2852
if ( root === workInProgressRoot ) {
2788
2853
// We can reset these now that they are finished.
2789
2854
workInProgressRoot = null ;
@@ -3036,10 +3101,15 @@ function commitRootImpl(
3036
3101
// hydration lanes in this check, because render triggered by selective
3037
3102
// hydration is conceptually not an update.
3038
3103
if (
3104
+ // Check if there was a recursive update spawned by this render, in either
3105
+ // the render phase or the commit phase. We track these explicitly because
3106
+ // we can't infer from the remaining lanes alone.
3107
+ didIncludeCommitPhaseUpdate ||
3108
+ didIncludeRenderPhaseUpdate ||
3039
3109
// Was the finished render the result of an update (not hydration)?
3040
- includesSomeLane ( lanes , UpdateLanes ) &&
3041
- // Did it schedule a sync update?
3042
- includesSomeLane ( remainingLanes , SyncUpdateLanes )
3110
+ ( includesSomeLane ( lanes , UpdateLanes ) &&
3111
+ // Did it schedule a sync update?
3112
+ includesSomeLane ( remainingLanes , SyncUpdateLanes ) )
3043
3113
) {
3044
3114
if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
3045
3115
markNestedUpdateScheduled ( ) ;
@@ -3582,6 +3652,17 @@ export function throwIfInfiniteUpdateLoopDetected() {
3582
3652
rootWithNestedUpdates = null ;
3583
3653
rootWithPassiveNestedUpdates = null ;
3584
3654
3655
+ if ( executionContext & RenderContext && workInProgressRoot !== null ) {
3656
+ // We're in the render phase. Disable the concurrent error recovery
3657
+ // mechanism to ensure that the error we're about to throw gets handled.
3658
+ // We need it to trigger the nearest error boundary so that the infinite
3659
+ // update loop is broken.
3660
+ workInProgressRoot . errorRecoveryDisabledLanes = mergeLanes (
3661
+ workInProgressRoot . errorRecoveryDisabledLanes ,
3662
+ workInProgressRootRenderLanes ,
3663
+ ) ;
3664
+ }
3665
+
3585
3666
throw new Error (
3586
3667
'Maximum update depth exceeded. This can happen when a component ' +
3587
3668
'repeatedly calls setState inside componentWillUpdate or ' +
0 commit comments