Fix nested update bug (#13160)
A recent change to the scheduler caused a regression when scheduling many updates within a single batch. Added a test case that would have caught this.
Andrew Clark committed
Jul 6, 2018 at 13:55 UTC
3596e40b394385713049d996c5ba293f746b7561
2 files changed
+48
-13
packages/react-dom/src/__tests__/ReactUpdates-test.js
+33
@@ -1364,4 +1364,37 @@ describe('ReactUpdates', () => {
1364
ReactDOM.render(<NonTerminating />, container);
1365
}).toThrow('Maximum');
1366
});
1367
+
1368
+ it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', () => {
1369
+ const subscribers = [];
1370
+
1371
+ class Child extends React.Component {
1372
+ state = {value: 'initial'};
1373
+ componentDidMount() {
1374
+ subscribers.push(this);
1375
+ }
1376
+ render() {
1377
+ return null;
1378
+ }
1379
+ }
1380
+
1381
+ class App extends React.Component {
1382
+ render() {
1383
+ const children = [];
1384
+ for (let i = 0; i < 1200; i++) {
1385
+ children.push(<Child key={i} />);
1386
+ }
1387
+ return children;
1388
+ }
1389
+ }
1390
+
1391
+ const container = document.createElement('div');
1392
+ ReactDOM.render(<App />, container);
1393
+
1394
+ ReactDOM.unstable_batchedUpdates(() => {
1395
+ subscribers.forEach(s => {
1396
+ s.setState({value: 'update'});
1397
+ });
1398
+ });
1399
+ });
1400
});
packages/react-reconciler/src/ReactFiberScheduler.js
+15
-13
@@ -1474,6 +1474,8 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1474
requestWork(root, rootExpirationTime);
1475
}
1476
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1477
+ // Reset this back to zero so subsequent updates don't throw.
1478
+ nestedUpdateCount = 0;
1479
invariant(
1480
false,
1481
'Maximum update depth exceeded. This can happen when a ' +
@@ -1544,6 +1546,7 @@ let currentSchedulerTime: ExpirationTime = currentRendererTime;
1546
// Use these to prevent an infinite loop of nested updates
1547
const NESTED_UPDATE_LIMIT = 1000;
1548
let nestedUpdateCount: number = 0;
1549
+let lastCommittedRootDuringThisBatch: FiberRoot | null = null;
1550
1551
const timeHeuristicForUnitOfWork = 1;
1552
@@ -1798,19 +1801,6 @@ function findHighestPriorityRoot() {
1801
}
1802
}
1803
1801
- // If the next root is the same as the previous root, this is a nested
1802
- // update. To prevent an infinite loop, increment the nested update count.
1803
- const previousFlushedRoot = nextFlushedRoot;
1804
- if (
1805
- previousFlushedRoot !== null &&
1806
- previousFlushedRoot === highestPriorityRoot &&
1807
- highestPriorityWork === Sync
1808
- ) {
1809
- nestedUpdateCount++;
1810
- } else {
1811
- // Reset whenever we switch roots.
1812
- nestedUpdateCount = 0;
1813
- }
1804
nextFlushedRoot = highestPriorityRoot;
1805
nextFlushedExpirationTime = highestPriorityWork;
1806
}
@@ -1911,6 +1901,7 @@ function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1901
1902
function finishRendering() {
1903
nestedUpdateCount = 0;
1904
+ lastCommittedRootDuringThisBatch = null;
1905
1906
if (completedBatches !== null) {
1907
const batches = completedBatches;
@@ -2046,6 +2037,17 @@ function completeRoot(
2037
// Commit the root.
2038
root.finishedWork = null;
2039
2040
+ // Check if this is a nested update (a sync update scheduled during the
2041
+ // commit phase).
2042
+ if (root === lastCommittedRootDuringThisBatch) {
2043
+ // If the next root is the same as the previous root, this is a nested
2044
+ // update. To prevent an infinite loop, increment the nested update count.
2045
+ nestedUpdateCount++;
2046
+ } else {
2047
+ // Reset whenever we switch roots.
2048
+ lastCommittedRootDuringThisBatch = root;
2049
+ nestedUpdateCount = 0;
2050
+ }
2051
commitRoot(root, finishedWork);
2052
}
2053