@@ -751,6 +751,11 @@ let rootWithNestedUpdates: FiberRoot | null = null;
751751let isFlushingPassiveEffects = false ;
752752let didScheduleUpdateDuringPassiveEffects = false ;
753753
754+ const NO_NESTED_UPDATE = 0 ;
755+ const NESTED_UPDATE_SYNC_LANE = 1 ;
756+ const NESTED_UPDATE_PHASE_SPAWN = 2 ;
757+ let nestedUpdateKind : 0 | 1 | 2 = NO_NESTED_UPDATE ;
758+
754759const NESTED_PASSIVE_UPDATE_LIMIT = 50 ;
755760let nestedPassiveUpdateCount : number = 0 ;
756761let rootWithPassiveNestedUpdates : FiberRoot | null = null ;
@@ -4313,15 +4318,30 @@ function flushSpawnedWork(): void {
43134318 // hydration lanes in this check, because render triggered by selective
43144319 // hydration is conceptually not an update.
43154320 if (
4321+ // Was the finished render the result of an update (not hydration)?
4322+ includesSomeLane ( lanes , UpdateLanes ) &&
4323+ // Did it schedule a sync update?
4324+ includesSomeLane ( remainingLanes , SyncUpdateLanes )
4325+ ) {
4326+ if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
4327+ markNestedUpdateScheduled ( ) ;
4328+ }
4329+
4330+ // Count the number of times the root synchronously re-renders without
4331+ // finishing. If there are too many, it indicates an infinite update loop.
4332+ if ( root === rootWithNestedUpdates ) {
4333+ nestedUpdateCount ++ ;
4334+ } else {
4335+ nestedUpdateCount = 0 ;
4336+ rootWithNestedUpdates = root ;
4337+ }
4338+ nestedUpdateKind = NESTED_UPDATE_SYNC_LANE ;
4339+ } else if (
43164340 // Check if there was a recursive update spawned by this render, in either
43174341 // the render phase or the commit phase. We track these explicitly because
43184342 // we can't infer from the remaining lanes alone.
4319- ( enableInfiniteRenderLoopDetection &&
4320- ( didIncludeRenderPhaseUpdate || didIncludeCommitPhaseUpdate ) ) ||
4321- // Was the finished render the result of an update (not hydration)?
4322- ( includesSomeLane ( lanes , UpdateLanes ) &&
4323- // Did it schedule a sync update?
4324- includesSomeLane ( remainingLanes , SyncUpdateLanes ) )
4343+ enableInfiniteRenderLoopDetection &&
4344+ ( didIncludeRenderPhaseUpdate || didIncludeCommitPhaseUpdate )
43254345 ) {
43264346 if ( enableProfilerTimer && enableProfilerNestedUpdatePhase ) {
43274347 markNestedUpdateScheduled ( ) ;
@@ -4335,8 +4355,11 @@ function flushSpawnedWork(): void {
43354355 nestedUpdateCount = 0 ;
43364356 rootWithNestedUpdates = root ;
43374357 }
4358+ nestedUpdateKind = NESTED_UPDATE_PHASE_SPAWN ;
43384359 } else {
43394360 nestedUpdateCount = 0 ;
4361+ rootWithNestedUpdates = null ;
4362+ nestedUpdateKind = NO_NESTED_UPDATE ;
43404363 }
43414364
43424365 if ( enableProfilerTimer && enableComponentPerformanceTrack ) {
@@ -5152,25 +5175,47 @@ export function throwIfInfiniteUpdateLoopDetected() {
51525175 rootWithNestedUpdates = null ;
51535176 rootWithPassiveNestedUpdates = null ;
51545177
5178+ const updateKind = nestedUpdateKind ;
5179+ nestedUpdateKind = NO_NESTED_UPDATE ;
5180+
51555181 if ( enableInfiniteRenderLoopDetection ) {
5156- if ( executionContext & RenderContext && workInProgressRoot !== null ) {
5157- // We're in the render phase. Disable the concurrent error recovery
5158- // mechanism to ensure that the error we're about to throw gets handled.
5159- // We need it to trigger the nearest error boundary so that the infinite
5160- // update loop is broken.
5161- workInProgressRoot . errorRecoveryDisabledLanes = mergeLanes (
5162- workInProgressRoot . errorRecoveryDisabledLanes ,
5163- workInProgressRootRenderLanes ,
5164- ) ;
5182+ if ( updateKind === NESTED_UPDATE_SYNC_LANE ) {
5183+ if ( executionContext & RenderContext && workInProgressRoot !== null ) {
5184+ // This loop was identified only because of the instrumentation gated with enableInfiniteRenderLoopDetection, warn instead of throwing.
5185+ if ( __DEV__ ) {
5186+ console . error (
5187+ 'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
5188+ 'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
5189+ 'causing infinite render loop. React limits the number of nested updates to ' +
5190+ 'prevent infinite loops.' ,
5191+ ) ;
5192+ }
5193+ } else {
5194+ throw new Error (
5195+ 'Maximum update depth exceeded. This can happen when a component ' +
5196+ 'repeatedly calls setState inside componentWillUpdate or ' +
5197+ 'componentDidUpdate. React limits the number of nested updates to ' +
5198+ 'prevent infinite loops.' ,
5199+ ) ;
5200+ }
5201+ } else if ( updateKind === NESTED_UPDATE_PHASE_SPAWN ) {
5202+ if ( __DEV__ ) {
5203+ console . error (
5204+ 'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
5205+ 'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
5206+ 'causing infinite render loop. React limits the number of nested updates to ' +
5207+ 'prevent infinite loops.' ,
5208+ ) ;
5209+ }
51655210 }
5211+ } else {
5212+ throw new Error (
5213+ 'Maximum update depth exceeded. This can happen when a component ' +
5214+ 'repeatedly calls setState inside componentWillUpdate or ' +
5215+ 'componentDidUpdate. React limits the number of nested updates to ' +
5216+ 'prevent infinite loops.' ,
5217+ ) ;
51665218 }
5167-
5168- throw new Error (
5169- 'Maximum update depth exceeded. This can happen when a component ' +
5170- 'repeatedly calls setState inside componentWillUpdate or ' +
5171- 'componentDidUpdate. React limits the number of nested updates to ' +
5172- 'prevent infinite loops.' ,
5173- ) ;
51745219 }
51755220
51765221 if ( __DEV__ ) {
0 commit comments