@@ -40,6 +40,7 @@ import {
4040 useModernStrictMode ,
4141 disableLegacyContext ,
4242 alwaysThrottleRetries ,
43+ enableInfiniteRenderLoopDetection ,
4344} from 'shared/ReactFeatureFlags' ;
4445import ReactSharedInternals from 'shared/ReactSharedInternals' ;
4546import is from 'shared/objectIs' ;
@@ -147,10 +148,10 @@ import {
147148 getNextLanes ,
148149 getEntangledLanes ,
149150 getLanesToRetrySynchronouslyOnError ,
150- markRootUpdated ,
151- markRootSuspended as markRootSuspended_dontCallThisOneDirectly ,
152- markRootPinged ,
153151 upgradePendingLanesToSync ,
152+ markRootSuspended as _markRootSuspended ,
153+ markRootUpdated as _markRootUpdated ,
154+ markRootPinged as _markRootPinged ,
154155 markRootFinished ,
155156 addFiberToLanesMap ,
156157 movePendingFibersToMemoized ,
@@ -381,6 +382,13 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
381382let workInProgressRootRecoverableErrors : Array < CapturedValue < mixed >> | null =
382383 null ;
383384
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+
384392// The most recent time we either committed a fallback, or when a fallback was
385393// filled in with the resolved UI. This lets us throttle the appearance of new
386394// content as it streams in, to minimize jank.
@@ -1154,6 +1162,7 @@ function finishConcurrentRender(
11541162 root ,
11551163 workInProgressRootRecoverableErrors ,
11561164 workInProgressTransitions ,
1165+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
11571166 workInProgressDeferredLane ,
11581167 ) ;
11591168 } else {
@@ -1189,6 +1198,7 @@ function finishConcurrentRender(
11891198 finishedWork ,
11901199 workInProgressRootRecoverableErrors ,
11911200 workInProgressTransitions ,
1201+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
11921202 lanes ,
11931203 workInProgressDeferredLane ,
11941204 ) ,
@@ -1202,6 +1212,7 @@ function finishConcurrentRender(
12021212 finishedWork ,
12031213 workInProgressRootRecoverableErrors ,
12041214 workInProgressTransitions ,
1215+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
12051216 lanes ,
12061217 workInProgressDeferredLane ,
12071218 ) ;
@@ -1213,6 +1224,7 @@ function commitRootWhenReady(
12131224 finishedWork : Fiber ,
12141225 recoverableErrors : Array < CapturedValue < mixed >> | null ,
12151226 transitions : Array < Transition > | null ,
1227+ didIncludeRenderPhaseUpdate : boolean ,
12161228 lanes : Lanes ,
12171229 spawnedLane : Lane ,
12181230) {
@@ -1240,15 +1252,27 @@ function commitRootWhenReady(
12401252 // us that it's ready. This will be canceled if we start work on the
12411253 // root again.
12421254 root . cancelPendingCommit = schedulePendingCommit (
1243- commitRoot . bind ( null , root , recoverableErrors , transitions ) ,
1255+ commitRoot . bind (
1256+ null ,
1257+ root ,
1258+ recoverableErrors ,
1259+ transitions ,
1260+ didIncludeRenderPhaseUpdate ,
1261+ ) ,
12441262 ) ;
12451263 markRootSuspended ( root , lanes , spawnedLane ) ;
12461264 return ;
12471265 }
12481266 }
12491267
12501268 // Otherwise, commit immediately.
1251- commitRoot ( root , recoverableErrors , transitions , spawnedLane ) ;
1269+ commitRoot (
1270+ root ,
1271+ recoverableErrors ,
1272+ transitions ,
1273+ didIncludeRenderPhaseUpdate ,
1274+ spawnedLane ,
1275+ ) ;
12521276}
12531277
12541278function isRenderConsistentWithExternalStores ( finishedWork : Fiber ) : boolean {
@@ -1304,21 +1328,59 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
13041328 return true ;
13051329}
13061330
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+
13071371function markRootSuspended (
13081372 root : FiberRoot ,
13091373 suspendedLanes : Lanes ,
13101374 spawnedLane : Lane ,
13111375) {
13121376 // When suspending, we should always exclude lanes that were pinged or (more
13131377 // 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 :)
13161378 suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
13171379 suspendedLanes = removeLanes (
13181380 suspendedLanes ,
13191381 workInProgressRootInterleavedUpdatedLanes ,
13201382 ) ;
1321- markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes , spawnedLane ) ;
1383+ _markRootSuspended ( root , suspendedLanes , spawnedLane ) ;
13221384}
13231385
13241386// This is the entry point for synchronous tasks that don't go
@@ -1391,6 +1453,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
13911453 root ,
13921454 workInProgressRootRecoverableErrors ,
13931455 workInProgressTransitions ,
1456+ workInProgressRootDidIncludeRecursiveRenderUpdate ,
13941457 workInProgressDeferredLane ,
13951458 ) ;
13961459
@@ -1607,6 +1670,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
16071670 workInProgressDeferredLane = NoLane ;
16081671 workInProgressRootConcurrentErrors = null ;
16091672 workInProgressRootRecoverableErrors = null ;
1673+ workInProgressRootDidIncludeRecursiveRenderUpdate = false ;
16101674
16111675 // Get the lanes that are entangled with whatever we're about to render. We
16121676 // track these separately so we can distinguish the priority of the render
@@ -2675,6 +2739,7 @@ function commitRoot(
26752739 root : FiberRoot ,
26762740 recoverableErrors : null | Array < CapturedValue < mixed >> ,
26772741 transitions : Array < Transition > | null ,
2742+ didIncludeRenderPhaseUpdate : boolean ,
26782743 spawnedLane : Lane ,
26792744) {
26802745 // TODO: This no longer makes any sense. We already wrap the mutation and
@@ -2689,6 +2754,7 @@ function commitRoot(
26892754 root ,
26902755 recoverableErrors ,
26912756 transitions ,
2757+ didIncludeRenderPhaseUpdate ,
26922758 previousUpdateLanePriority ,
26932759 spawnedLane ,
26942760 ) ;
@@ -2704,6 +2770,7 @@ function commitRootImpl(
27042770 root : FiberRoot ,
27052771 recoverableErrors : null | Array < CapturedValue < mixed >> ,
27062772 transitions : Array < Transition > | null ,
2773+ didIncludeRenderPhaseUpdate : boolean ,
27072774 renderPriorityLevel : EventPriority ,
27082775 spawnedLane : Lane ,
27092776) {
@@ -2784,6 +2851,9 @@ function commitRootImpl(
27842851
27852852 markRootFinished ( root , remainingLanes , spawnedLane ) ;
27862853
2854+ // Reset this before firing side effects so we can detect recursive updates.
2855+ didIncludeCommitPhaseUpdate = false ;
2856+
27872857 if ( root === workInProgressRoot ) {
27882858 // We can reset these now that they are finished.
27892859 workInProgressRoot = null ;
@@ -3036,10 +3106,15 @@ function commitRootImpl(
30363106 // hydration lanes in this check, because render triggered by selective
30373107 // hydration is conceptually not an update.
30383108 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 ) ) ||
30393114 // 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 ) )
30433118 ) {
30443119 if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
30453120 markNestedUpdateScheduled ( ) ;
@@ -3582,6 +3657,19 @@ export function throwIfInfiniteUpdateLoopDetected() {
35823657 rootWithNestedUpdates = null ;
35833658 rootWithPassiveNestedUpdates = null ;
35843659
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+
35853673 throw new Error (
35863674 'Maximum update depth exceeded. This can happen when a component ' +
35873675 'repeatedly calls setState inside componentWillUpdate or ' +
0 commit comments