@samitouri / QOS-React / commits / 3596e40b39

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