@@ -141,9 +141,9 @@ import {
141
141
includesExpiredLane ,
142
142
getNextLanes ,
143
143
getLanesToRetrySynchronouslyOnError ,
144
- markRootUpdated ,
145
- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
146
- markRootPinged ,
144
+ markRootSuspended as _markRootSuspended ,
145
+ markRootUpdated as _markRootUpdated ,
146
+ markRootPinged as _markRootPinged ,
147
147
markRootEntangled ,
148
148
markRootFinished ,
149
149
addFiberToLanesMap ,
@@ -370,6 +370,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
370
370
let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
371
371
null ;
372
372
373
+ // Tracks when an update occurs during the render phase.
374
+ let workInProgressRootDidIncludeRecursiveRenderUpdate : boolean = false ;
375
+ // Thacks when an update occurs during the commit phase. It's a separate
376
+ // variable from the one for renders because the commit phase may run
377
+ // concurrently to a render phase.
378
+ let didIncludeCommitPhaseUpdate : boolean = false ;
379
+
373
380
// The most recent time we either committed a fallback, or when a fallback was
374
381
// filled in with the resolved UI. This lets us throttle the appearance of new
375
382
// content as it streams in, to minimize jank.
@@ -1114,6 +1121,7 @@ function finishConcurrentRender(
1114
1121
root ,
1115
1122
workInProgressRootRecoverableErrors ,
1116
1123
workInProgressTransitions ,
1124
+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
1117
1125
) ;
1118
1126
} else {
1119
1127
if (
@@ -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
@@ -2577,6 +2630,7 @@ function commitRoot(
2577
2630
root : FiberRoot ,
2578
2631
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2579
2632
transitions : Array < Transition > | null ,
2633
+ didIncludeRenderPhaseUpdate : boolean ,
2580
2634
) {
2581
2635
// TODO: This no longer makes any sense. We already wrap the mutation and
2582
2636
// layout phases. Should be able to remove.
@@ -2590,6 +2644,7 @@ function commitRoot(
2590
2644
root ,
2591
2645
recoverableErrors ,
2592
2646
transitions ,
2647
+ didIncludeRenderPhaseUpdate ,
2593
2648
previousUpdateLanePriority ,
2594
2649
) ;
2595
2650
} finally {
@@ -2604,6 +2659,7 @@ function commitRootImpl(
2604
2659
root : FiberRoot ,
2605
2660
recoverableErrors : null | Array < CapturedValue < mixed >> ,
2606
2661
transitions : Array < Transition > | null ,
2662
+ didIncludeRenderPhaseUpdate : boolean ,
2607
2663
renderPriorityLevel : EventPriority ,
2608
2664
) {
2609
2665
do {
@@ -2683,6 +2739,9 @@ function commitRootImpl(
2683
2739
2684
2740
markRootFinished ( root , remainingLanes ) ;
2685
2741
2742
+ // Reset this before firing side effects so we can detect recursive updates.
2743
+ didIncludeCommitPhaseUpdate = false ;
2744
+
2686
2745
if ( root === workInProgressRoot ) {
2687
2746
// We can reset these now that they are finished.
2688
2747
workInProgressRoot = null ;
@@ -2929,7 +2988,19 @@ function commitRootImpl(
2929
2988
2930
2989
// Read this again, since a passive effect might have updated it
2931
2990
remainingLanes = root . pendingLanes ;
2932
- if ( includesSyncLane ( remainingLanes ) ) {
2991
+ if (
2992
+ // Check if there was a recursive update spawned by this render, in either
2993
+ // the render phase or the commit phase. We track these explicitly because
2994
+ // we can't infer from the remaining lanes alone.
2995
+ didIncludeCommitPhaseUpdate ||
2996
+ didIncludeRenderPhaseUpdate ||
2997
+ // As an additional precaution, we also check if there's any remaining sync
2998
+ // work. Theoretically this should be unreachable but if there's a mistake
2999
+ // in React it helps to be overly defensive given how hard it is to debug
3000
+ // those scenarios otherwise. This won't catch recursive async updates,
3001
+ // though, which is why we check the flags above first.
3002
+ includesSyncLane ( remainingLanes )
3003
+ ) {
2933
3004
if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
2934
3005
markNestedUpdateScheduled ( ) ;
2935
3006
}
0 commit comments