@@ -147,10 +147,10 @@ import {
147147 getNextLanes ,
148148 getEntangledLanes ,
149149 getLanesToRetrySynchronouslyOnError ,
150- markRootUpdated ,
151- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
152- markRootPinged ,
153150 upgradePendingLanesToSync ,
151+ markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
152+ markRootUpdated as _markRootUpdated ,
153+ markRootPinged as _markRootPinged ,
154154 markRootFinished ,
155155 addFiberToLanesMap ,
156156 movePendingFibersToMemoized ,
@@ -381,6 +381,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
381381let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
382382 null ;
383383
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+
384391// The most recent time we either committed a fallback, or when a fallback was
385392// filled in with the resolved UI. This lets us throttle the appearance of new
386393// content as it streams in, to minimize jank.
@@ -1155,6 +1162,7 @@ function finishConcurrentRender(
11551162 workInProgressRootRecoverableErrors ,
11561163 workInProgressTransitions ,
11571164 workInProgressDeferredLane ,
1165+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
11581166 ) ;
11591167 } else {
11601168 if (
@@ -1189,6 +1197,7 @@ function finishConcurrentRender(
11891197 finishedWork ,
11901198 workInProgressRootRecoverableErrors ,
11911199 workInProgressTransitions ,
1200+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
11921201 lanes ,
11931202 workInProgressDeferredLane ,
11941203 ) ,
@@ -1202,6 +1211,7 @@ function finishConcurrentRender(
12021211 finishedWork ,
12031212 workInProgressRootRecoverableErrors ,
12041213 workInProgressTransitions ,
1214+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
12051215 lanes ,
12061216 workInProgressDeferredLane ,
12071217 ) ;
@@ -1213,6 +1223,7 @@ function commitRootWhenReady(
12131223 finishedWork : Fiber ,
12141224 recoverableErrors : Array < CapturedValue < mixed >> | null ,
12151225 transitions : Array < Transition > | null ,
1226+ didIncludeRenderPhaseUpdate : boolean ,
12161227 lanes : Lanes ,
12171228 spawnedLane : Lane ,
12181229) {
@@ -1240,15 +1251,27 @@ function commitRootWhenReady(
12401251 // us that it's ready. This will be canceled if we start work on the
12411252 // root again.
12421253 root . cancelPendingCommit = schedulePendingCommit (
1243- commitRoot . bind ( null , root , recoverableErrors , transitions ) ,
1254+ commitRoot . bind (
1255+ null ,
1256+ root ,
1257+ recoverableErrors ,
1258+ transitions ,
1259+ didIncludeRenderPhaseUpdate ,
1260+ ) ,
12441261 ) ;
12451262 markRootSuspended ( root , lanes , spawnedLane ) ;
12461263 return ;
12471264 }
12481265 }
12491266
12501267 // Otherwise, commit immediately.
1251- commitRoot ( root , recoverableErrors , transitions , spawnedLane ) ;
1268+ commitRoot (
1269+ root ,
1270+ recoverableErrors ,
1271+ transitions ,
1272+ spawnedLane ,
1273+ didIncludeRenderPhaseUpdate ,
1274+ ) ;
12521275}
12531276
12541277function isRenderConsistentWithExternalStores ( finishedWork : Fiber ) : boolean {
@@ -1304,21 +1327,55 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
13041327 return true ;
13051328}
13061329
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+
13071366function markRootSuspended (
13081367 root : FiberRoot ,
13091368 suspendedLanes : Lanes ,
13101369 spawnedLane : Lane ,
13111370) {
13121371 // When suspending, we should always exclude lanes that were pinged or (more
13131372 // 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 :)
13161373 suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
13171374 suspendedLanes = removeLanes (
13181375 suspendedLanes ,
13191376 workInProgressRootInterleavedUpdatedLanes ,
13201377 ) ;
1321- markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes , spawnedLane ) ;
1378+ _markRootSuspended ( root , suspendedLanes , spawnedLane ) ;
13221379}
13231380
13241381// This is the entry point for synchronous tasks that don't go
@@ -1392,6 +1449,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
13921449 workInProgressRootRecoverableErrors ,
13931450 workInProgressTransitions ,
13941451 workInProgressDeferredLane ,
1452+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
13951453 ) ;
13961454
13971455 // Before exiting, make sure there's a callback scheduled for the next
@@ -1607,6 +1665,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
16071665 workInProgressDeferredLane = NoLane ;
16081666 workInProgressRootConcurrentErrors = null ;
16091667 workInProgressRootRecoverableErrors = null ;
1668+ workInProgressRootDidIncludeRecursiveRenderUpdate = false ;
16101669
16111670 // Get the lanes that are entangled with whatever we're about to render. We
16121671 // track these separately so we can distinguish the priority of the render
@@ -2676,6 +2735,7 @@ function commitRoot(
26762735 recoverableErrors : null | Array < CapturedValue < mixed >> ,
26772736 transitions : Array < Transition > | null ,
26782737 spawnedLane : Lane ,
2738+ didIncludeRenderPhaseUpdate : boolean ,
26792739) {
26802740 // TODO: This no longer makes any sense. We already wrap the mutation and
26812741 // layout phases. Should be able to remove.
@@ -2689,6 +2749,7 @@ function commitRoot(
26892749 root ,
26902750 recoverableErrors ,
26912751 transitions ,
2752+ didIncludeRenderPhaseUpdate ,
26922753 previousUpdateLanePriority ,
26932754 spawnedLane ,
26942755 ) ;
@@ -2704,6 +2765,7 @@ function commitRootImpl(
27042765 root : FiberRoot ,
27052766 recoverableErrors : null | Array < CapturedValue < mixed >> ,
27062767 transitions : Array < Transition > | null ,
2768+ didIncludeRenderPhaseUpdate : boolean ,
27072769 renderPriorityLevel : EventPriority ,
27082770 spawnedLane : Lane ,
27092771) {
@@ -2784,6 +2846,9 @@ function commitRootImpl(
27842846
27852847 markRootFinished ( root , remainingLanes , spawnedLane ) ;
27862848
2849+ // Reset this before firing side effects so we can detect recursive updates.
2850+ didIncludeCommitPhaseUpdate = false ;
2851+
27872852 if ( root === workInProgressRoot ) {
27882853 // We can reset these now that they are finished.
27892854 workInProgressRoot = null ;
@@ -3036,10 +3101,15 @@ function commitRootImpl(
30363101 // hydration lanes in this check, because render triggered by selective
30373102 // hydration is conceptually not an update.
30383103 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 ||
30393109 // 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 ) )
30433113 ) {
30443114 if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
30453115 markNestedUpdateScheduled ( ) ;
@@ -3582,6 +3652,17 @@ export function throwIfInfiniteUpdateLoopDetected() {
35823652 rootWithNestedUpdates = null ;
35833653 rootWithPassiveNestedUpdates = null ;
35843654
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+
35853666 throw new Error (
35863667 'Maximum update depth exceeded. This can happen when a component ' +
35873668 'repeatedly calls setState inside componentWillUpdate or ' +
0 commit comments