Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions packages/react-dom/src/__tests__/ReactUpdates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1884,7 +1884,7 @@ describe('ReactUpdates', () => {

// TODO: Replace this branch with @gate pragmas
if (__DEV__) {
it('warns about a deferred infinite update loop with useEffect', async () => {
it('throws on deferred infinite update loop with useEffect and includes owner stack', async () => {
function NonTerminating() {
const [step, setStep] = React.useState(0);
React.useEffect(function myEffect() {
Expand All @@ -1897,29 +1897,23 @@ describe('ReactUpdates', () => {
return <NonTerminating />;
}

let error = null;
let caughtError = null;
let ownerStack = null;
let debugStack = null;
const originalConsoleError = console.error;
console.error = e => {
error = e;
ownerStack = React.captureOwnerStack();
debugStack = new Error().stack;
Scheduler.log('stop');
};
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container, {
onUncaughtError: (error, errorInfo) => {
caughtError = error;
ownerStack = errorInfo.componentStack;
},
});
await act(async () => {
root.render(<App />);
await waitFor(['stop']);
} finally {
console.error = originalConsoleError;
}
});

expect(error).toContain('Maximum update depth exceeded');
// The currently executing effect should be on the native stack
expect(debugStack).toContain('at myEffect');
expect(ownerStack).toContain('at App');
expect(caughtError).not.toBeNull();
expect(caughtError.message).toContain('Maximum update depth exceeded');
// Owner stack should include the component that scheduled the looping effect
expect(ownerStack).toContain('NonTerminating');
});

it('can have nested updates if they do not cross the limit', async () => {
Expand Down Expand Up @@ -1975,6 +1969,34 @@ describe('ReactUpdates', () => {
});
}

it('throws on deferred infinite update loop with useEffect in production', async () => {
function NonTerminating() {
const [step, setStep] = React.useState(0);
React.useEffect(function myEffect() {
setStep(x => x + 1);
});
return step;
}

const container = document.createElement('div');
const errors = [];
const root = ReactDOMClient.createRoot(container, {
onUncaughtError: (error, errorInfo) => {
errors.push(error.message);
},
});
await act(async () => {
root.render(<NonTerminating />);
});

expect(errors).toEqual([
'Maximum update depth exceeded. This can happen when a component ' +
"calls setState inside useEffect, but useEffect either doesn't " +
'have a dependency array, or one of the dependencies changes on ' +
'every render.',
]);
});

it('prevents infinite update loop triggered by synchronous updates in useEffect', async () => {
// Ignore flushSync warning
spyOnDev(console, 'error').mockImplementation(() => {});
Expand Down
20 changes: 9 additions & 11 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -5263,18 +5263,16 @@ export function throwIfInfiniteUpdateLoopDetected(
}
}

if (__DEV__) {
if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) {
nestedPassiveUpdateCount = 0;
rootWithPassiveNestedUpdates = null;
if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) {
nestedPassiveUpdateCount = 0;
rootWithPassiveNestedUpdates = null;

console.error(
'Maximum update depth exceeded. This can happen when a component ' +
"calls setState inside useEffect, but useEffect either doesn't " +
'have a dependency array, or one of the dependencies changes on ' +
'every render.',
);
}
throw new Error(
'Maximum update depth exceeded. This can happen when a component ' +
"calls setState inside useEffect, but useEffect either doesn't " +
'have a dependency array, or one of the dependencies changes on ' +
'every render.',
);
}
}

Expand Down