From 76a8c9e9fee6aa0931e77ec9ae25273d3a1abec7 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 2 Jul 2025 16:08:06 -0400 Subject: [PATCH 1/3] [tests] Test component stack for Maximum Update error --- .../src/__tests__/ReactUpdates-test.js | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index bc7767b12efd..a0d4f595f470 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -20,6 +20,19 @@ let waitFor; let assertLog; let assertConsoleErrorDev; +function normalizeCodeLocInfo(str) { + return ( + str && + str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) { + const dot = name.lastIndexOf('.'); + if (dot !== -1) { + name = name.slice(dot + 1); + } + return ' in ' + name + (/\d/.test(m) ? ' (at **)' : ''); + }) + ); +} + describe('ReactUpdates', () => { beforeEach(() => { jest.resetModules(); @@ -1972,13 +1985,24 @@ describe('ReactUpdates', () => { } const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - ReactDOM.flushSync(() => { - root.render(); - }); + let errors = []; + const root = ReactDOMClient.createRoot(container, { + onUncaughtError: (error, errorInfo) => { + errors.push( + error.message, + normalizeCodeLocInfo(errorInfo.componentStack), + ); + }, + }); + await act(() => { + ReactDOM.flushSync(() => { + root.render(); }); - }).rejects.toThrow('Maximum update depth exceeded'); + }); + + expect(errors).toEqual([ + 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.', + '\n in NonTerminating (at **)', + ]); }); }); From 13ccc06c74f82bcea186eb63b8da9ce64e4160f6 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 2 Jul 2025 16:24:42 -0400 Subject: [PATCH 2/3] add ref test --- .../src/__tests__/ReactUpdates-test.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index a0d4f595f470..157acaa1c9d9 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -2005,4 +2005,44 @@ describe('ReactUpdates', () => { '\n in NonTerminating (at **)', ]); }); + + it('prevents infinite update loop triggered by too many updates in ref callbacks', async () => { + // Ignore flushSync warning + spyOnDev(console, 'error').mockImplementation(() => {}); + + let scheduleUpdate; + function NonTerminating() { + const [_, _scheduleUpdate] = React.useReducer(count => count + 1, 0); + scheduleUpdate = _scheduleUpdate; + + return ( +
{ + for (let i = 0; i < 50; i++) { + scheduleUpdate(1); + } + }} + /> + ); + } + + const container = document.createElement('div'); + let errors = []; + const root = ReactDOMClient.createRoot(container, { + onUncaughtError: (error, errorInfo) => { + errors.push( + error.message, + normalizeCodeLocInfo(errorInfo.componentStack), + ); + }, + }); + await act(() => { + root.render(); + }); + + expect(errors).toEqual([ + 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.', + '\n in div' + '\n in NonTerminating (at **)', + ]); + }); }); From c93422f75cdc70207b867411e66b0668ab7afb8f Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 7 Jul 2025 11:22:18 -0400 Subject: [PATCH 3/3] cleanup --- .../src/__tests__/ReactUpdates-test.js | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 157acaa1c9d9..2bfab445cc3b 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1985,12 +1985,11 @@ describe('ReactUpdates', () => { } const container = document.createElement('div'); - let errors = []; + const errors = []; const root = ReactDOMClient.createRoot(container, { onUncaughtError: (error, errorInfo) => { errors.push( - error.message, - normalizeCodeLocInfo(errorInfo.componentStack), + `${error.message}${normalizeCodeLocInfo(errorInfo.componentStack)}`, ); }, }); @@ -2001,18 +2000,17 @@ describe('ReactUpdates', () => { }); expect(errors).toEqual([ - 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.', - '\n in NonTerminating (at **)', + 'Maximum update depth exceeded. ' + + 'This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ' + + 'React limits the number of nested updates to prevent infinite loops.' + + '\n in NonTerminating (at **)', ]); }); it('prevents infinite update loop triggered by too many updates in ref callbacks', async () => { - // Ignore flushSync warning - spyOnDev(console, 'error').mockImplementation(() => {}); - let scheduleUpdate; - function NonTerminating() { - const [_, _scheduleUpdate] = React.useReducer(count => count + 1, 0); + function TooManyRefUpdates() { + const [count, _scheduleUpdate] = React.useReducer(c => c + 1, 0); scheduleUpdate = _scheduleUpdate; return ( @@ -2021,28 +2019,31 @@ describe('ReactUpdates', () => { for (let i = 0; i < 50; i++) { scheduleUpdate(1); } - }} - /> + }}> + {count} +
); } const container = document.createElement('div'); - let errors = []; + const errors = []; const root = ReactDOMClient.createRoot(container, { onUncaughtError: (error, errorInfo) => { errors.push( - error.message, - normalizeCodeLocInfo(errorInfo.componentStack), + `${error.message}${normalizeCodeLocInfo(errorInfo.componentStack)}`, ); }, }); await act(() => { - root.render(); + root.render(); }); expect(errors).toEqual([ - 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.', - '\n in div' + '\n in NonTerminating (at **)', + 'Maximum update depth exceeded. ' + + 'This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ' + + 'React limits the number of nested updates to prevent infinite loops.' + + '\n in div' + + '\n in TooManyRefUpdates (at **)', ]); }); });