Skip to content

Commit 40f9658

Browse files
kassensAndyPengc12
authored andcommitted
Revert "Fix: Detect infinite update loops caused by render phase updates (facebook#26625)" (facebook#27027)
This reverts commit 822386f. This broke a number of tests when synced internally. We'll need to investigate the breakages before relanding this.
1 parent 32d0c31 commit 40f9658

File tree

4 files changed

+14
-207
lines changed

4 files changed

+14
-207
lines changed

packages/react-dom/src/__tests__/ReactUpdates-test.js

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,64 +1620,6 @@ describe('ReactUpdates', () => {
16201620
});
16211621
});
16221622

1623-
it("does not infinite loop if there's a synchronous render phase update on another component", () => {
1624-
let setState;
1625-
function App() {
1626-
const [, _setState] = React.useState(0);
1627-
setState = _setState;
1628-
return <Child />;
1629-
}
1630-
1631-
function Child(step) {
1632-
// This will cause an infinite update loop, and a warning in dev.
1633-
setState(n => n + 1);
1634-
return null;
1635-
}
1636-
1637-
const container = document.createElement('div');
1638-
const root = ReactDOMClient.createRoot(container);
1639-
1640-
expect(() => {
1641-
expect(() => ReactDOM.flushSync(() => root.render(<App />))).toThrow(
1642-
'Maximum update depth exceeded',
1643-
);
1644-
}).toErrorDev(
1645-
'Warning: Cannot update a component (`App`) while rendering a different component (`Child`)',
1646-
);
1647-
});
1648-
1649-
it("does not infinite loop if there's an async render phase update on another component", async () => {
1650-
let setState;
1651-
function App() {
1652-
const [, _setState] = React.useState(0);
1653-
setState = _setState;
1654-
return <Child />;
1655-
}
1656-
1657-
function Child(step) {
1658-
// This will cause an infinite update loop, and a warning in dev.
1659-
setState(n => n + 1);
1660-
return null;
1661-
}
1662-
1663-
const container = document.createElement('div');
1664-
const root = ReactDOMClient.createRoot(container);
1665-
1666-
await expect(async () => {
1667-
let error;
1668-
try {
1669-
await act(() => {
1670-
React.startTransition(() => root.render(<App />));
1671-
});
1672-
} catch (e) {
1673-
error = e;
1674-
}
1675-
expect(error.message).toMatch('Maximum update depth exceeded');
1676-
}).toErrorDev(
1677-
'Warning: Cannot update a component (`App`) while rendering a different component (`Child`)',
1678-
);
1679-
});
1680-
16811623
// TODO: Replace this branch with @gate pragmas
16821624
if (__DEV__) {
16831625
it('warns about a deferred infinite update loop with useEffect', async () => {

packages/react-reconciler/src/ReactFiberRootScheduler.js

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,6 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
139139
}
140140
}
141141

142-
function unscheduleAllRoots() {
143-
// This is only done in a fatal error situation, as a last resort to prevent
144-
// an infinite render loop.
145-
let root = firstScheduledRoot;
146-
while (root !== null) {
147-
const next = root.next;
148-
root.next = null;
149-
root = next;
150-
}
151-
firstScheduledRoot = lastScheduledRoot = null;
152-
}
153-
154142
export function flushSyncWorkOnAllRoots() {
155143
// This is allowed to be called synchronously, but the caller should check
156144
// the execution context first.
@@ -181,47 +169,10 @@ function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
181169

182170
// There may or may not be synchronous work scheduled. Let's check.
183171
let didPerformSomeWork;
184-
let nestedUpdatePasses = 0;
185172
let errors: Array<mixed> | null = null;
186173
isFlushingWork = true;
187174
do {
188175
didPerformSomeWork = false;
189-
190-
// This outer loop re-runs if performing sync work on a root spawns
191-
// additional sync work. If it happens too many times, it's very likely
192-
// caused by some sort of infinite update loop. We already have a loop guard
193-
// in place that will trigger an error on the n+1th update, but it's
194-
// possible for that error to get swallowed if the setState is called from
195-
// an unexpected place, like during the render phase. So as an added
196-
// precaution, we also use a guard here.
197-
//
198-
// Ideally, there should be no known way to trigger this synchronous loop.
199-
// It's really just here as a safety net.
200-
//
201-
// This limit is slightly larger than the one that throws inside setState,
202-
// because that one is preferable because it includes a componens stack.
203-
if (++nestedUpdatePasses > 60) {
204-
// This is a fatal error, so we'll unschedule all the roots.
205-
unscheduleAllRoots();
206-
// TODO: Change this error message to something different to distinguish
207-
// it from the one that is thrown from setState. Those are less fatal
208-
// because they usually will result in the bad component being unmounted,
209-
// and an error boundary being triggered, rather than us having to
210-
// forcibly stop the entire scheduler.
211-
const infiniteUpdateError = new Error(
212-
'Maximum update depth exceeded. This can happen when a component ' +
213-
'repeatedly calls setState inside componentWillUpdate or ' +
214-
'componentDidUpdate. React limits the number of nested updates to ' +
215-
'prevent infinite loops.',
216-
);
217-
if (errors === null) {
218-
errors = [infiniteUpdateError];
219-
} else {
220-
errors.push(infiniteUpdateError);
221-
}
222-
break;
223-
}
224-
225176
let root = firstScheduledRoot;
226177
while (root !== null) {
227178
if (onlyLegacy && root.tag !== LegacyRoot) {

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 9 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ import {
141141
includesExpiredLane,
142142
getNextLanes,
143143
getLanesToRetrySynchronouslyOnError,
144-
markRootSuspended as _markRootSuspended,
145-
markRootUpdated as _markRootUpdated,
146-
markRootPinged as _markRootPinged,
144+
markRootUpdated,
145+
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
146+
markRootPinged,
147147
markRootEntangled,
148148
markRootFinished,
149149
addFiberToLanesMap,
@@ -370,13 +370,6 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
370370
let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
371371
null;
372372

373-
// Tracks when an update occurs during the render phase.
374-
let workInProgressRootDidIncludeRecursiveRenderUpdate: boolean = false;
375-
// Thacks when an update occurs during the commit phase. It's a separate
376-
// variable from the one for renders because the commit phase may run
377-
// concurrently to a render phase.
378-
let didIncludeCommitPhaseUpdate: boolean = false;
379-
380373
// The most recent time we either committed a fallback, or when a fallback was
381374
// filled in with the resolved UI. This lets us throttle the appearance of new
382375
// content as it streams in, to minimize jank.
@@ -1121,7 +1114,6 @@ function finishConcurrentRender(
11211114
root,
11221115
workInProgressRootRecoverableErrors,
11231116
workInProgressTransitions,
1124-
workInProgressRootDidIncludeRecursiveRenderUpdate,
11251117
);
11261118
} else {
11271119
if (
@@ -1156,7 +1148,6 @@ function finishConcurrentRender(
11561148
finishedWork,
11571149
workInProgressRootRecoverableErrors,
11581150
workInProgressTransitions,
1159-
workInProgressRootDidIncludeRecursiveRenderUpdate,
11601151
lanes,
11611152
),
11621153
msUntilTimeout,
@@ -1169,7 +1160,6 @@ function finishConcurrentRender(
11691160
finishedWork,
11701161
workInProgressRootRecoverableErrors,
11711162
workInProgressTransitions,
1172-
workInProgressRootDidIncludeRecursiveRenderUpdate,
11731163
lanes,
11741164
);
11751165
}
@@ -1180,7 +1170,6 @@ function commitRootWhenReady(
11801170
finishedWork: Fiber,
11811171
recoverableErrors: Array<CapturedValue<mixed>> | null,
11821172
transitions: Array<Transition> | null,
1183-
didIncludeRenderPhaseUpdate: boolean,
11841173
lanes: Lanes,
11851174
) {
11861175
// TODO: Combine retry throttling with Suspensey commits. Right now they run
@@ -1207,21 +1196,15 @@ function commitRootWhenReady(
12071196
// us that it's ready. This will be canceled if we start work on the
12081197
// root again.
12091198
root.cancelPendingCommit = schedulePendingCommit(
1210-
commitRoot.bind(
1211-
null,
1212-
root,
1213-
recoverableErrors,
1214-
transitions,
1215-
didIncludeRenderPhaseUpdate,
1216-
),
1199+
commitRoot.bind(null, root, recoverableErrors, transitions),
12171200
);
12181201
markRootSuspended(root, lanes);
12191202
return;
12201203
}
12211204
}
12221205

12231206
// Otherwise, commit immediately.
1224-
commitRoot(root, recoverableErrors, transitions, didIncludeRenderPhaseUpdate);
1207+
commitRoot(root, recoverableErrors, transitions);
12251208
}
12261209

12271210
function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
@@ -1277,51 +1260,17 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
12771260
return true;
12781261
}
12791262

1280-
// The extra indirections around markRootUpdated and markRootSuspended is
1281-
// needed to avoid a circular dependency between this module and
1282-
// ReactFiberLane. There's probably a better way to split up these modules and
1283-
// avoid this problem. Perhaps all the root-marking functions should move into
1284-
// the work loop.
1285-
1286-
function markRootUpdated(root: FiberRoot, updatedLanes: Lanes) {
1287-
_markRootUpdated(root, updatedLanes);
1288-
1289-
// Check for recursive updates
1290-
if (executionContext & RenderContext) {
1291-
workInProgressRootDidIncludeRecursiveRenderUpdate = true;
1292-
} else if (executionContext & CommitContext) {
1293-
didIncludeCommitPhaseUpdate = true;
1294-
}
1295-
1296-
throwIfInfiniteUpdateLoopDetected();
1297-
}
1298-
1299-
function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
1300-
_markRootPinged(root, pingedLanes);
1301-
1302-
// Check for recursive pings. Pings are conceptually different from updates in
1303-
// other contexts but we call it an "update" in this context because
1304-
// repeatedly pinging a suspended render can cause a recursive render loop.
1305-
// The relevant property is that it can result in a new render attempt
1306-
// being scheduled.
1307-
if (executionContext & RenderContext) {
1308-
workInProgressRootDidIncludeRecursiveRenderUpdate = true;
1309-
} else if (executionContext & CommitContext) {
1310-
didIncludeCommitPhaseUpdate = true;
1311-
}
1312-
1313-
throwIfInfiniteUpdateLoopDetected();
1314-
}
1315-
13161263
function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
13171264
// When suspending, we should always exclude lanes that were pinged or (more
13181265
// rarely, since we try to avoid it) updated during the render phase.
1266+
// TODO: Lol maybe there's a better way to factor this besides this
1267+
// obnoxiously named function :)
13191268
suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes);
13201269
suspendedLanes = removeLanes(
13211270
suspendedLanes,
13221271
workInProgressRootInterleavedUpdatedLanes,
13231272
);
1324-
_markRootSuspended(root, suspendedLanes);
1273+
markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
13251274
}
13261275

13271276
// This is the entry point for synchronous tasks that don't go
@@ -1392,7 +1341,6 @@ export function performSyncWorkOnRoot(root: FiberRoot): null {
13921341
root,
13931342
workInProgressRootRecoverableErrors,
13941343
workInProgressTransitions,
1395-
workInProgressRootDidIncludeRecursiveRenderUpdate,
13961344
);
13971345

13981346
// Before exiting, make sure there's a callback scheduled for the next
@@ -1607,7 +1555,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
16071555
workInProgressRootPingedLanes = NoLanes;
16081556
workInProgressRootConcurrentErrors = null;
16091557
workInProgressRootRecoverableErrors = null;
1610-
workInProgressRootDidIncludeRecursiveRenderUpdate = false;
16111558

16121559
finishQueueingConcurrentUpdates();
16131560

@@ -2649,7 +2596,6 @@ function commitRoot(
26492596
root: FiberRoot,
26502597
recoverableErrors: null | Array<CapturedValue<mixed>>,
26512598
transitions: Array<Transition> | null,
2652-
didIncludeRenderPhaseUpdate: boolean,
26532599
) {
26542600
// TODO: This no longer makes any sense. We already wrap the mutation and
26552601
// layout phases. Should be able to remove.
@@ -2663,7 +2609,6 @@ function commitRoot(
26632609
root,
26642610
recoverableErrors,
26652611
transitions,
2666-
didIncludeRenderPhaseUpdate,
26672612
previousUpdateLanePriority,
26682613
);
26692614
} finally {
@@ -2678,7 +2623,6 @@ function commitRootImpl(
26782623
root: FiberRoot,
26792624
recoverableErrors: null | Array<CapturedValue<mixed>>,
26802625
transitions: Array<Transition> | null,
2681-
didIncludeRenderPhaseUpdate: boolean,
26822626
renderPriorityLevel: EventPriority,
26832627
) {
26842628
do {
@@ -2758,9 +2702,6 @@ function commitRootImpl(
27582702

27592703
markRootFinished(root, remainingLanes);
27602704

2761-
// Reset this before firing side effects so we can detect recursive updates.
2762-
didIncludeCommitPhaseUpdate = false;
2763-
27642705
if (root === workInProgressRoot) {
27652706
// We can reset these now that they are finished.
27662707
workInProgressRoot = null;
@@ -3007,19 +2948,7 @@ function commitRootImpl(
30072948

30082949
// Read this again, since a passive effect might have updated it
30092950
remainingLanes = root.pendingLanes;
3010-
if (
3011-
// Check if there was a recursive update spawned by this render, in either
3012-
// the render phase or the commit phase. We track these explicitly because
3013-
// we can't infer from the remaining lanes alone.
3014-
didIncludeCommitPhaseUpdate ||
3015-
didIncludeRenderPhaseUpdate ||
3016-
// As an additional precaution, we also check if there's any remaining sync
3017-
// work. Theoretically this should be unreachable but if there's a mistake
3018-
// in React it helps to be overly defensive given how hard it is to debug
3019-
// those scenarios otherwise. This won't catch recursive async updates,
3020-
// though, which is why we check the flags above first.
3021-
includesSyncLane(remainingLanes)
3022-
) {
2951+
if (includesSyncLane(remainingLanes)) {
30232952
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
30242953
markNestedUpdateScheduled();
30252954
}
@@ -3561,17 +3490,6 @@ export function throwIfInfiniteUpdateLoopDetected() {
35613490
rootWithNestedUpdates = null;
35623491
rootWithPassiveNestedUpdates = null;
35633492

3564-
if (executionContext & RenderContext && workInProgressRoot !== null) {
3565-
// We're in the render phase. Disable the concurrent error recovery
3566-
// mechanism to ensure that the error we're about to throw gets handled.
3567-
// We need it to trigger the nearest error boundary so that the infinite
3568-
// update loop is broken.
3569-
workInProgressRoot.errorRecoveryDisabledLanes = mergeLanes(
3570-
workInProgressRoot.errorRecoveryDisabledLanes,
3571-
workInProgressRootRenderLanes,
3572-
);
3573-
}
3574-
35753493
throw new Error(
35763494
'Maximum update depth exceeded. This can happen when a component ' +
35773495
'repeatedly calls setState inside componentWillUpdate or ' +

scripts/jest/matchers/toWarnDev.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,12 @@ const createMatcherFor = (consoleMethod, matcherName) =>
6969
(message.includes('\n in ') || message.includes('\n at '));
7070

7171
const consoleSpy = (format, ...args) => {
72+
// Ignore uncaught errors reported by jsdom
73+
// and React addendums because they're too noisy.
7274
if (
73-
// Ignore uncaught errors reported by jsdom
74-
// and React addendums because they're too noisy.
75-
(!logAllErrors &&
76-
consoleMethod === 'error' &&
77-
shouldIgnoreConsoleError(format, args)) ||
78-
// Ignore error objects passed to console.error, which we sometimes
79-
// use as a fallback behavior, like when reportError
80-
// isn't available.
81-
typeof format !== 'string'
75+
!logAllErrors &&
76+
consoleMethod === 'error' &&
77+
shouldIgnoreConsoleError(format, args)
8278
) {
8379
return;
8480
}

0 commit comments

Comments
 (0)