diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index bc7767b12efd1..2bfab445cc3b5 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,65 @@ describe('ReactUpdates', () => { } const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - ReactDOM.flushSync(() => { - root.render(); - }); + const 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 **)', + ]); + }); + + it('prevents infinite update loop triggered by too many updates in ref callbacks', async () => { + let scheduleUpdate; + function TooManyRefUpdates() { + const [count, _scheduleUpdate] = React.useReducer(c => c + 1, 0); + scheduleUpdate = _scheduleUpdate; + + return ( +
{ + for (let i = 0; i < 50; i++) { + scheduleUpdate(1); + } + }}> + {count} +
+ ); + } + + const container = document.createElement('div'); + const 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 TooManyRefUpdates (at **)', + ]); }); });