Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Disable error recovery mechanism if infinite loop is detected
If an infinite update loop is caused by a render phase update, the
mechanism we typically use to break the loop doesn't work. Our mechanism
assumes that by throwing inside `setState`, the error will caise the
component to be unmounted, but that only works if the error happens in
an effect or lifecycle method. During the render phase, what happens is
that React will try to render the component one more time,
synchronously, which we do as a way to recover from concurrent data
races. But during this second attempt, the "Maximum update" error won't
be thrown, because the counter was already reset.

I considered a few different ways to fix this, like waiting to reset the
counter until after the error has been surfaced. However, it's not
obvious where this should happen. So instead the approach I landed on is
to temporarily disable the error recovery mechanism. This is the same
trick we use to prevent infinite ping loops when an uncached promise is
passed to `use` during a sync render.

This category of error is also covered by the more generic loop guard I
added in the previous commit, but I also confirmed that this change
alone fixes it.
  • Loading branch information
acdlite committed Jun 27, 2023
commit 6adc321b03617de33b6eab83a212eb29e8ce3a14
22 changes: 21 additions & 1 deletion packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
}
}

function unscheduleAllRoots() {
// This is only done in a fatal error situation, as a last resort to prevent
// an infinite render loop.
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
root.next = null;
root = next;
}
firstScheduledRoot = lastScheduledRoot = null;
}

export function flushSyncWorkOnAllRoots() {
// This is allowed to be called synchronously, but the caller should check
// the execution context first.
Expand Down Expand Up @@ -183,11 +195,19 @@ function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
// an unexpected place, like during the render phase. So as an added
// precaution, we also use a guard here.
//
// Ideally, there should be no known way to trigger this synchronous loop.
// It's really just here as a safety net.
//
// This limit is slightly larger than the one that throws inside setState,
// because that one is preferable because it includes a componens stack.
if (++nestedUpdatePasses > 60) {
// This is a fatal error, so we'll unschedule all the roots.
firstScheduledRoot = lastScheduledRoot = null;
unscheduleAllRoots();
// TODO: Change this error message to something different to distinguish
// it from the one that is thrown from setState. Those are less fatal
// because they usually will result in the bad component being unmounted,
// and an error boundary being triggered, rather than us having to
// forcibly stop the entire scheduler.
const infiniteUpdateError = new Error(
'Maximum update depth exceeded. This can happen when a component ' +
'repeatedly calls setState inside componentWillUpdate or ' +
Expand Down
11 changes: 11 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -3471,6 +3471,17 @@ export function throwIfInfiniteUpdateLoopDetected() {
rootWithNestedUpdates = null;
rootWithPassiveNestedUpdates = null;

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,
);
}

throw new Error(
'Maximum update depth exceeded. This can happen when a component ' +
'repeatedly calls setState inside componentWillUpdate or ' +
Expand Down