@@ -2006,6 +2006,114 @@ describe('ReactUpdates', () => {
20062006 ] ) ;
20072007 } ) ;
20082008
2009+ it ( 'warns instead of throwing when infinite Suspense ping loop is detected via enableInfiniteRenderLoopDetection during commit phase' , async ( ) => {
2010+ if ( ! __DEV__ || gate ( flags => ! flags . enableInfiniteRenderLoopDetection ) ) {
2011+ return ;
2012+ }
2013+
2014+ // When a Suspense child throws a thenable, React registers two listeners:
2015+ // 1. ping (attachPingListener, render) → pingSuspendedRoot → markRootPinged
2016+ // 2. retry (attachSuspenseRetryListeners, commit) → resolveRetryWakeable
2017+ //
2018+ // The ping path calls throwIfInfiniteUpdateLoopDetected(true) via
2019+ // markRootPinged WITHOUT a prior getRootForUpdatedFiber(false) check.
2020+ // When this fires during CommitContext (not RenderContext),
2021+ // the isFromInfiniteRenderLoopDetectionInstrumentation=true parameter
2022+ // ensures we warn instead of throw.
2023+ //
2024+ // Without the fix (passing false), the condition
2025+ // false || (executionContext & RenderContext && ...)
2026+ // evaluates to false in CommitContext, causing a throw.
2027+ let currentResolve = null ;
2028+ let shouldStop = false ;
2029+
2030+ function App ( ) {
2031+ const [ , setState ] = React . useState ( 0 ) ;
2032+
2033+ React . useLayoutEffect ( ( ) => {
2034+ if ( shouldStop ) {
2035+ return ;
2036+ }
2037+ // Resolve the suspended thenable during commit phase (CommitContext).
2038+ // The ping callback (registered first during render) fires first,
2039+ // triggering markRootPinged → throwIfInfiniteUpdateLoopDetected(true).
2040+ if ( currentResolve !== null ) {
2041+ const resolve = currentResolve ;
2042+ currentResolve = null ;
2043+ resolve ( ) ;
2044+ }
2045+ // Schedule a sync update to ensure nestedUpdateKind is
2046+ // NESTED_UPDATE_SYNC_LANE at commitRootImpl epilogue.
2047+ setState ( n => n + 1 ) ;
2048+ } ) ;
2049+
2050+ return (
2051+ < React . Suspense fallback = "loading" >
2052+ < SuspendingChild />
2053+ </ React . Suspense >
2054+ ) ;
2055+ }
2056+
2057+ function SuspendingChild ( ) {
2058+ if ( shouldStop ) {
2059+ return null ;
2060+ }
2061+ // Each render throws a new thenable. React calls .then() on it twice
2062+ // (ping during render, retry during commit). We collect all callbacks
2063+ // so resolve() fires them in registration order: ping first.
2064+ const callbacks = [ ] ;
2065+ throw {
2066+ then ( onFulfilled ) {
2067+ callbacks . push ( onFulfilled ) ;
2068+ currentResolve = ( ) => {
2069+ for ( let i = 0 ; i < callbacks . length ; i ++ ) {
2070+ callbacks [ i ] ( ) ;
2071+ }
2072+ } ;
2073+ } ,
2074+ } ;
2075+ }
2076+
2077+ const container = document . createElement ( 'div' ) ;
2078+ const errors = [ ] ;
2079+ const root = ReactDOMClient . createRoot ( container , {
2080+ onUncaughtError : error => {
2081+ errors . push ( error . message ) ;
2082+ } ,
2083+ } ) ;
2084+
2085+ let warning = null ;
2086+ const originalConsoleError = console . error ;
2087+ console . error = e => {
2088+ if (
2089+ typeof e === 'string' &&
2090+ e . startsWith (
2091+ 'Maximum update depth exceeded. This could be an infinite loop.' ,
2092+ )
2093+ ) {
2094+ // Stop the loop after the first warning so act() can finish.
2095+ shouldStop = true ;
2096+ warning = e ;
2097+ }
2098+ } ;
2099+
2100+ try {
2101+ await act ( ( ) => {
2102+ root . render ( < App /> ) ;
2103+ } ) ;
2104+ } finally {
2105+ console . error = originalConsoleError ;
2106+ }
2107+
2108+ // With the fix (throwIfInfiniteUpdateLoopDetected(true) in markRootPinged):
2109+ // the loop is discovered via enableInfiniteRenderLoopDetection instrumentation
2110+ // and produces a warning.
2111+ // Without the fix (throwIfInfiniteUpdateLoopDetected(false)):
2112+ // the same check throws because executionContext is CommitContext, not
2113+ // RenderContext.
2114+ expect ( errors ) . toEqual ( [ ] ) ;
2115+ } ) ;
2116+
20092117 it ( 'prevents infinite update loop triggered by too many updates in ref callbacks' , async ( ) => {
20102118 let scheduleUpdate ;
20112119 function TooManyRefUpdates ( ) {
0 commit comments