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
73 changes: 42 additions & 31 deletions packages/react-dom/src/__tests__/ReactUpdates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1792,8 +1792,8 @@ describe('ReactUpdates', () => {
expect(subscribers.length).toBe(limit);
});

it("does not infinite loop if there's a synchronous render phase update on another component", async () => {
if (gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
it("warns about potential infinite loop if there's a synchronous render phase update on another component", async () => {
if (!__DEV__ || gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
return;
}
let setState;
Expand All @@ -1809,22 +1809,29 @@ describe('ReactUpdates', () => {
return null;
}

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await expect(async () => {
await act(() => ReactDOM.flushSync(() => root.render(<App />)));
}).rejects.toThrow('Maximum update depth exceeded');
assertConsoleErrorDev([
'Cannot update a component (`App`) while rendering a different component (`Child`). ' +
'To locate the bad setState() call inside `Child`, ' +
'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
' in App (at **)',
]);
const originalConsoleError = console.error;
console.error = e => {
if (
typeof e === 'string' &&
e.startsWith(
'Maximum update depth exceeded. This could be an infinite loop.',
)
) {
Scheduler.log('stop');
}
};
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
await waitFor(['stop']);
} finally {
console.error = originalConsoleError;
}
});

it("does not infinite loop if there's an async render phase update on another component", async () => {
if (gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
it("warns about potential infinite loop if there's an async render phase update on another component", async () => {
if (!__DEV__ || gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
return;
}
let setState;
Expand All @@ -1840,21 +1847,25 @@ describe('ReactUpdates', () => {
return null;
}

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await expect(async () => {
await act(() => {
React.startTransition(() => root.render(<App />));
});
}).rejects.toThrow('Maximum update depth exceeded');

assertConsoleErrorDev([
'Cannot update a component (`App`) while rendering a different component (`Child`). ' +
'To locate the bad setState() call inside `Child`, ' +
'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
' in App (at **)',
]);
const originalConsoleError = console.error;
console.error = e => {
if (
typeof e === 'string' &&
e.startsWith(
'Maximum update depth exceeded. This could be an infinite loop.',
)
) {
Scheduler.log('stop');
}
};
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
React.startTransition(() => root.render(<App />));
await waitFor(['stop']);
} finally {
console.error = originalConsoleError;
}
});

// TODO: Replace this branch with @gate pragmas
Expand Down
80 changes: 58 additions & 22 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,11 @@ let rootWithNestedUpdates: FiberRoot | null = null;
let isFlushingPassiveEffects = false;
let didScheduleUpdateDuringPassiveEffects = false;

const NO_NESTED_UPDATE = 0;
const NESTED_UPDATE_SYNC_LANE = 1;
const NESTED_UPDATE_PHASE_SPAWN = 2;
let nestedUpdateKind: 0 | 1 | 2 = NO_NESTED_UPDATE;

const NESTED_PASSIVE_UPDATE_LIMIT = 50;
let nestedPassiveUpdateCount: number = 0;
let rootWithPassiveNestedUpdates: FiberRoot | null = null;
Expand Down Expand Up @@ -4313,15 +4318,30 @@ function flushSpawnedWork(): void {
// hydration lanes in this check, because render triggered by selective
// hydration is conceptually not an update.
if (
// Was the finished render the result of an update (not hydration)?
includesSomeLane(lanes, UpdateLanes) &&
// Did it schedule a sync update?
includesSomeLane(remainingLanes, SyncUpdateLanes)
) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
markNestedUpdateScheduled();
}

// Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
if (root === rootWithNestedUpdates) {
nestedUpdateCount++;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
nestedUpdateKind = NESTED_UPDATE_SYNC_LANE;
} else if (
// Check if there was a recursive update spawned by this render, in either
// the render phase or the commit phase. We track these explicitly because
// we can't infer from the remaining lanes alone.
(enableInfiniteRenderLoopDetection &&
(didIncludeRenderPhaseUpdate || didIncludeCommitPhaseUpdate)) ||
// Was the finished render the result of an update (not hydration)?
(includesSomeLane(lanes, UpdateLanes) &&
// Did it schedule a sync update?
includesSomeLane(remainingLanes, SyncUpdateLanes))
enableInfiniteRenderLoopDetection &&
(didIncludeRenderPhaseUpdate || didIncludeCommitPhaseUpdate)
) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
markNestedUpdateScheduled();
Expand All @@ -4335,8 +4355,11 @@ function flushSpawnedWork(): void {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
nestedUpdateKind = NESTED_UPDATE_PHASE_SPAWN;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = null;
nestedUpdateKind = NO_NESTED_UPDATE;
}

if (enableProfilerTimer && enableComponentPerformanceTrack) {
Expand Down Expand Up @@ -5152,25 +5175,38 @@ export function throwIfInfiniteUpdateLoopDetected() {
rootWithNestedUpdates = null;
rootWithPassiveNestedUpdates = null;

if (enableInfiniteRenderLoopDetection) {
if (executionContext & RenderContext && workInProgressRoot !== null) {
// We're in the render phase. Disable the concurrent error recovery
// mechanism to ensure that the error we're about to throw gets handled.
// We need it to trigger the nearest error boundary so that the infinite
// update loop is broken.
workInProgressRoot.errorRecoveryDisabledLanes = mergeLanes(
workInProgressRoot.errorRecoveryDisabledLanes,
workInProgressRootRenderLanes,
if (nestedUpdateKind === NESTED_UPDATE_SYNC_LANE) {
if (
enableInfiniteRenderLoopDetection &&
executionContext & RenderContext &&
workInProgressRoot !== null
) {
if (__DEV__) {
console.error(
'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
'causing infinite render loop. React limits the number of nested updates to ' +
'prevent infinite loops.',
);
}
} else {
throw new Error(
'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.',
);
}
} else if (nestedUpdateKind === NESTED_UPDATE_PHASE_SPAWN) {
if (__DEV__) {
console.error(
'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
'causing infinite render loop. React limits the number of nested updates to ' +
'prevent infinite loops.',
);
}
}

throw new Error(
'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.',
);
}

if (__DEV__) {
Expand Down
Loading