@@ -40,6 +40,7 @@ import {
40
40
useModernStrictMode ,
41
41
disableLegacyContext ,
42
42
alwaysThrottleRetries ,
43
+ enableInfiniteRenderLoopDetection ,
43
44
} from 'shared/ReactFeatureFlags' ;
44
45
import ReactSharedInternals from 'shared/ReactSharedInternals' ;
45
46
import is from 'shared/objectIs' ;
@@ -147,10 +148,10 @@ import {
147
148
getNextLanes ,
148
149
getEntangledLanes ,
149
150
getLanesToRetrySynchronouslyOnError ,
150
- markRootUpdated ,
151
- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
152
- markRootPinged ,
153
151
upgradePendingLanesToSync ,
152
+ markRootSuspended as _markRootSuspended ,
153
+ markRootUpdated as _markRootUpdated ,
154
+ markRootPinged as _markRootPinged ,
154
155
markRootFinished ,
155
156
addFiberToLanesMap ,
156
157
movePendingFibersToMemoized ,
@@ -381,6 +382,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
381
382
let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
382
383
null ;
383
384
385
+ // Tracks when an update occurs during the render phase.
386
+ let workInProgressRootDidIncludeRecursiveRenderUpdate : boolean = false ;
387
+ // Thacks when an update occurs during the commit phase. It's a separate
388
+ // variable from the one for renders because the commit phase may run
389
+ // concurrently to a render phase.
390
+ let didIncludeCommitPhaseUpdate : boolean = false ;
391
+
384
392
// The most recent time we either committed a fallback, or when a fallback was
385
393
// filled in with the resolved UI. This lets us throttle the appearance of new
386
394
// content as it streams in, to minimize jank.
@@ -1154,6 +1162,7 @@ function finishConcurrentRender(
1154
1162
root ,
1155
1163
workInProgressRootRecoverableErrors ,
1156
1164
workInProgressTransitions ,
1165
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1157
1166
workInProgressDeferredLane ,
1158
1167
) ;
1159
1168
} else {
@@ -1189,6 +1198,7 @@ function finishConcurrentRender(
1189
1198
finishedWork ,
1190
1199
workInProgressRootRecoverableErrors ,
1191
1200
workInProgressTransitions ,
1201
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1192
1202
lanes ,
1193
1203
workInProgressDeferredLane ,
1194
1204
) ,
@@ -1202,6 +1212,7 @@ function finishConcurrentRender(
1202
1212
finishedWork ,
1203
1213
workInProgressRootRecoverableErrors ,
1204
1214
workInProgressTransitions ,
1215
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1205
1216
lanes ,
1206
1217
workInProgressDeferredLane ,
1207
1218
) ;
@@ -1213,6 +1224,7 @@ function commitRootWhenReady(
1213
1224
finishedWork : Fiber ,
1214
1225
recoverableErrors : Array < CapturedValue < mixed >> | null ,
1215
1226
transitions : Array < Transition > | null ,
1227
+ didIncludeRenderPhaseUpdate : boolean ,
1216
1228
lanes : Lanes ,
1217
1229
spawnedLane : Lane ,
1218
1230
) {
@@ -1240,15 +1252,27 @@ function commitRootWhenReady(
1240
1252
// us that it's ready. This will be canceled if we start work on the
1241
1253
// root again.
1242
1254
root . cancelPendingCommit = schedulePendingCommit (
1243
- commitRoot . bind ( null , root , recoverableErrors , transitions ) ,
1255
+ commitRoot . bind (
1256
+ null ,
1257
+ root ,
1258
+ recoverableErrors ,
1259
+ transitions ,
1260
+ didIncludeRenderPhaseUpdate ,
1261
+ ) ,
1244
1262
) ;
1245
1263
markRootSuspended ( root , lanes , spawnedLane ) ;
1246
1264
return ;
1247
1265
}
1248
1266
}
1249
1267
1250
1268
// Otherwise, commit immediately.
1251
- commitRoot ( root , recoverableErrors , transitions , spawnedLane ) ;
1269
+ commitRoot (
1270
+ root ,
1271
+ recoverableErrors ,
1272
+ transitions ,
1273
+ didIncludeRenderPhaseUpdate ,
1274
+ spawnedLane ,
1275
+ ) ;
1252
1276
}
1253
1277
1254
1278
function isRenderConsistentWithExternalStores ( finishedWork : Fiber ) : boolean {
@@ -1304,21 +1328,59 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1304
1328
return true ;
1305
1329
}
1306
1330
1331
+ // The extra indirections around markRootUpdated and markRootSuspended is
1332
+ // needed to avoid a circular dependency between this module and
1333
+ // ReactFiberLane. There's probably a better way to split up these modules and
1334
+ // avoid this problem. Perhaps all the root-marking functions should move into
1335
+ // the work loop.
1336
+
1337
+ function markRootUpdated ( root : FiberRoot , updatedLanes : Lanes ) {
1338
+ _markRootUpdated ( root , updatedLanes ) ;
1339
+
1340
+ if ( enableInfiniteRenderLoopDetection ) {
1341
+ // Check for recursive updates
1342
+ if ( executionContext & RenderContext ) {
1343
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1344
+ } else if ( executionContext & CommitContext ) {
1345
+ didIncludeCommitPhaseUpdate = true ;
1346
+ }
1347
+
1348
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1349
+ }
1350
+ }
1351
+
1352
+ function markRootPinged ( root : FiberRoot , pingedLanes : Lanes ) {
1353
+ _markRootPinged ( root , pingedLanes ) ;
1354
+
1355
+ if ( enableInfiniteRenderLoopDetection ) {
1356
+ // Check for recursive pings. Pings are conceptually different from updates in
1357
+ // other contexts but we call it an "update" in this context because
1358
+ // repeatedly pinging a suspended render can cause a recursive render loop.
1359
+ // The relevant property is that it can result in a new render attempt
1360
+ // being scheduled.
1361
+ if ( executionContext & RenderContext ) {
1362
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1363
+ } else if ( executionContext & CommitContext ) {
1364
+ didIncludeCommitPhaseUpdate = true ;
1365
+ }
1366
+
1367
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1368
+ }
1369
+ }
1370
+
1307
1371
function markRootSuspended (
1308
1372
root : FiberRoot ,
1309
1373
suspendedLanes : Lanes ,
1310
1374
spawnedLane : Lane ,
1311
1375
) {
1312
1376
// When suspending, we should always exclude lanes that were pinged or (more
1313
1377
// 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
1378
suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
1317
1379
suspendedLanes = removeLanes (
1318
1380
suspendedLanes ,
1319
1381
workInProgressRootInterleavedUpdatedLanes ,
1320
1382
) ;
1321
- markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes , spawnedLane ) ;
1383
+ _markRootSuspended ( root , suspendedLanes , spawnedLane ) ;
1322
1384
}
1323
1385
1324
1386
// This is the entry point for synchronous tasks that don't go
@@ -1391,6 +1453,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1391
1453
root ,
1392
1454
workInProgressRootRecoverableErrors ,
1393
1455
workInProgressTransitions ,
1456
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1394
1457
workInProgressDeferredLane ,
1395
1458
) ;
1396
1459
@@ -1607,6 +1670,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1607
1670
workInProgressDeferredLane = NoLane ;
1608
1671
workInProgressRootConcurrentErrors = null ;
1609
1672
workInProgressRootRecoverableErrors = null ;
1673
+ workInProgressRootDidIncludeRecursiveRenderUpdate = false ;
1610
1674
1611
1675
// Get the lanes that are entangled with whatever we're about to render. We
1612
1676
// track these separately so we can distinguish the priority of the render
@@ -2675,6 +2739,7 @@ function commitRoot(
2675
2739
root : FiberRoot ,
2676
2740
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2677
2741
transitions : Array < Transition > | null ,
2742
+ didIncludeRenderPhaseUpdate : boolean ,
2678
2743
spawnedLane : Lane ,
2679
2744
) {
2680
2745
// TODO: This no longer makes any sense. We already wrap the mutation and
@@ -2689,6 +2754,7 @@ function commitRoot(
2689
2754
root ,
2690
2755
recoverableErrors ,
2691
2756
transitions ,
2757
+ didIncludeRenderPhaseUpdate ,
2692
2758
previousUpdateLanePriority ,
2693
2759
spawnedLane ,
2694
2760
) ;
@@ -2704,6 +2770,7 @@ function commitRootImpl(
2704
2770
root : FiberRoot ,
2705
2771
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2706
2772
transitions : Array < Transition > | null ,
2773
+ didIncludeRenderPhaseUpdate : boolean ,
2707
2774
renderPriorityLevel : EventPriority ,
2708
2775
spawnedLane : Lane ,
2709
2776
) {
@@ -2784,6 +2851,9 @@ function commitRootImpl(
2784
2851
2785
2852
markRootFinished ( root , remainingLanes , spawnedLane ) ;
2786
2853
2854
+ // Reset this before firing side effects so we can detect recursive updates.
2855
+ didIncludeCommitPhaseUpdate = false ;
2856
+
2787
2857
if ( root === workInProgressRoot ) {
2788
2858
// We can reset these now that they are finished.
2789
2859
workInProgressRoot = null ;
@@ -3036,10 +3106,15 @@ function commitRootImpl(
3036
3106
// hydration lanes in this check, because render triggered by selective
3037
3107
// hydration is conceptually not an update.
3038
3108
if (
3109
+ // Check if there was a recursive update spawned by this render, in either
3110
+ // the render phase or the commit phase. We track these explicitly because
3111
+ // we can't infer from the remaining lanes alone.
3112
+ ( enableInfiniteRenderLoopDetection &&
3113
+ ( didIncludeRenderPhaseUpdate || didIncludeCommitPhaseUpdate ) ) ||
3039
3114
// 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 )
3115
+ ( includesSomeLane ( lanes , UpdateLanes ) &&
3116
+ // Did it schedule a sync update?
3117
+ includesSomeLane ( remainingLanes , SyncUpdateLanes ) )
3043
3118
) {
3044
3119
if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
3045
3120
markNestedUpdateScheduled ( ) ;
@@ -3582,6 +3657,19 @@ export function throwIfInfiniteUpdateLoopDetected() {
3582
3657
rootWithNestedUpdates = null ;
3583
3658
rootWithPassiveNestedUpdates = null ;
3584
3659
3660
+ if ( enableInfiniteRenderLoopDetection ) {
3661
+ if ( executionContext & RenderContext && workInProgressRoot !== null ) {
3662
+ // We're in the render phase. Disable the concurrent error recovery
3663
+ // mechanism to ensure that the error we're about to throw gets handled.
3664
+ // We need it to trigger the nearest error boundary so that the infinite
3665
+ // update loop is broken.
3666
+ workInProgressRoot . errorRecoveryDisabledLanes = mergeLanes (
3667
+ workInProgressRoot . errorRecoveryDisabledLanes ,
3668
+ workInProgressRootRenderLanes ,
3669
+ ) ;
3670
+ }
3671
+ }
3672
+
3585
3673
throw new Error (
3586
3674
'Maximum update depth exceeded. This can happen when a component ' +
3587
3675
'repeatedly calls setState inside componentWillUpdate or ' +
0 commit comments