@@ -143,9 +143,9 @@ import {
143
143
includesExpiredLane ,
144
144
getNextLanes ,
145
145
getLanesToRetrySynchronouslyOnError ,
146
- markRootUpdated ,
147
- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
148
- markRootPinged ,
146
+ markRootSuspended as _markRootSuspended ,
147
+ markRootUpdated as _markRootUpdated ,
148
+ markRootPinged as _markRootPinged ,
149
149
markRootEntangled ,
150
150
markRootFinished ,
151
151
addFiberToLanesMap ,
@@ -372,6 +372,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
372
372
let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
373
373
null ;
374
374
375
+ // Tracks when an update occurs during the render phase.
376
+ let workInProgressRootDidIncludeRecursiveRenderUpdate : boolean = false ;
377
+ // Thacks when an update occurs during the commit phase. It's a separate
378
+ // variable from the one for renders because the commit phase may run
379
+ // concurrently to a render phase.
380
+ let didIncludeCommitPhaseUpdate : boolean = false ;
381
+
375
382
// The most recent time we committed a fallback. This lets us ensure a train
376
383
// model where we don't commit new loading states in too quick succession.
377
384
let globalMostRecentFallbackTime : number = 0 ;
@@ -1117,6 +1124,7 @@ function finishConcurrentRender(
1117
1124
root ,
1118
1125
workInProgressRootRecoverableErrors ,
1119
1126
workInProgressTransitions ,
1127
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1120
1128
) ;
1121
1129
} else {
1122
1130
if ( includesOnlyRetries ( lanes ) ) {
@@ -1148,6 +1156,7 @@ function finishConcurrentRender(
1148
1156
finishedWork ,
1149
1157
workInProgressRootRecoverableErrors ,
1150
1158
workInProgressTransitions ,
1159
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1151
1160
lanes ,
1152
1161
) ,
1153
1162
msUntilTimeout ,
@@ -1160,6 +1169,7 @@ function finishConcurrentRender(
1160
1169
finishedWork ,
1161
1170
workInProgressRootRecoverableErrors ,
1162
1171
workInProgressTransitions ,
1172
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1163
1173
lanes ,
1164
1174
) ;
1165
1175
}
@@ -1170,6 +1180,7 @@ function commitRootWhenReady(
1170
1180
finishedWork : Fiber ,
1171
1181
recoverableErrors : Array < CapturedValue < mixed >> | null ,
1172
1182
transitions : Array < Transition > | null ,
1183
+ didIncludeRenderPhaseUpdate : boolean ,
1173
1184
lanes : Lanes ,
1174
1185
) {
1175
1186
// TODO: Combine retry throttling with Suspensey commits. Right now they run
@@ -1196,15 +1207,21 @@ function commitRootWhenReady(
1196
1207
// us that it's ready. This will be canceled if we start work on the
1197
1208
// root again.
1198
1209
root . cancelPendingCommit = schedulePendingCommit (
1199
- commitRoot . bind ( null , root , recoverableErrors , transitions ) ,
1210
+ commitRoot . bind (
1211
+ null ,
1212
+ root ,
1213
+ recoverableErrors ,
1214
+ transitions ,
1215
+ didIncludeRenderPhaseUpdate ,
1216
+ ) ,
1200
1217
) ;
1201
1218
markRootSuspended ( root , lanes ) ;
1202
1219
return ;
1203
1220
}
1204
1221
}
1205
1222
1206
1223
// Otherwise, commit immediately.
1207
- commitRoot ( root , recoverableErrors , transitions ) ;
1224
+ commitRoot ( root , recoverableErrors , transitions , didIncludeRenderPhaseUpdate ) ;
1208
1225
}
1209
1226
1210
1227
function isRenderConsistentWithExternalStores ( finishedWork : Fiber ) : boolean {
@@ -1260,17 +1277,51 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1260
1277
return true ;
1261
1278
}
1262
1279
1280
+ // The extra indirections around markRootUpdated and markRootSuspended is
1281
+ // needed to avoid a circular dependency between this module and
1282
+ // ReactFiberLane. There's probably a better way to split up these modules and
1283
+ // avoid this problem. Perhaps all the root-marking functions should move into
1284
+ // the work loop.
1285
+
1286
+ function markRootUpdated ( root : FiberRoot , updatedLanes : Lanes ) {
1287
+ _markRootUpdated ( root , updatedLanes ) ;
1288
+
1289
+ // Check for recursive updates
1290
+ if ( executionContext & RenderContext ) {
1291
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1292
+ } else if ( executionContext & CommitContext ) {
1293
+ didIncludeCommitPhaseUpdate = true ;
1294
+ }
1295
+
1296
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1297
+ }
1298
+
1299
+ function markRootPinged ( root : FiberRoot , pingedLanes : Lanes ) {
1300
+ _markRootPinged ( root , pingedLanes ) ;
1301
+
1302
+ // Check for recursive pings. Pings are conceptually different from updates in
1303
+ // other contexts but we call it an "update" in this context because
1304
+ // repeatedly pinging a suspended render can cause a recursive render loop.
1305
+ // The relevant property is that it can result in a new render attempt
1306
+ // being scheduled.
1307
+ if ( executionContext & RenderContext ) {
1308
+ workInProgressRootDidIncludeRecursiveRenderUpdate = true ;
1309
+ } else if ( executionContext & CommitContext ) {
1310
+ didIncludeCommitPhaseUpdate = true ;
1311
+ }
1312
+
1313
+ throwIfInfiniteUpdateLoopDetected ( ) ;
1314
+ }
1315
+
1263
1316
function markRootSuspended ( root : FiberRoot , suspendedLanes : Lanes ) {
1264
1317
// When suspending, we should always exclude lanes that were pinged or (more
1265
1318
// rarely, since we try to avoid it) updated during the render phase.
1266
- // TODO: Lol maybe there's a better way to factor this besides this
1267
- // obnoxiously named function :)
1268
1319
suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
1269
1320
suspendedLanes = removeLanes (
1270
1321
suspendedLanes ,
1271
1322
workInProgressRootInterleavedUpdatedLanes ,
1272
1323
) ;
1273
- markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes ) ;
1324
+ _markRootSuspended ( root , suspendedLanes ) ;
1274
1325
}
1275
1326
1276
1327
// This is the entry point for synchronous tasks that don't go
@@ -1341,6 +1392,7 @@ export function performSyncWorkOnRoot(root: FiberRoot): null {
1341
1392
root ,
1342
1393
workInProgressRootRecoverableErrors ,
1343
1394
workInProgressTransitions ,
1395
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1344
1396
) ;
1345
1397
1346
1398
// Before exiting, make sure there's a callback scheduled for the next
@@ -1555,6 +1607,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1555
1607
workInProgressRootPingedLanes = NoLanes ;
1556
1608
workInProgressRootConcurrentErrors = null ;
1557
1609
workInProgressRootRecoverableErrors = null ;
1610
+ workInProgressRootDidIncludeRecursiveRenderUpdate = false ;
1558
1611
1559
1612
finishQueueingConcurrentUpdates ( ) ;
1560
1613
@@ -2569,6 +2622,7 @@ function commitRoot(
2569
2622
root : FiberRoot ,
2570
2623
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2571
2624
transitions : Array < Transition > | null ,
2625
+ didIncludeRenderPhaseUpdate : boolean ,
2572
2626
) {
2573
2627
// TODO: This no longer makes any sense. We already wrap the mutation and
2574
2628
// layout phases. Should be able to remove.
@@ -2582,6 +2636,7 @@ function commitRoot(
2582
2636
root ,
2583
2637
recoverableErrors ,
2584
2638
transitions ,
2639
+ didIncludeRenderPhaseUpdate ,
2585
2640
previousUpdateLanePriority ,
2586
2641
) ;
2587
2642
} finally {
@@ -2596,6 +2651,7 @@ function commitRootImpl(
2596
2651
root : FiberRoot ,
2597
2652
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2598
2653
transitions : Array < Transition > | null ,
2654
+ didIncludeRenderPhaseUpdate : boolean ,
2599
2655
renderPriorityLevel : EventPriority ,
2600
2656
) {
2601
2657
do {
@@ -2675,6 +2731,9 @@ function commitRootImpl(
2675
2731
2676
2732
markRootFinished ( root , remainingLanes ) ;
2677
2733
2734
+ // Reset this before firing side effects so we can detect recursive updates.
2735
+ didIncludeCommitPhaseUpdate = false ;
2736
+
2678
2737
if ( root === workInProgressRoot ) {
2679
2738
// We can reset these now that they are finished.
2680
2739
workInProgressRoot = null ;
@@ -2921,7 +2980,19 @@ function commitRootImpl(
2921
2980
2922
2981
// Read this again, since a passive effect might have updated it
2923
2982
remainingLanes = root . pendingLanes ;
2924
- if ( includesSyncLane ( remainingLanes ) ) {
2983
+ if (
2984
+ // Check if there was a recursive update spawned by this render, in either
2985
+ // the render phase or the commit phase. We track these explicitly because
2986
+ // we can't infer from the remaining lanes alone.
2987
+ didIncludeCommitPhaseUpdate ||
2988
+ didIncludeRenderPhaseUpdate ||
2989
+ // As an additional precaution, we also check if there's any remaining sync
2990
+ // work. Theoretically this should be unreachable but if there's a mistake
2991
+ // in React it helps to be overly defensive given how hard it is to debug
2992
+ // those scenarios otherwise. This won't catch recursive async updates,
2993
+ // though, which is why we check the flags above first.
2994
+ includesSyncLane ( remainingLanes )
2995
+ ) {
2925
2996
if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
2926
2997
markNestedUpdateScheduled ( ) ;
2927
2998
}
0 commit comments