Strengthen nested update counter test coverage (#15166)
* Isolate ReactUpdates-test cases This ensures their behavior is consistent when run in isolation, and that they actually test the cases they're describing. * Add coverage for cases where we reset nestedUpdateCounter These cases explicitly verify that we reset the counter in right places. * Add a mutually recursive test case * Add test coverage for useLayoutEffect loop
Dan Abramov committed
Mar 21, 2019 at 14:52 UTC
31518135c25aaa1b5c2799d2a18b6b9e9178409c
1 file changed
+123
packages/react-dom/src/__tests__/ReactUpdates-test.js
+123
@@ -15,6 +15,7 @@ let ReactTestUtils;
15
16
describe('ReactUpdates', () => {
17
beforeEach(() => {
18
+ jest.resetModules();
19
React = require('react');
20
ReactDOM = require('react-dom');
21
ReactTestUtils = require('react-dom/test-utils');
@@ -1311,6 +1312,46 @@ describe('ReactUpdates', () => {
1312
ReactDOM.render(<Foo />, container);
1313
});
1314
1315
+ it('resets the update counter for unrelated updates', () => {
1316
+ const container = document.createElement('div');
1317
+ const ref = React.createRef();
1318
+
1319
+ class EventuallyTerminating extends React.Component {
1320
+ state = {step: 0};
1321
+ componentDidMount() {
1322
+ this.setState({step: 1});
1323
+ }
1324
+ componentDidUpdate() {
1325
+ if (this.state.step < limit) {
1326
+ this.setState({step: this.state.step + 1});
1327
+ }
1328
+ }
1329
+ render() {
1330
+ return this.state.step;
1331
+ }
1332
+ }
1333
+
1334
+ let limit = 55;
1335
+ expect(() => {
1336
+ ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1337
+ }).toThrow('Maximum');
1338
+
1339
+ // Verify that we don't go over the limit if these updates are unrelated.
1340
+ limit -= 10;
1341
+ ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1342
+ expect(container.textContent).toBe(limit.toString());
1343
+ ref.current.setState({step: 0});
1344
+ expect(container.textContent).toBe(limit.toString());
1345
+ ref.current.setState({step: 0});
1346
+ expect(container.textContent).toBe(limit.toString());
1347
+
1348
+ limit += 10;
1349
+ expect(() => {
1350
+ ref.current.setState({step: 0});
1351
+ }).toThrow('Maximum');
1352
+ expect(ref.current).toBe(null);
1353
+ });
1354
+
1355
it('does not fall into an infinite update loop', () => {
1356
class NonTerminating extends React.Component {
1357
state = {step: 0};
@@ -1336,6 +1377,88 @@ describe('ReactUpdates', () => {
1377
}).toThrow('Maximum');
1378
});
1379
1380
+ it('does not fall into an infinite update loop with useLayoutEffect', () => {
1381
+ function NonTerminating() {
1382
+ const [step, setStep] = React.useState(0);
1383
+ React.useLayoutEffect(() => {
1384
+ setStep(x => x + 1);
1385
+ });
1386
+ return step;
1387
+ }
1388
+
1389
+ const container = document.createElement('div');
1390
+ expect(() => {
1391
+ ReactDOM.render(<NonTerminating />, container);
1392
+ }).toThrow('Maximum');
1393
+ });
1394
+
1395
+ it('can recover after falling into an infinite update loop', () => {
1396
+ class NonTerminating extends React.Component {
1397
+ state = {step: 0};
1398
+ componentDidMount() {
1399
+ this.setState({step: 1});
1400
+ }
1401
+ componentDidUpdate() {
1402
+ this.setState({step: 2});
1403
+ }
1404
+ render() {
1405
+ return this.state.step;
1406
+ }
1407
+ }
1408
+
1409
+ class Terminating extends React.Component {
1410
+ state = {step: 0};
1411
+ componentDidMount() {
1412
+ this.setState({step: 1});
1413
+ }
1414
+ render() {
1415
+ return this.state.step;
1416
+ }
1417
+ }
1418
+
1419
+ const container = document.createElement('div');
1420
+ expect(() => {
1421
+ ReactDOM.render(<NonTerminating />, container);
1422
+ }).toThrow('Maximum');
1423
+
1424
+ ReactDOM.render(<Terminating />, container);
1425
+ expect(container.textContent).toBe('1');
1426
+
1427
+ expect(() => {
1428
+ ReactDOM.render(<NonTerminating />, container);
1429
+ }).toThrow('Maximum');
1430
+
1431
+ ReactDOM.render(<Terminating />, container);
1432
+ expect(container.textContent).toBe('1');
1433
+ });
1434
+
1435
+ it('does not fall into mutually recursive infinite update loop with same container', () => {
1436
+ // Note: this test would fail if there were two or more different roots.
1437
+
1438
+ class A extends React.Component {
1439
+ componentDidMount() {
1440
+ ReactDOM.render(<B />, container);
1441
+ }
1442
+ render() {
1443
+ return null;
1444
+ }
1445
+ }
1446
+
1447
+ class B extends React.Component {
1448
+ componentDidMount() {
1449
+ ReactDOM.render(<A />, container);
1450
+ }
1451
+ render() {
1452
+ return null;
1453
+ }
1454
+ }
1455
+
1456
+ const container = document.createElement('div');
1457
+ expect(() => {
1458
+ ReactDOM.render(<A />, container);
1459
+ }).toThrow('Maximum');
1460
+ });
1461
+
1462
it('does not fall into an infinite error loop', () => {
1463
function BadRender() {
1464
throw new Error('error');