Revert "Fix: Detect infinite update loops caused by render phase updates (#26625)" (#27027)
This reverts commit 822386f252fd1f0e949efa904a1ed790133329f7. This broke a number of tests when synced internally. We'll need to investigate the breakages before relanding this.
Jan Kassens committed
Jun 30, 2023 at 12:51 UTC
7f362de1588d98438787d652941533e21f2f332d
4 files changed
+14
-207
packages/react-dom/src/__tests__/ReactUpdates-test.js
-58
@@ -1620,64 +1620,6 @@ describe('ReactUpdates', () => {
1620
});
1621
});
1622
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
-
1623
// TODO: Replace this branch with @gate pragmas
1624
if (__DEV__) {
1625
it('warns about a deferred infinite update loop with useEffect', async () => {
packages/react-reconciler/src/ReactFiberRootScheduler.js
-49
@@ -139,18 +139,6 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
139
}
140
}
141
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
-
142
export function flushSyncWorkOnAllRoots() {
143
// This is allowed to be called synchronously, but the caller should check
144
// the execution context first.
@@ -181,47 +169,10 @@ function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
169
170
// There may or may not be synchronous work scheduled. Let's check.
171
let didPerformSomeWork;
184
- let nestedUpdatePasses = 0;
172
let errors: Array<mixed> | null = null;
173
isFlushingWork = true;
174
do {
175
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
-
176
let root = firstScheduledRoot;
177
while (root !== null) {
178
if (onlyLegacy && root.tag !== LegacyRoot) {
packages/react-reconciler/src/ReactFiberWorkLoop.js
+9
-91
@@ -141,9 +141,9 @@ import {
141
includesExpiredLane,
142
getNextLanes,
143
getLanesToRetrySynchronouslyOnError,
144
- markRootSuspended as _markRootSuspended,
145
- markRootUpdated as _markRootUpdated,
146
- markRootPinged as _markRootPinged,
144
+ markRootUpdated,
145
+ markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
146
+ markRootPinged,
147
markRootEntangled,
148
markRootFinished,
149
addFiberToLanesMap,
@@ -370,13 +370,6 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
370
let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
371
null;
372
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
-
373
// The most recent time we either committed a fallback, or when a fallback was
374
// filled in with the resolved UI. This lets us throttle the appearance of new
375
// content as it streams in, to minimize jank.
@@ -1121,7 +1114,6 @@ function finishConcurrentRender(
1114
root,
1115
workInProgressRootRecoverableErrors,
1116
workInProgressTransitions,
1124
- workInProgressRootDidIncludeRecursiveRenderUpdate,
1117
);
1118
} else {
1119
if (
@@ -1156,7 +1148,6 @@ function finishConcurrentRender(
1148
finishedWork,
1149
workInProgressRootRecoverableErrors,
1150
workInProgressTransitions,
1159
- workInProgressRootDidIncludeRecursiveRenderUpdate,
1151
lanes,
1152
),
1153
msUntilTimeout,
@@ -1169,7 +1160,6 @@ function finishConcurrentRender(
1160
finishedWork,
1161
workInProgressRootRecoverableErrors,
1162
workInProgressTransitions,
1172
- workInProgressRootDidIncludeRecursiveRenderUpdate,
1163
lanes,
1164
);
1165
}
@@ -1180,7 +1170,6 @@ function commitRootWhenReady(
1170
finishedWork: Fiber,
1171
recoverableErrors: Array<CapturedValue<mixed>> | null,
1172
transitions: Array<Transition> | null,
1183
- didIncludeRenderPhaseUpdate: boolean,
1173
lanes: Lanes,
1174
) {
1175
// TODO: Combine retry throttling with Suspensey commits. Right now they run
@@ -1207,13 +1196,7 @@ function commitRootWhenReady(
1196
// us that it's ready. This will be canceled if we start work on the
1197
// root again.
1198
root.cancelPendingCommit = schedulePendingCommit(
1210
- commitRoot.bind(
1211
- null,
1212
- root,
1213
- recoverableErrors,
1214
- transitions,
1215
- didIncludeRenderPhaseUpdate,
1216
- ),
1199
+ commitRoot.bind(null, root, recoverableErrors, transitions),
1200
);
1201
markRootSuspended(root, lanes);
1202
return;
@@ -1221,7 +1204,7 @@ function commitRootWhenReady(
1204
}
1205
1206
// Otherwise, commit immediately.
1224
- commitRoot(root, recoverableErrors, transitions, didIncludeRenderPhaseUpdate);
1207
+ commitRoot(root, recoverableErrors, transitions);
1208
}
1209
1210
function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
@@ -1277,51 +1260,17 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1260
return true;
1261
}
1262
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
-
1263
function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
1264
// When suspending, we should always exclude lanes that were pinged or (more
1265
// 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 :)
1268
suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes);
1269
suspendedLanes = removeLanes(
1270
suspendedLanes,
1271
workInProgressRootInterleavedUpdatedLanes,
1272
);
1324
- _markRootSuspended(root, suspendedLanes);
1273
+ markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
1274
}
1275
1276
// This is the entry point for synchronous tasks that don't go
@@ -1392,7 +1341,6 @@ export function performSyncWorkOnRoot(root: FiberRoot): null {
1341
root,
1342
workInProgressRootRecoverableErrors,
1343
workInProgressTransitions,
1395
- workInProgressRootDidIncludeRecursiveRenderUpdate,
1344
);
1345
1346
// Before exiting, make sure there's a callback scheduled for the next
@@ -1607,7 +1555,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1555
workInProgressRootPingedLanes = NoLanes;
1556
workInProgressRootConcurrentErrors = null;
1557
workInProgressRootRecoverableErrors = null;
1610
- workInProgressRootDidIncludeRecursiveRenderUpdate = false;
1558
1559
finishQueueingConcurrentUpdates();
1560
@@ -2649,7 +2596,6 @@ function commitRoot(
2596
root: FiberRoot,
2597
recoverableErrors: null | Array<CapturedValue<mixed>>,
2598
transitions: Array<Transition> | null,
2652
- didIncludeRenderPhaseUpdate: boolean,
2599
) {
2600
// TODO: This no longer makes any sense. We already wrap the mutation and
2601
// layout phases. Should be able to remove.
@@ -2663,7 +2609,6 @@ function commitRoot(
2609
root,
2610
recoverableErrors,
2611
transitions,
2666
- didIncludeRenderPhaseUpdate,
2612
previousUpdateLanePriority,
2613
);
2614
} finally {
@@ -2678,7 +2623,6 @@ function commitRootImpl(
2623
root: FiberRoot,
2624
recoverableErrors: null | Array<CapturedValue<mixed>>,
2625
transitions: Array<Transition> | null,
2681
- didIncludeRenderPhaseUpdate: boolean,
2626
renderPriorityLevel: EventPriority,
2627
) {
2628
do {
@@ -2758,9 +2702,6 @@ function commitRootImpl(
2702
2703
markRootFinished(root, remainingLanes);
2704
2761
- // Reset this before firing side effects so we can detect recursive updates.
2762
- didIncludeCommitPhaseUpdate = false;
2763
-
2705
if (root === workInProgressRoot) {
2706
// We can reset these now that they are finished.
2707
workInProgressRoot = null;
@@ -3007,19 +2948,7 @@ function commitRootImpl(
2948
2949
// Read this again, since a passive effect might have updated it
2950
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)) {
2952
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
2953
markNestedUpdateScheduled();
2954
}
@@ -3561,17 +3490,6 @@ export function throwIfInfiniteUpdateLoopDetected() {
3490
rootWithNestedUpdates = null;
3491
rootWithPassiveNestedUpdates = null;
3492
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
-
3493
throw new Error(
3494
'Maximum update depth exceeded. This can happen when a component ' +
3495
'repeatedly calls setState inside componentWillUpdate or ' +
scripts/jest/matchers/toWarnDev.js
+5
-9
@@ -69,16 +69,12 @@ const createMatcherFor = (consoleMethod, matcherName) =>
69
(message.includes('\n in ') || message.includes('\n at '));
70
71
const consoleSpy = (format, ...args) => {
72
+ // Ignore uncaught errors reported by jsdom
73
+ // and React addendums because they're too noisy.
74
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)
78
) {
79
return;
80
}