Update ReactUpdates-test (#28061)
## Overview These tests are important for `ReactDOM.render`, so instead of just re-writing them to `createRoot` and losing coverage: - Moved the `.render` tests to `ReactLegacyUpdates` - Re-wrote the tests in `ReactUpdates` to use `createRoot` - Remove `unstable_batchedUpdates` from `ReactUpdates` In a future PR, when I flag `batchedUpdates` with a Noop, I can add the gate to just the tests in `ReactLegacyUpdates`.
Ricky committed
Jan 25, 2024 at 01:17 UTC
8bb6ee1d33ca6c7e34342bc4b17aac0449ab6899
2 files changed
+2158
-475
packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js
new
+1591
@@ -0,0 +1,1591 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React;
13
+let ReactDOM;
14
+let ReactTestUtils;
15
+let act;
16
+let Scheduler;
17
+let assertLog;
18
+
19
+// Copy of ReactUpdates using ReactDOM.render and ReactDOM.unstable_batchedUpdates.
20
+// Can be deleted when we remove both.
21
+describe('ReactLegacyUpdates', () => {
22
+ beforeEach(() => {
23
+ jest.resetModules();
24
+ React = require('react');
25
+ ReactDOM = require('react-dom');
26
+ ReactTestUtils = require('react-dom/test-utils');
27
+ act = require('internal-test-utils').act;
28
+ Scheduler = require('scheduler');
29
+
30
+ const InternalTestUtils = require('internal-test-utils');
31
+ assertLog = InternalTestUtils.assertLog;
32
+ });
33
+
34
+ it('should batch state when updating state twice', () => {
35
+ let updateCount = 0;
36
+
37
+ class Component extends React.Component {
38
+ state = {x: 0};
39
+
40
+ componentDidUpdate() {
41
+ updateCount++;
42
+ }
43
+
44
+ render() {
45
+ return <div>{this.state.x}</div>;
46
+ }
47
+ }
48
+
49
+ const instance = ReactTestUtils.renderIntoDocument(<Component />);
50
+ expect(instance.state.x).toBe(0);
51
+
52
+ ReactDOM.unstable_batchedUpdates(function () {
53
+ instance.setState({x: 1});
54
+ instance.setState({x: 2});
55
+ expect(instance.state.x).toBe(0);
56
+ expect(updateCount).toBe(0);
57
+ });
58
+
59
+ expect(instance.state.x).toBe(2);
60
+ expect(updateCount).toBe(1);
61
+ });
62
+
63
+ it('should batch state when updating two different state keys', () => {
64
+ let updateCount = 0;
65
+
66
+ class Component extends React.Component {
67
+ state = {x: 0, y: 0};
68
+
69
+ componentDidUpdate() {
70
+ updateCount++;
71
+ }
72
+
73
+ render() {
74
+ return <div>{`(${this.state.x}, ${this.state.y})`}</div>;
75
+ }
76
+ }
77
+
78
+ const instance = ReactTestUtils.renderIntoDocument(<Component />);
79
+ expect(instance.state.x).toBe(0);
80
+ expect(instance.state.y).toBe(0);
81
+
82
+ ReactDOM.unstable_batchedUpdates(function () {
83
+ instance.setState({x: 1});
84
+ instance.setState({y: 2});
85
+ expect(instance.state.x).toBe(0);
86
+ expect(instance.state.y).toBe(0);
87
+ expect(updateCount).toBe(0);
88
+ });
89
+
90
+ expect(instance.state.x).toBe(1);
91
+ expect(instance.state.y).toBe(2);
92
+ expect(updateCount).toBe(1);
93
+ });
94
+
95
+ it('should batch state and props together', () => {
96
+ let updateCount = 0;
97
+
98
+ class Component extends React.Component {
99
+ state = {y: 0};
100
+
101
+ componentDidUpdate() {
102
+ updateCount++;
103
+ }
104
+
105
+ render() {
106
+ return <div>{`(${this.props.x}, ${this.state.y})`}</div>;
107
+ }
108
+ }
109
+
110
+ const container = document.createElement('div');
111
+ const instance = ReactDOM.render(<Component x={0} />, container);
112
+ expect(instance.props.x).toBe(0);
113
+ expect(instance.state.y).toBe(0);
114
+
115
+ ReactDOM.unstable_batchedUpdates(function () {
116
+ ReactDOM.render(<Component x={1} />, container);
117
+ instance.setState({y: 2});
118
+ expect(instance.props.x).toBe(0);
119
+ expect(instance.state.y).toBe(0);
120
+ expect(updateCount).toBe(0);
121
+ });
122
+
123
+ expect(instance.props.x).toBe(1);
124
+ expect(instance.state.y).toBe(2);
125
+ expect(updateCount).toBe(1);
126
+ });
127
+
128
+ it('should batch parent/child state updates together', () => {
129
+ let parentUpdateCount = 0;
130
+
131
+ class Parent extends React.Component {
132
+ state = {x: 0};
133
+ childRef = React.createRef();
134
+
135
+ componentDidUpdate() {
136
+ parentUpdateCount++;
137
+ }
138
+
139
+ render() {
140
+ return (
141
+ <div>
142
+ <Child ref={this.childRef} x={this.state.x} />
143
+ </div>
144
+ );
145
+ }
146
+ }
147
+
148
+ let childUpdateCount = 0;
149
+
150
+ class Child extends React.Component {
151
+ state = {y: 0};
152
+
153
+ componentDidUpdate() {
154
+ childUpdateCount++;
155
+ }
156
+
157
+ render() {
158
+ return <div>{this.props.x + this.state.y}</div>;
159
+ }
160
+ }
161
+
162
+ const instance = ReactTestUtils.renderIntoDocument(<Parent />);
163
+ const child = instance.childRef.current;
164
+ expect(instance.state.x).toBe(0);
165
+ expect(child.state.y).toBe(0);
166
+
167
+ ReactDOM.unstable_batchedUpdates(function () {
168
+ instance.setState({x: 1});
169
+ child.setState({y: 2});
170
+ expect(instance.state.x).toBe(0);
171
+ expect(child.state.y).toBe(0);
172
+ expect(parentUpdateCount).toBe(0);
173
+ expect(childUpdateCount).toBe(0);
174
+ });
175
+
176
+ expect(instance.state.x).toBe(1);
177
+ expect(child.state.y).toBe(2);
178
+ expect(parentUpdateCount).toBe(1);
179
+ expect(childUpdateCount).toBe(1);
180
+ });
181
+
182
+ it('should batch child/parent state updates together', () => {
183
+ let parentUpdateCount = 0;
184
+
185
+ class Parent extends React.Component {
186
+ state = {x: 0};
187
+ childRef = React.createRef();
188
+
189
+ componentDidUpdate() {
190
+ parentUpdateCount++;
191
+ }
192
+
193
+ render() {
194
+ return (
195
+ <div>
196
+ <Child ref={this.childRef} x={this.state.x} />
197
+ </div>
198
+ );
199
+ }
200
+ }
201
+
202
+ let childUpdateCount = 0;
203
+
204
+ class Child extends React.Component {
205
+ state = {y: 0};
206
+
207
+ componentDidUpdate() {
208
+ childUpdateCount++;
209
+ }
210
+
211
+ render() {
212
+ return <div>{this.props.x + this.state.y}</div>;
213
+ }
214
+ }
215
+
216
+ const instance = ReactTestUtils.renderIntoDocument(<Parent />);
217
+ const child = instance.childRef.current;
218
+ expect(instance.state.x).toBe(0);
219
+ expect(child.state.y).toBe(0);
220
+
221
+ ReactDOM.unstable_batchedUpdates(function () {
222
+ child.setState({y: 2});
223
+ instance.setState({x: 1});
224
+ expect(instance.state.x).toBe(0);
225
+ expect(child.state.y).toBe(0);
226
+ expect(parentUpdateCount).toBe(0);
227
+ expect(childUpdateCount).toBe(0);
228
+ });
229
+
230
+ expect(instance.state.x).toBe(1);
231
+ expect(child.state.y).toBe(2);
232
+ expect(parentUpdateCount).toBe(1);
233
+
234
+ // Batching reduces the number of updates here to 1.
235
+ expect(childUpdateCount).toBe(1);
236
+ });
237
+
238
+ it('should support chained state updates', () => {
239
+ let updateCount = 0;
240
+
241
+ class Component extends React.Component {
242
+ state = {x: 0};
243
+
244
+ componentDidUpdate() {
245
+ updateCount++;
246
+ }
247
+
248
+ render() {
249
+ return <div>{this.state.x}</div>;
250
+ }
251
+ }
252
+
253
+ const instance = ReactTestUtils.renderIntoDocument(<Component />);
254
+ expect(instance.state.x).toBe(0);
255
+
256
+ let innerCallbackRun = false;
257
+ ReactDOM.unstable_batchedUpdates(function () {
258
+ instance.setState({x: 1}, function () {
259
+ instance.setState({x: 2}, function () {
260
+ expect(this).toBe(instance);
261
+ innerCallbackRun = true;
262
+ expect(instance.state.x).toBe(2);
263
+ expect(updateCount).toBe(2);
264
+ });
265
+ expect(instance.state.x).toBe(1);
266
+ expect(updateCount).toBe(1);
267
+ });
268
+ expect(instance.state.x).toBe(0);
269
+ expect(updateCount).toBe(0);
270
+ });
271
+
272
+ expect(innerCallbackRun).toBeTruthy();
273
+ expect(instance.state.x).toBe(2);
274
+ expect(updateCount).toBe(2);
275
+ });
276
+
277
+ it('should batch forceUpdate together', () => {
278
+ let shouldUpdateCount = 0;
279
+ let updateCount = 0;
280
+
281
+ class Component extends React.Component {
282
+ state = {x: 0};
283
+
284
+ shouldComponentUpdate() {
285
+ shouldUpdateCount++;
286
+ }
287
+
288
+ componentDidUpdate() {
289
+ updateCount++;
290
+ }
291
+
292
+ render() {
293
+ return <div>{this.state.x}</div>;
294
+ }
295
+ }
296
+
297
+ const instance = ReactTestUtils.renderIntoDocument(<Component />);
298
+ expect(instance.state.x).toBe(0);
299
+
300
+ let callbacksRun = 0;
301
+ ReactDOM.unstable_batchedUpdates(function () {
302
+ instance.setState({x: 1}, function () {
303
+ callbacksRun++;
304
+ });
305
+ instance.forceUpdate(function () {
306
+ callbacksRun++;
307
+ });
308
+ expect(instance.state.x).toBe(0);
309
+ expect(updateCount).toBe(0);
310
+ });
311
+
312
+ expect(callbacksRun).toBe(2);
313
+ // shouldComponentUpdate shouldn't be called since we're forcing
314
+ expect(shouldUpdateCount).toBe(0);
315
+ expect(instance.state.x).toBe(1);
316
+ expect(updateCount).toBe(1);
317
+ });
318
+
319
+ it('should update children even if parent blocks updates', () => {
320
+ let parentRenderCount = 0;
321
+ let childRenderCount = 0;
322
+
323
+ class Parent extends React.Component {
324
+ childRef = React.createRef();
325
+
326
+ shouldComponentUpdate() {
327
+ return false;
328
+ }
329
+
330
+ render() {
331
+ parentRenderCount++;
332
+ return <Child ref={this.childRef} />;
333
+ }
334
+ }
335
+
336
+ class Child extends React.Component {
337
+ render() {
338
+ childRenderCount++;
339
+ return <div />;
340
+ }
341
+ }
342
+
343
+ expect(parentRenderCount).toBe(0);
344
+ expect(childRenderCount).toBe(0);
345
+
346
+ let instance = <Parent />;
347
+ instance = ReactTestUtils.renderIntoDocument(instance);
348
+
349
+ expect(parentRenderCount).toBe(1);
350
+ expect(childRenderCount).toBe(1);
351
+
352
+ ReactDOM.unstable_batchedUpdates(function () {
353
+ instance.setState({x: 1});
354
+ });
355
+
356
+ expect(parentRenderCount).toBe(1);
357
+ expect(childRenderCount).toBe(1);
358
+
359
+ ReactDOM.unstable_batchedUpdates(function () {
360
+ instance.childRef.current.setState({x: 1});
361
+ });
362
+
363
+ expect(parentRenderCount).toBe(1);
364
+ expect(childRenderCount).toBe(2);
365
+ });
366
+
367
+ it('should not reconcile children passed via props', () => {
368
+ let numMiddleRenders = 0;
369
+ let numBottomRenders = 0;
370
+
371
+ class Top extends React.Component {
372
+ render() {
373
+ return (
374
+ <Middle>
375
+ <Bottom />
376
+ </Middle>
377
+ );
378
+ }
379
+ }
380
+
381
+ class Middle extends React.Component {
382
+ componentDidMount() {
383
+ this.forceUpdate();
384
+ }
385
+
386
+ render() {
387
+ numMiddleRenders++;
388
+ return React.Children.only(this.props.children);
389
+ }
390
+ }
391
+
392
+ class Bottom extends React.Component {
393
+ render() {
394
+ numBottomRenders++;
395
+ return null;
396
+ }
397
+ }
398
+
399
+ ReactTestUtils.renderIntoDocument(<Top />);
400
+ expect(numMiddleRenders).toBe(2);
401
+ expect(numBottomRenders).toBe(1);
402
+ });
403
+
404
+ it('should flow updates correctly', () => {
405
+ let willUpdates = [];
406
+ let didUpdates = [];
407
+
408
+ const UpdateLoggingMixin = {
409
+ UNSAFE_componentWillUpdate: function () {
410
+ willUpdates.push(this.constructor.displayName);
411
+ },
412
+ componentDidUpdate: function () {
413
+ didUpdates.push(this.constructor.displayName);
414
+ },
415
+ };
416
+
417
+ class Box extends React.Component {
418
+ boxDivRef = React.createRef();
419
+
420
+ render() {
421
+ return <div ref={this.boxDivRef}>{this.props.children}</div>;
422
+ }
423
+ }
424
+ Object.assign(Box.prototype, UpdateLoggingMixin);
425
+
426
+ class Child extends React.Component {
427
+ spanRef = React.createRef();
428
+
429
+ render() {
430
+ return <span ref={this.spanRef}>child</span>;
431
+ }
432
+ }
433
+ Object.assign(Child.prototype, UpdateLoggingMixin);
434
+
435
+ class Switcher extends React.Component {
436
+ state = {tabKey: 'hello'};
437
+ boxRef = React.createRef();
438
+ switcherDivRef = React.createRef();
439
+ render() {
440
+ const child = this.props.children;
441
+
442
+ return (
443
+ <Box ref={this.boxRef}>
444
+ <div
445
+ ref={this.switcherDivRef}
446
+ style={{
447
+ display: this.state.tabKey === child.key ? '' : 'none',
448
+ }}>
449
+ {child}
450
+ </div>
451
+ </Box>
452
+ );
453
+ }
454
+ }
455
+ Object.assign(Switcher.prototype, UpdateLoggingMixin);
456
+
457
+ class App extends React.Component {
458
+ switcherRef = React.createRef();
459
+ childRef = React.createRef();
460
+
461
+ render() {
462
+ return (
463
+ <Switcher ref={this.switcherRef}>
464
+ <Child key="hello" ref={this.childRef} />
465
+ </Switcher>
466
+ );
467
+ }
468
+ }
469
+ Object.assign(App.prototype, UpdateLoggingMixin);
470
+
471
+ let root = <App />;
472
+ root = ReactTestUtils.renderIntoDocument(root);
473
+
474
+ function expectUpdates(desiredWillUpdates, desiredDidUpdates) {
475
+ let i;
476
+ for (i = 0; i < desiredWillUpdates; i++) {
477
+ expect(willUpdates).toContain(desiredWillUpdates[i]);
478
+ }
479
+ for (i = 0; i < desiredDidUpdates; i++) {
480
+ expect(didUpdates).toContain(desiredDidUpdates[i]);
481
+ }
482
+ willUpdates = [];
483
+ didUpdates = [];
484
+ }
485
+
486
+ function triggerUpdate(c) {
487
+ c.setState({x: 1});
488
+ }
489
+
490
+ function testUpdates(components, desiredWillUpdates, desiredDidUpdates) {
491
+ let i;
492
+
493
+ ReactDOM.unstable_batchedUpdates(function () {
494
+ for (i = 0; i < components.length; i++) {
495
+ triggerUpdate(components[i]);
496
+ }
497
+ });
498
+
499
+ expectUpdates(desiredWillUpdates, desiredDidUpdates);
500
+
501
+ // Try them in reverse order
502
+
503
+ ReactDOM.unstable_batchedUpdates(function () {
504
+ for (i = components.length - 1; i >= 0; i--) {
505
+ triggerUpdate(components[i]);
506
+ }
507
+ });
508
+
509
+ expectUpdates(desiredWillUpdates, desiredDidUpdates);
510
+ }
511
+ testUpdates(
512
+ [root.switcherRef.current.boxRef.current, root.switcherRef.current],
513
+ // Owner-child relationships have inverse will and did
514
+ ['Switcher', 'Box'],
515
+ ['Box', 'Switcher'],
516
+ );
517
+
518
+ testUpdates(
519
+ [root.childRef.current, root.switcherRef.current.boxRef.current],
520
+ // Not owner-child so reconcile independently
521
+ ['Box', 'Child'],
522
+ ['Box', 'Child'],
523
+ );
524
+
525
+ testUpdates(
526
+ [root.childRef.current, root.switcherRef.current],
527
+ // Switcher owns Box and Child, Box does not own Child
528
+ ['Switcher', 'Box', 'Child'],
529
+ ['Box', 'Switcher', 'Child'],
530
+ );
531
+ });
532
+
533
+ it('should queue mount-ready handlers across different roots', () => {
534
+ // We'll define two components A and B, then update both of them. When A's
535
+ // componentDidUpdate handlers is called, B's DOM should already have been
536
+ // updated.
537
+
538
+ const bContainer = document.createElement('div');
539
+
540
+ let b;
541
+
542
+ let aUpdated = false;
543
+
544
+ class A extends React.Component {
545
+ state = {x: 0};
546
+
547
+ componentDidUpdate() {
548
+ expect(ReactDOM.findDOMNode(b).textContent).toBe('B1');
549
+ aUpdated = true;
550
+ }
551
+
552
+ render() {
553
+ let portal = null;
554
+ // If we're using Fiber, we use Portals instead to achieve this.
555
+ portal = ReactDOM.createPortal(<B ref={n => (b = n)} />, bContainer);
556
+ return (
557
+ <div>
558
+ A{this.state.x}
559
+ {portal}
560
+ </div>
561
+ );
562
+ }
563
+ }
564
+
565
+ class B extends React.Component {
566
+ state = {x: 0};
567
+
568
+ render() {
569
+ return <div>B{this.state.x}</div>;
570
+ }
571
+ }
572
+
573
+ const a = ReactTestUtils.renderIntoDocument(<A />);
574
+ ReactDOM.unstable_batchedUpdates(function () {
575
+ a.setState({x: 1});
576
+ b.setState({x: 1});
577
+ });
578
+
579
+ expect(aUpdated).toBe(true);
580
+ });
581
+
582
+ it('should flush updates in the correct order', () => {
583
+ const updates = [];
584
+
585
+ class Outer extends React.Component {
586
+ state = {x: 0};
587
+ innerRef = React.createRef();
588
+
589
+ render() {
590
+ updates.push('Outer-render-' + this.state.x);
591
+ return (
592
+ <div>
593
+ <Inner x={this.state.x} ref={this.innerRef} />
594
+ </div>
595
+ );
596
+ }
597
+
598
+ componentDidUpdate() {
599
+ const x = this.state.x;
600
+ updates.push('Outer-didUpdate-' + x);
601
+ updates.push('Inner-setState-' + x);
602
+ this.innerRef.current.setState({x: x}, function () {
603
+ updates.push('Inner-callback-' + x);
604
+ });
605
+ }
606
+ }
607
+
608
+ class Inner extends React.Component {
609
+ state = {x: 0};
610
+
611
+ render() {
612
+ updates.push('Inner-render-' + this.props.x + '-' + this.state.x);
613
+ return <div />;
614
+ }
615
+
616
+ componentDidUpdate() {
617
+ updates.push('Inner-didUpdate-' + this.props.x + '-' + this.state.x);
618
+ }
619
+ }
620
+
621
+ const instance = ReactTestUtils.renderIntoDocument(<Outer />);
622
+
623
+ updates.push('Outer-setState-1');
624
+ instance.setState({x: 1}, function () {
625
+ updates.push('Outer-callback-1');
626
+ updates.push('Outer-setState-2');
627
+ instance.setState({x: 2}, function () {
628
+ updates.push('Outer-callback-2');
629
+ });
630
+ });
631
+
632
+ /* eslint-disable indent */
633
+ expect(updates).toEqual([
634
+ 'Outer-render-0',
635
+ 'Inner-render-0-0',
636
+
637
+ 'Outer-setState-1',
638
+ 'Outer-render-1',
639
+ 'Inner-render-1-0',
640
+ 'Inner-didUpdate-1-0',
641
+ 'Outer-didUpdate-1',
642
+ // Happens in a batch, so don't re-render yet
643
+ 'Inner-setState-1',
644
+ 'Outer-callback-1',
645
+
646
+ // Happens in a batch
647
+ 'Outer-setState-2',
648
+
649
+ // Flush batched updates all at once
650
+ 'Outer-render-2',
651
+ 'Inner-render-2-1',
652
+ 'Inner-didUpdate-2-1',
653
+ 'Inner-callback-1',
654
+ 'Outer-didUpdate-2',
655
+ 'Inner-setState-2',
656
+ 'Outer-callback-2',
657
+ 'Inner-render-2-2',
658
+ 'Inner-didUpdate-2-2',
659
+ 'Inner-callback-2',
660
+ ]);
661
+ /* eslint-enable indent */
662
+ });
663
+
664
+ it('should flush updates in the correct order across roots', () => {
665
+ const instances = [];
666
+ const updates = [];
667
+
668
+ class MockComponent extends React.Component {
669
+ render() {
670
+ updates.push(this.props.depth);
671
+ return <div />;
672
+ }
673
+
674
+ componentDidMount() {
675
+ instances.push(this);
676
+ if (this.props.depth < this.props.count) {
677
+ ReactDOM.render(
678
+ <MockComponent
679
+ depth={this.props.depth + 1}
680
+ count={this.props.count}
681
+ />,
682
+ ReactDOM.findDOMNode(this),
683
+ );
684
+ }
685
+ }
686
+ }
687
+
688
+ ReactTestUtils.renderIntoDocument(<MockComponent depth={0} count={2} />);
689
+
690
+ expect(updates).toEqual([0, 1, 2]);
691
+
692
+ ReactDOM.unstable_batchedUpdates(function () {
693
+ // Simulate update on each component from top to bottom.
694
+ instances.forEach(function (instance) {
695
+ instance.forceUpdate();
696
+ });
697
+ });
698
+
699
+ expect(updates).toEqual([0, 1, 2, 0, 1, 2]);
700
+ });
701
+
702
+ it('should queue nested updates', () => {
703
+ // See https://github.com/facebook/react/issues/1147
704
+
705
+ class X extends React.Component {
706
+ state = {s: 0};
707
+
708
+ render() {
709
+ if (this.state.s === 0) {
710
+ return (
711
+ <div>
712
+ <span>0</span>
713
+ </div>
714
+ );
715
+ } else {
716
+ return <div>1</div>;
717
+ }
718
+ }
719
+
720
+ go = () => {
721
+ this.setState({s: 1});
722
+ this.setState({s: 0});
723
+ this.setState({s: 1});
724
+ };
725
+ }
726
+
727
+ class Y extends React.Component {
728
+ render() {
729
+ return (
730
+ <div>
731
+ <Z />
732
+ </div>
733
+ );
734
+ }
735
+ }
736
+
737
+ class Z extends React.Component {
738
+ render() {
739
+ return <div />;
740
+ }
741
+
742
+ UNSAFE_componentWillUpdate() {
743
+ x.go();
744
+ }
745
+ }
746
+
747
+ const x = ReactTestUtils.renderIntoDocument(<X />);
748
+ const y = ReactTestUtils.renderIntoDocument(<Y />);
749
+ expect(ReactDOM.findDOMNode(x).textContent).toBe('0');
750
+
751
+ y.forceUpdate();
752
+ expect(ReactDOM.findDOMNode(x).textContent).toBe('1');
753
+ });
754
+
755
+ it('should queue updates from during mount', () => {
756
+ // See https://github.com/facebook/react/issues/1353
757
+ let a;
758
+
759
+ class A extends React.Component {
760
+ state = {x: 0};
761
+
762
+ UNSAFE_componentWillMount() {
763
+ a = this;
764
+ }
765
+
766
+ render() {
767
+ return <div>A{this.state.x}</div>;
768
+ }
769
+ }
770
+
771
+ class B extends React.Component {
772
+ UNSAFE_componentWillMount() {
773
+ a.setState({x: 1});
774
+ }
775
+
776
+ render() {
777
+ return <div />;
778
+ }
779
+ }
780
+
781
+ ReactDOM.unstable_batchedUpdates(function () {
782
+ ReactTestUtils.renderIntoDocument(
783
+ <div>
784
+ <A />
785
+ <B />
786
+ </div>,
787
+ );
788
+ });
789
+
790
+ expect(a.state.x).toBe(1);
791
+ expect(ReactDOM.findDOMNode(a).textContent).toBe('A1');
792
+ });
793
+
794
+ it('calls componentWillReceiveProps setState callback properly', () => {
795
+ let callbackCount = 0;
796
+
797
+ class A extends React.Component {
798
+ state = {x: this.props.x};
799
+
800
+ UNSAFE_componentWillReceiveProps(nextProps) {
801
+ const newX = nextProps.x;
802
+ this.setState({x: newX}, function () {
803
+ // State should have updated by the time this callback gets called
804
+ expect(this.state.x).toBe(newX);
805
+ callbackCount++;
806
+ });
807
+ }
808
+
809
+ render() {
810
+ return <div>{this.state.x}</div>;
811
+ }
812
+ }
813
+
814
+ const container = document.createElement('div');
815
+ ReactDOM.render(<A x={1} />, container);
816
+ ReactDOM.render(<A x={2} />, container);
817
+ expect(callbackCount).toBe(1);
818
+ });
819
+
820
+ it('does not call render after a component as been deleted', () => {
821
+ let renderCount = 0;
822
+ let componentB = null;
823
+
824
+ class B extends React.Component {
825
+ state = {updates: 0};
826
+
827
+ componentDidMount() {
828
+ componentB = this;
829
+ }
830
+
831
+ render() {
832
+ renderCount++;
833
+ return <div />;
834
+ }
835
+ }
836
+
837
+ class A extends React.Component {
838
+ state = {showB: true};
839
+
840
+ render() {
841
+ return this.state.showB ? <B /> : <div />;
842
+ }
843
+ }
844
+
845
+ const component = ReactTestUtils.renderIntoDocument(<A />);
846
+
847
+ ReactDOM.unstable_batchedUpdates(function () {
848
+ // B will have scheduled an update but the batching should ensure that its
849
+ // update never fires.
850
+ componentB.setState({updates: 1});
851
+ component.setState({showB: false});
852
+ });
853
+
854
+ expect(renderCount).toBe(1);
855
+ });
856
+
857
+ it('throws in setState if the update callback is not a function', () => {
858
+ function Foo() {
859
+ this.a = 1;
860
+ this.b = 2;
861
+ }
862
+
863
+ class A extends React.Component {
864
+ state = {};
865
+
866
+ render() {
867
+ return <div />;
868
+ }
869
+ }
870
+
871
+ let component = ReactTestUtils.renderIntoDocument(<A />);
872
+
873
+ expect(() => {
874
+ expect(() => component.setState({}, 'no')).toErrorDev(
875
+ 'setState(...): Expected the last optional `callback` argument to be ' +
876
+ 'a function. Instead received: no.',
877
+ );
878
+ }).toThrowError(
879
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
880
+ 'received: no',
881
+ );
882
+ component = ReactTestUtils.renderIntoDocument(<A />);
883
+ expect(() => {
884
+ expect(() => component.setState({}, {foo: 'bar'})).toErrorDev(
885
+ 'setState(...): Expected the last optional `callback` argument to be ' +
886
+ 'a function. Instead received: [object Object].',
887
+ );
888
+ }).toThrowError(
889
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
890
+ 'received: [object Object]',
891
+ );
892
+ // Make sure the warning is deduplicated and doesn't fire again
893
+ component = ReactTestUtils.renderIntoDocument(<A />);
894
+ expect(() => component.setState({}, new Foo())).toThrowError(
895
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
896
+ 'received: [object Object]',
897
+ );
898
+ });
899
+
900
+ it('throws in forceUpdate if the update callback is not a function', () => {
901
+ function Foo() {
902
+ this.a = 1;
903
+ this.b = 2;
904
+ }
905
+
906
+ class A extends React.Component {
907
+ state = {};
908
+
909
+ render() {
910
+ return <div />;
911
+ }
912
+ }
913
+
914
+ let component = ReactTestUtils.renderIntoDocument(<A />);
915
+
916
+ expect(() => {
917
+ expect(() => component.forceUpdate('no')).toErrorDev(
918
+ 'forceUpdate(...): Expected the last optional `callback` argument to be ' +
919
+ 'a function. Instead received: no.',
920
+ );
921
+ }).toThrowError(
922
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
923
+ 'received: no',
924
+ );
925
+ component = ReactTestUtils.renderIntoDocument(<A />);
926
+ expect(() => {
927
+ expect(() => component.forceUpdate({foo: 'bar'})).toErrorDev(
928
+ 'forceUpdate(...): Expected the last optional `callback` argument to be ' +
929
+ 'a function. Instead received: [object Object].',
930
+ );
931
+ }).toThrowError(
932
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
933
+ 'received: [object Object]',
934
+ );
935
+ // Make sure the warning is deduplicated and doesn't fire again
936
+ component = ReactTestUtils.renderIntoDocument(<A />);
937
+ expect(() => component.forceUpdate(new Foo())).toThrowError(
938
+ 'Invalid argument passed as callback. Expected a function. Instead ' +
939
+ 'received: [object Object]',
940
+ );
941
+ });
942
+
943
+ it('does not update one component twice in a batch (#2410)', () => {
944
+ class Parent extends React.Component {
945
+ childRef = React.createRef();
946
+
947
+ getChild = () => {
948
+ return this.childRef.current;
949
+ };
950
+
951
+ render() {
952
+ return <Child ref={this.childRef} />;
953
+ }
954
+ }
955
+
956
+ let renderCount = 0;
957
+ let postRenderCount = 0;
958
+ let once = false;
959
+
960
+ class Child extends React.Component {
961
+ state = {updated: false};
962
+
963
+ UNSAFE_componentWillUpdate() {
964
+ if (!once) {
965
+ once = true;
966
+ this.setState({updated: true});
967
+ }
968
+ }
969
+
970
+ componentDidMount() {
971
+ expect(renderCount).toBe(postRenderCount + 1);
972
+ postRenderCount++;
973
+ }
974
+
975
+ componentDidUpdate() {
976
+ expect(renderCount).toBe(postRenderCount + 1);
977
+ postRenderCount++;
978
+ }
979
+
980
+ render() {
981
+ expect(renderCount).toBe(postRenderCount);
982
+ renderCount++;
983
+ return <div />;
984
+ }
985
+ }
986
+
987
+ const parent = ReactTestUtils.renderIntoDocument(<Parent />);
988
+ const child = parent.getChild();
989
+ ReactDOM.unstable_batchedUpdates(function () {
990
+ parent.forceUpdate();
991
+ child.forceUpdate();
992
+ });
993
+ });
994
+
995
+ it('does not update one component twice in a batch (#6371)', () => {
996
+ let callbacks = [];
997
+ function emitChange() {
998
+ callbacks.forEach(c => c());
999
+ }
1000
+
1001
+ class App extends React.Component {
1002
+ constructor(props) {
1003
+ super(props);
1004
+ this.state = {showChild: true};
1005
+ }
1006
+ componentDidMount() {
1007
+ this.setState({showChild: false});
1008
+ }
1009
+ render() {
1010
+ return (
1011
+ <div>
1012
+ <ForceUpdatesOnChange />
1013
+ {this.state.showChild && <EmitsChangeOnUnmount />}
1014
+ </div>
1015
+ );
1016
+ }
1017
+ }
1018
+
1019
+ class EmitsChangeOnUnmount extends React.Component {
1020
+ componentWillUnmount() {
1021
+ emitChange();
1022
+ }
1023
+ render() {
1024
+ return null;
1025
+ }
1026
+ }
1027
+
1028
+ class ForceUpdatesOnChange extends React.Component {
1029
+ componentDidMount() {
1030
+ this.onChange = () => this.forceUpdate();
1031
+ this.onChange();
1032
+ callbacks.push(this.onChange);
1033
+ }
1034
+ componentWillUnmount() {
1035
+ callbacks = callbacks.filter(c => c !== this.onChange);
1036
+ }
1037
+ render() {
1038
+ return <div key={Math.random()} onClick={function () {}} />;
1039
+ }
1040
+ }
1041
+
1042
+ ReactDOM.render(<App />, document.createElement('div'));
1043
+ });
1044
+
1045
+ it('unstable_batchedUpdates should return value from a callback', () => {
1046
+ const result = ReactDOM.unstable_batchedUpdates(function () {
1047
+ return 42;
1048
+ });
1049
+ expect(result).toEqual(42);
1050
+ });
1051
+
1052
+ it('unmounts and remounts a root in the same batch', () => {
1053
+ const container = document.createElement('div');
1054
+ ReactDOM.render(<span>a</span>, container);
1055
+ ReactDOM.unstable_batchedUpdates(function () {
1056
+ ReactDOM.unmountComponentAtNode(container);
1057
+ ReactDOM.render(<span>b</span>, container);
1058
+ });
1059
+ expect(container.textContent).toBe('b');
1060
+ });
1061
+
1062
+ it('handles reentrant mounting in synchronous mode', () => {
1063
+ let mounts = 0;
1064
+ class Editor extends React.Component {
1065
+ render() {
1066
+ return <div>{this.props.text}</div>;
1067
+ }
1068
+ componentDidMount() {
1069
+ mounts++;
1070
+ // This should be called only once but we guard just in case.
1071
+ if (!this.props.rendered) {
1072
+ this.props.onChange({rendered: true});
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ const container = document.createElement('div');
1078
+ function render() {
1079
+ ReactDOM.render(
1080
+ <Editor
1081
+ onChange={newProps => {
1082
+ props = {...props, ...newProps};
1083
+ render();
1084
+ }}
1085
+ {...props}
1086
+ />,
1087
+ container,
1088
+ );
1089
+ }
1090
+
1091
+ let props = {text: 'hello', rendered: false};
1092
+ render();
1093
+ props = {...props, text: 'goodbye'};
1094
+ render();
1095
+ expect(container.textContent).toBe('goodbye');
1096
+ expect(mounts).toBe(1);
1097
+ });
1098
+
1099
+ it('mounts and unmounts are sync even in a batch', () => {
1100
+ const ops = [];
1101
+ const container = document.createElement('div');
1102
+ ReactDOM.unstable_batchedUpdates(() => {
1103
+ ReactDOM.render(<div>Hello</div>, container);
1104
+ ops.push(container.textContent);
1105
+ ReactDOM.unmountComponentAtNode(container);
1106
+ ops.push(container.textContent);
1107
+ });
1108
+ expect(ops).toEqual(['Hello', '']);
1109
+ });
1110
+
1111
+ it(
1112
+ 'in legacy mode, updates in componentWillUpdate and componentDidUpdate ' +
1113
+ 'should both flush in the immediately subsequent commit',
1114
+ () => {
1115
+ const ops = [];
1116
+ class Foo extends React.Component {
1117
+ state = {a: false, b: false};
1118
+ UNSAFE_componentWillUpdate(_, nextState) {
1119
+ if (!nextState.a) {
1120
+ this.setState({a: true});
1121
+ }
1122
+ }
1123
+ componentDidUpdate() {
1124
+ ops.push('Foo updated');
1125
+ if (!this.state.b) {
1126
+ this.setState({b: true});
1127
+ }
1128
+ }
1129
+ render() {
1130
+ ops.push(`a: ${this.state.a}, b: ${this.state.b}`);
1131
+ return null;
1132
+ }
1133
+ }
1134
+
1135
+ const container = document.createElement('div');
1136
+ // Mount
1137
+ ReactDOM.render(<Foo />, container);
1138
+ // Root update
1139
+ ReactDOM.render(<Foo />, container);
1140
+ expect(ops).toEqual([
1141
+ // Mount
1142
+ 'a: false, b: false',
1143
+ // Root update
1144
+ 'a: false, b: false',
1145
+ 'Foo updated',
1146
+ // Subsequent update (both a and b should have flushed)
1147
+ 'a: true, b: true',
1148
+ 'Foo updated',
1149
+ // There should not be any additional updates
1150
+ ]);
1151
+ },
1152
+ );
1153
+
1154
+ it(
1155
+ 'in legacy mode, updates in componentWillUpdate and componentDidUpdate ' +
1156
+ '(on a sibling) should both flush in the immediately subsequent commit',
1157
+ () => {
1158
+ const ops = [];
1159
+ class Foo extends React.Component {
1160
+ state = {a: false};
1161
+ UNSAFE_componentWillUpdate(_, nextState) {
1162
+ if (!nextState.a) {
1163
+ this.setState({a: true});
1164
+ }
1165
+ }
1166
+ componentDidUpdate() {
1167
+ ops.push('Foo updated');
1168
+ }
1169
+ render() {
1170
+ ops.push(`a: ${this.state.a}`);
1171
+ return null;
1172
+ }
1173
+ }
1174
+
1175
+ class Bar extends React.Component {
1176
+ state = {b: false};
1177
+ componentDidUpdate() {
1178
+ ops.push('Bar updated');
1179
+ if (!this.state.b) {
1180
+ this.setState({b: true});
1181
+ }
1182
+ }
1183
+ render() {
1184
+ ops.push(`b: ${this.state.b}`);
1185
+ return null;
1186
+ }
1187
+ }
1188
+
1189
+ const container = document.createElement('div');
1190
+ // Mount
1191
+ ReactDOM.render(
1192
+ <div>
1193
+ <Foo />
1194
+ <Bar />
1195
+ </div>,
1196
+ container,
1197
+ );
1198
+ // Root update
1199
+ ReactDOM.render(
1200
+ <div>
1201
+ <Foo />
1202
+ <Bar />
1203
+ </div>,
1204
+ container,
1205
+ );
1206
+ expect(ops).toEqual([
1207
+ // Mount
1208
+ 'a: false',
1209
+ 'b: false',
1210
+ // Root update
1211
+ 'a: false',
1212
+ 'b: false',
1213
+ 'Foo updated',
1214
+ 'Bar updated',
1215
+ // Subsequent update (both a and b should have flushed)
1216
+ 'a: true',
1217
+ 'b: true',
1218
+ 'Foo updated',
1219
+ 'Bar updated',
1220
+ // There should not be any additional updates
1221
+ ]);
1222
+ },
1223
+ );
1224
+
1225
+ it('uses correct base state for setState inside render phase', () => {
1226
+ const ops = [];
1227
+
1228
+ class Foo extends React.Component {
1229
+ state = {step: 0};
1230
+ render() {
1231
+ const memoizedStep = this.state.step;
1232
+ this.setState(baseState => {
1233
+ const baseStep = baseState.step;
1234
+ ops.push(`base: ${baseStep}, memoized: ${memoizedStep}`);
1235
+ return baseStep === 0 ? {step: 1} : null;
1236
+ });
1237
+ return null;
1238
+ }
1239
+ }
1240
+
1241
+ const container = document.createElement('div');
1242
+ expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
1243
+ 'Cannot update during an existing state transition',
1244
+ );
1245
+ expect(ops).toEqual(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
1246
+ });
1247
+
1248
+ it('does not re-render if state update is null', () => {
1249
+ const container = document.createElement('div');
1250
+
1251
+ let instance;
1252
+ let ops = [];
1253
+ class Foo extends React.Component {
1254
+ render() {
1255
+ instance = this;
1256
+ ops.push('render');
1257
+ return <div />;
1258
+ }
1259
+ }
1260
+ ReactDOM.render(<Foo />, container);
1261
+
1262
+ ops = [];
1263
+ instance.setState(() => null);
1264
+ expect(ops).toEqual([]);
1265
+ });
1266
+
1267
+ // Will change once we switch to async by default
1268
+ it('synchronously renders hidden subtrees', () => {
1269
+ const container = document.createElement('div');
1270
+ let ops = [];
1271
+
1272
+ function Baz() {
1273
+ ops.push('Baz');
1274
+ return null;
1275
+ }
1276
+
1277
+ function Bar() {
1278
+ ops.push('Bar');
1279
+ return null;
1280
+ }
1281
+
1282
+ function Foo() {
1283
+ ops.push('Foo');
1284
+ return (
1285
+ <div>
1286
+ <div hidden={true}>
1287
+ <Bar />
1288
+ </div>
1289
+ <Baz />
1290
+ </div>
1291
+ );
1292
+ }
1293
+
1294
+ // Mount
1295
+ ReactDOM.render(<Foo />, container);
1296
+ expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
1297
+ ops = [];
1298
+
1299
+ // Update
1300
+ ReactDOM.render(<Foo />, container);
1301
+ expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
1302
+ });
1303
+
1304
+ it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
1305
+ class Foo extends React.Component {
1306
+ componentDidMount() {
1307
+ const limit = 1200;
1308
+ for (let i = 0; i < limit; i++) {
1309
+ if (i < limit - 1) {
1310
+ ReactDOM.render(<div />, document.createElement('div'));
1311
+ } else {
1312
+ ReactDOM.render(<div />, document.createElement('div'), () => {
1313
+ // The "nested update limit" error isn't thrown until setState
1314
+ this.setState({});
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+ render() {
1320
+ return null;
1321
+ }
1322
+ }
1323
+
1324
+ const container = document.createElement('div');
1325
+ ReactDOM.render(<Foo />, container);
1326
+ });
1327
+
1328
+ it('resets the update counter for unrelated updates', () => {
1329
+ const container = document.createElement('div');
1330
+ const ref = React.createRef();
1331
+
1332
+ class EventuallyTerminating extends React.Component {
1333
+ state = {step: 0};
1334
+ componentDidMount() {
1335
+ this.setState({step: 1});
1336
+ }
1337
+ componentDidUpdate() {
1338
+ if (this.state.step < limit) {
1339
+ this.setState({step: this.state.step + 1});
1340
+ }
1341
+ }
1342
+ render() {
1343
+ return this.state.step;
1344
+ }
1345
+ }
1346
+
1347
+ let limit = 55;
1348
+ expect(() => {
1349
+ ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1350
+ }).toThrow('Maximum');
1351
+
1352
+ // Verify that we don't go over the limit if these updates are unrelated.
1353
+ limit -= 10;
1354
+ ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1355
+ expect(container.textContent).toBe(limit.toString());
1356
+ ref.current.setState({step: 0});
1357
+ expect(container.textContent).toBe(limit.toString());
1358
+ ref.current.setState({step: 0});
1359
+ expect(container.textContent).toBe(limit.toString());
1360
+
1361
+ limit += 10;
1362
+ expect(() => {
1363
+ ref.current.setState({step: 0});
1364
+ }).toThrow('Maximum');
1365
+ expect(ref.current).toBe(null);
1366
+ });
1367
+
1368
+ it('does not fall into an infinite update loop', () => {
1369
+ class NonTerminating extends React.Component {
1370
+ state = {step: 0};
1371
+ componentDidMount() {
1372
+ this.setState({step: 1});
1373
+ }
1374
+ UNSAFE_componentWillUpdate() {
1375
+ this.setState({step: 2});
1376
+ }
1377
+ render() {
1378
+ return (
1379
+ <div>
1380
+ Hello {this.props.name}
1381
+ {this.state.step}
1382
+ </div>
1383
+ );
1384
+ }
1385
+ }
1386
+
1387
+ const container = document.createElement('div');
1388
+ expect(() => {
1389
+ ReactDOM.render(<NonTerminating />, container);
1390
+ }).toThrow('Maximum');
1391
+ });
1392
+
1393
+ it('does not fall into an infinite update loop with useLayoutEffect', () => {
1394
+ function NonTerminating() {
1395
+ const [step, setStep] = React.useState(0);
1396
+ React.useLayoutEffect(() => {
1397
+ setStep(x => x + 1);
1398
+ });
1399
+ return step;
1400
+ }
1401
+
1402
+ const container = document.createElement('div');
1403
+ expect(() => {
1404
+ ReactDOM.render(<NonTerminating />, container);
1405
+ }).toThrow('Maximum');
1406
+ });
1407
+
1408
+ it('can recover after falling into an infinite update loop', () => {
1409
+ class NonTerminating extends React.Component {
1410
+ state = {step: 0};
1411
+ componentDidMount() {
1412
+ this.setState({step: 1});
1413
+ }
1414
+ componentDidUpdate() {
1415
+ this.setState({step: 2});
1416
+ }
1417
+ render() {
1418
+ return this.state.step;
1419
+ }
1420
+ }
1421
+
1422
+ class Terminating extends React.Component {
1423
+ state = {step: 0};
1424
+ componentDidMount() {
1425
+ this.setState({step: 1});
1426
+ }
1427
+ render() {
1428
+ return this.state.step;
1429
+ }
1430
+ }
1431
+
1432
+ const container = document.createElement('div');
1433
+ expect(() => {
1434
+ ReactDOM.render(<NonTerminating />, container);
1435
+ }).toThrow('Maximum');
1436
+
1437
+ ReactDOM.render(<Terminating />, container);
1438
+ expect(container.textContent).toBe('1');
1439
+
1440
+ expect(() => {
1441
+ ReactDOM.render(<NonTerminating />, container);
1442
+ }).toThrow('Maximum');
1443
+
1444
+ ReactDOM.render(<Terminating />, container);
1445
+ expect(container.textContent).toBe('1');
1446
+ });
1447
+
1448
+ it('does not fall into mutually recursive infinite update loop with same container', () => {
1449
+ // Note: this test would fail if there were two or more different roots.
1450
+
1451
+ class A extends React.Component {
1452
+ componentDidMount() {
1453
+ ReactDOM.render(<B />, container);
1454
+ }
1455
+ render() {
1456
+ return null;
1457
+ }
1458
+ }
1459
+
1460
+ class B extends React.Component {
1461
+ componentDidMount() {
1462
+ ReactDOM.render(<A />, container);
1463
+ }
1464
+ render() {
1465
+ return null;
1466
+ }
1467
+ }
1468
+
1469
+ const container = document.createElement('div');
1470
+ expect(() => {
1471
+ ReactDOM.render(<A />, container);
1472
+ }).toThrow('Maximum');
1473
+ });
1474
+
1475
+ it('does not fall into an infinite error loop', () => {
1476
+ function BadRender() {
1477
+ throw new Error('error');
1478
+ }
1479
+
1480
+ class ErrorBoundary extends React.Component {
1481
+ componentDidCatch() {
1482
+ // Schedule a no-op state update to avoid triggering a DEV warning in the test.
1483
+ this.setState({});
1484
+
1485
+ this.props.parent.remount();
1486
+ }
1487
+ render() {
1488
+ return <BadRender />;
1489
+ }
1490
+ }
1491
+
1492
+ class NonTerminating extends React.Component {
1493
+ state = {step: 0};
1494
+ remount() {
1495
+ this.setState(state => ({step: state.step + 1}));
1496
+ }
1497
+ render() {
1498
+ return <ErrorBoundary key={this.state.step} parent={this} />;
1499
+ }
1500
+ }
1501
+
1502
+ const container = document.createElement('div');
1503
+ expect(() => {
1504
+ ReactDOM.render(<NonTerminating />, container);
1505
+ }).toThrow('Maximum');
1506
+ });
1507
+
1508
+ it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', () => {
1509
+ const subscribers = [];
1510
+
1511
+ class Child extends React.Component {
1512
+ state = {value: 'initial'};
1513
+ componentDidMount() {
1514
+ subscribers.push(this);
1515
+ }
1516
+ render() {
1517
+ return null;
1518
+ }
1519
+ }
1520
+
1521
+ class App extends React.Component {
1522
+ render() {
1523
+ const children = [];
1524
+ for (let i = 0; i < 1200; i++) {
1525
+ children.push(<Child key={i} />);
1526
+ }
1527
+ return children;
1528
+ }
1529
+ }
1530
+
1531
+ const container = document.createElement('div');
1532
+ ReactDOM.render(<App />, container);
1533
+
1534
+ ReactDOM.unstable_batchedUpdates(() => {
1535
+ subscribers.forEach(s => {
1536
+ s.setState({value: 'update'});
1537
+ });
1538
+ });
1539
+ });
1540
+
1541
+ // TODO: Replace this branch with @gate pragmas
1542
+ if (__DEV__) {
1543
+ it('can have nested updates if they do not cross the limit', async () => {
1544
+ let _setStep;
1545
+ const LIMIT = 50;
1546
+
1547
+ function Terminating() {
1548
+ const [step, setStep] = React.useState(0);
1549
+ _setStep = setStep;
1550
+ React.useEffect(() => {
1551
+ if (step < LIMIT) {
1552
+ setStep(x => x + 1);
1553
+ }
1554
+ });
1555
+ Scheduler.log(step);
1556
+ return step;
1557
+ }
1558
+
1559
+ const container = document.createElement('div');
1560
+ await act(() => {
1561
+ ReactDOM.render(<Terminating />, container);
1562
+ });
1563
+ expect(container.textContent).toBe('50');
1564
+ await act(() => {
1565
+ _setStep(0);
1566
+ });
1567
+ expect(container.textContent).toBe('50');
1568
+ });
1569
+
1570
+ it('can have many updates inside useEffect without triggering a warning', async () => {
1571
+ function Terminating() {
1572
+ const [step, setStep] = React.useState(0);
1573
+ React.useEffect(() => {
1574
+ for (let i = 0; i < 1000; i++) {
1575
+ setStep(x => x + 1);
1576
+ }
1577
+ Scheduler.log('Done');
1578
+ }, []);
1579
+ return step;
1580
+ }
1581
+
1582
+ const container = document.createElement('div');
1583
+ await act(() => {
1584
+ ReactDOM.render(<Terminating />, container);
1585
+ });
1586
+
1587
+ assertLog(['Done']);
1588
+ expect(container.textContent).toBe('1000');
1589
+ });
1590
+ }
1591
+});
packages/react-dom/src/__tests__/ReactUpdates-test.js
+567
-475
@@ -48,226 +48,283 @@ describe('ReactUpdates', () => {
48
);
49
}
50
51
- it('should batch state when updating state twice', () => {
52
- let updateCount = 0;
53
-
54
- class Component extends React.Component {
55
- state = {x: 0};
56
-
57
- componentDidUpdate() {
58
- updateCount++;
59
- }
51
+ it('should batch state when updating state twice', async () => {
52
+ let componentState;
53
+ let setState;
54
+
55
+ function Component() {
56
+ const [state, _setState] = React.useState(0);
57
+ componentState = state;
58
+ setState = _setState;
59
+ React.useLayoutEffect(() => {
60
+ Scheduler.log('Commit');
61
+ });
62
61
- render() {
62
- return <div>{this.state.x}</div>;
63
- }
63
+ return <div>{state}</div>;
64
}
65
66
- const instance = ReactTestUtils.renderIntoDocument(<Component />);
67
- expect(instance.state.x).toBe(0);
66
+ const container = document.createElement('div');
67
+ const root = ReactDOMClient.createRoot(container);
68
+ await act(() => {
69
+ root.render(<Component />);
70
+ });
71
69
- ReactDOM.unstable_batchedUpdates(function () {
70
- instance.setState({x: 1});
71
- instance.setState({x: 2});
72
- expect(instance.state.x).toBe(0);
73
- expect(updateCount).toBe(0);
72
+ assertLog(['Commit']);
73
+ expect(container.firstChild.textContent).toBe('0');
74
+
75
+ await act(() => {
76
+ setState(1);
77
+ setState(2);
78
+ expect(componentState).toBe(0);
79
+ expect(container.firstChild.textContent).toBe('0');
80
+ assertLog([]);
81
});
82
76
- expect(instance.state.x).toBe(2);
77
- expect(updateCount).toBe(1);
83
+ expect(componentState).toBe(2);
84
+ assertLog(['Commit']);
85
+ expect(container.firstChild.textContent).toBe('2');
86
});
87
80
- it('should batch state when updating two different state keys', () => {
81
- let updateCount = 0;
88
+ it('should batch state when updating two different states', async () => {
89
+ let componentStateA;
90
+ let componentStateB;
91
+ let setStateA;
92
+ let setStateB;
93
83
- class Component extends React.Component {
84
- state = {x: 0, y: 0};
94
+ function Component() {
95
+ const [stateA, _setStateA] = React.useState(0);
96
+ const [stateB, _setStateB] = React.useState(0);
97
+ componentStateA = stateA;
98
+ componentStateB = stateB;
99
+ setStateA = _setStateA;
100
+ setStateB = _setStateB;
101
86
- componentDidUpdate() {
87
- updateCount++;
88
- }
102
+ React.useLayoutEffect(() => {
103
+ Scheduler.log('Commit');
104
+ });
105
90
- render() {
91
- return (
92
- <div>
93
- ({this.state.x}, {this.state.y})
94
- </div>
95
- );
96
- }
106
+ return (
107
+ <div>
108
+ {stateA} {stateB}
109
+ </div>
110
+ );
111
}
112
99
- const instance = ReactTestUtils.renderIntoDocument(<Component />);
100
- expect(instance.state.x).toBe(0);
101
- expect(instance.state.y).toBe(0);
113
+ const container = document.createElement('div');
114
+ const root = ReactDOMClient.createRoot(container);
115
+ await act(() => {
116
+ root.render(<Component />);
117
+ });
118
103
- ReactDOM.unstable_batchedUpdates(function () {
104
- instance.setState({x: 1});
105
- instance.setState({y: 2});
106
- expect(instance.state.x).toBe(0);
107
- expect(instance.state.y).toBe(0);
108
- expect(updateCount).toBe(0);
119
+ assertLog(['Commit']);
120
+ expect(container.firstChild.textContent).toBe('0 0');
121
+
122
+ await act(() => {
123
+ setStateA(1);
124
+ setStateB(2);
125
+ expect(componentStateA).toBe(0);
126
+ expect(componentStateB).toBe(0);
127
+ expect(container.firstChild.textContent).toBe('0 0');
128
+ assertLog([]);
129
});
130
111
- expect(instance.state.x).toBe(1);
112
- expect(instance.state.y).toBe(2);
113
- expect(updateCount).toBe(1);
131
+ expect(componentStateA).toBe(1);
132
+ expect(componentStateB).toBe(2);
133
+ assertLog(['Commit']);
134
+ expect(container.firstChild.textContent).toBe('1 2');
135
});
136
116
- it('should batch state and props together', () => {
117
- let updateCount = 0;
137
+ it('should batch state and props together', async () => {
138
+ let setState;
139
+ let componentProp;
140
+ let componentState;
141
119
- class Component extends React.Component {
120
- state = {y: 0};
142
+ function Component({prop}) {
143
+ const [state, _setState] = React.useState(0);
144
+ componentProp = prop;
145
+ componentState = state;
146
+ setState = _setState;
147
122
- componentDidUpdate() {
123
- updateCount++;
124
- }
148
+ React.useLayoutEffect(() => {
149
+ Scheduler.log('Commit');
150
+ });
151
126
- render() {
127
- return (
128
- <div>
129
- ({this.props.x}, {this.state.y})
130
- </div>
131
- );
132
- }
152
+ return (
153
+ <div>
154
+ {prop} {state}
155
+ </div>
156
+ );
157
}
158
159
const container = document.createElement('div');
136
- const instance = ReactDOM.render(<Component x={0} />, container);
137
- expect(instance.props.x).toBe(0);
138
- expect(instance.state.y).toBe(0);
139
-
140
- ReactDOM.unstable_batchedUpdates(function () {
141
- ReactDOM.render(<Component x={1} />, container);
142
- instance.setState({y: 2});
143
- expect(instance.props.x).toBe(0);
144
- expect(instance.state.y).toBe(0);
145
- expect(updateCount).toBe(0);
160
+ const root = ReactDOMClient.createRoot(container);
161
+ await act(() => {
162
+ root.render(<Component prop={0} />);
163
+ });
164
+
165
+ assertLog(['Commit']);
166
+ expect(container.firstChild.textContent).toBe('0 0');
167
+
168
+ await act(() => {
169
+ root.render(<Component prop={1} />);
170
+ setState(2);
171
+ expect(componentProp).toBe(0);
172
+ expect(componentState).toBe(0);
173
+ expect(container.firstChild.textContent).toBe('0 0');
174
+ assertLog([]);
175
});
176
148
- expect(instance.props.x).toBe(1);
149
- expect(instance.state.y).toBe(2);
150
- expect(updateCount).toBe(1);
177
+ expect(componentProp).toBe(1);
178
+ expect(componentState).toBe(2);
179
+ assertLog(['Commit']);
180
+ expect(container.firstChild.textContent).toBe('1 2');
181
});
182
153
- it('should batch parent/child state updates together', () => {
154
- let parentUpdateCount = 0;
183
+ it('should batch parent/child state updates together', async () => {
184
+ let childRef;
185
+ let parentState;
186
+ let childState;
187
+ let setParentState;
188
+ let setChildState;
189
156
- class Parent extends React.Component {
157
- state = {x: 0};
158
- childRef = React.createRef();
190
+ function Parent() {
191
+ const [state, _setState] = React.useState(0);
192
+ parentState = state;
193
+ setParentState = _setState;
194
160
- componentDidUpdate() {
161
- parentUpdateCount++;
162
- }
195
+ React.useLayoutEffect(() => {
196
+ Scheduler.log('Parent Commit');
197
+ });
198
164
- render() {
165
- return (
166
- <div>
167
- <Child ref={this.childRef} x={this.state.x} />
168
- </div>
169
- );
170
- }
199
+ return (
200
+ <div>
201
+ <Child prop={state} />
202
+ </div>
203
+ );
204
}
205
173
- let childUpdateCount = 0;
174
-
175
- class Child extends React.Component {
176
- state = {y: 0};
206
+ function Child({prop}) {
207
+ const [state, _setState] = React.useState(0);
208
+ childState = state;
209
+ setChildState = _setState;
210
178
- componentDidUpdate() {
179
- childUpdateCount++;
180
- }
211
+ React.useLayoutEffect(() => {
212
+ Scheduler.log('Child Commit');
213
+ });
214
182
- render() {
183
- return <div>{this.props.x + this.state.y}</div>;
184
- }
215
+ return (
216
+ <div
217
+ ref={ref => {
218
+ childRef = ref;
219
+ }}>
220
+ {prop} {state}
221
+ </div>
222
+ );
223
}
224
187
- const instance = ReactTestUtils.renderIntoDocument(<Parent />);
188
- const child = instance.childRef.current;
189
- expect(instance.state.x).toBe(0);
190
- expect(child.state.y).toBe(0);
225
+ const container = document.createElement('div');
226
+ const root = ReactDOMClient.createRoot(container);
227
+ await act(() => {
228
+ root.render(<Parent />);
229
+ });
230
192
- ReactDOM.unstable_batchedUpdates(function () {
193
- instance.setState({x: 1});
194
- child.setState({y: 2});
195
- expect(instance.state.x).toBe(0);
196
- expect(child.state.y).toBe(0);
197
- expect(parentUpdateCount).toBe(0);
198
- expect(childUpdateCount).toBe(0);
231
+ assertLog(['Child Commit', 'Parent Commit']);
232
+ expect(childRef.textContent).toBe('0 0');
233
+
234
+ await act(() => {
235
+ // Parent update first.
236
+ setParentState(1);
237
+ setChildState(2);
238
+ expect(parentState).toBe(0);
239
+ expect(childState).toBe(0);
240
+ expect(childRef.textContent).toBe('0 0');
241
+ assertLog([]);
242
});
243
201
- expect(instance.state.x).toBe(1);
202
- expect(child.state.y).toBe(2);
203
- expect(parentUpdateCount).toBe(1);
204
- expect(childUpdateCount).toBe(1);
244
+ expect(parentState).toBe(1);
245
+ expect(childState).toBe(2);
246
+ expect(childRef.textContent).toBe('1 2');
247
+ assertLog(['Child Commit', 'Parent Commit']);
248
});
249
207
- it('should batch child/parent state updates together', () => {
208
- let parentUpdateCount = 0;
250
+ it('should batch child/parent state updates together', async () => {
251
+ let childRef;
252
+ let parentState;
253
+ let childState;
254
+ let setParentState;
255
+ let setChildState;
256
210
- class Parent extends React.Component {
211
- state = {x: 0};
212
- childRef = React.createRef();
257
+ function Parent() {
258
+ const [state, _setState] = React.useState(0);
259
+ parentState = state;
260
+ setParentState = _setState;
261
214
- componentDidUpdate() {
215
- parentUpdateCount++;
216
- }
262
+ React.useLayoutEffect(() => {
263
+ Scheduler.log('Parent Commit');
264
+ });
265
218
- render() {
219
- return (
220
- <div>
221
- <Child ref={this.childRef} x={this.state.x} />
222
- </div>
223
- );
224
- }
266
+ return (
267
+ <div>
268
+ <Child prop={state} />
269
+ </div>
270
+ );
271
}
272
227
- let childUpdateCount = 0;
273
+ function Child({prop}) {
274
+ const [state, _setState] = React.useState(0);
275
+ childState = state;
276
+ setChildState = _setState;
277
229
- class Child extends React.Component {
230
- state = {y: 0};
231
-
232
- componentDidUpdate() {
233
- childUpdateCount++;
234
- }
278
+ React.useLayoutEffect(() => {
279
+ Scheduler.log('Child Commit');
280
+ });
281
236
- render() {
237
- return <div>{this.props.x + this.state.y}</div>;
238
- }
282
+ return (
283
+ <div
284
+ ref={ref => {
285
+ childRef = ref;
286
+ }}>
287
+ {prop} {state}
288
+ </div>
289
+ );
290
}
291
241
- const instance = ReactTestUtils.renderIntoDocument(<Parent />);
242
- const child = instance.childRef.current;
243
- expect(instance.state.x).toBe(0);
244
- expect(child.state.y).toBe(0);
245
-
246
- ReactDOM.unstable_batchedUpdates(function () {
247
- child.setState({y: 2});
248
- instance.setState({x: 1});
249
- expect(instance.state.x).toBe(0);
250
- expect(child.state.y).toBe(0);
251
- expect(parentUpdateCount).toBe(0);
252
- expect(childUpdateCount).toBe(0);
292
+ const container = document.createElement('div');
293
+ const root = ReactDOMClient.createRoot(container);
294
+ await act(() => {
295
+ root.render(<Parent />);
296
});
297
255
- expect(instance.state.x).toBe(1);
256
- expect(child.state.y).toBe(2);
257
- expect(parentUpdateCount).toBe(1);
298
+ assertLog(['Child Commit', 'Parent Commit']);
299
+ expect(childRef.textContent).toBe('0 0');
300
+
301
+ await act(() => {
302
+ // Child update first.
303
+ setChildState(2);
304
+ setParentState(1);
305
+ expect(parentState).toBe(0);
306
+ expect(childState).toBe(0);
307
+ expect(childRef.textContent).toBe('0 0');
308
+ assertLog([]);
309
+ });
310
259
- // Batching reduces the number of updates here to 1.
260
- expect(childUpdateCount).toBe(1);
311
+ expect(parentState).toBe(1);
312
+ expect(childState).toBe(2);
313
+ expect(childRef.textContent).toBe('1 2');
314
+ assertLog(['Child Commit', 'Parent Commit']);
315
});
316
263
- it('should support chained state updates', () => {
264
- let updateCount = 0;
265
-
317
+ it('should support chained state updates', async () => {
318
+ let instance;
319
class Component extends React.Component {
320
state = {x: 0};
321
+ constructor(props) {
322
+ super(props);
323
+ instance = this;
324
+ }
325
326
componentDidUpdate() {
270
- updateCount++;
327
+ Scheduler.log('Update');
328
}
329
330
render() {
@@ -275,43 +332,55 @@ describe('ReactUpdates', () => {
332
}
333
}
334
278
- const instance = ReactTestUtils.renderIntoDocument(<Component />);
335
+ const container = document.createElement('div');
336
+ const root = ReactDOMClient.createRoot(container);
337
+ await act(() => {
338
+ root.render(<Component />);
339
+ });
340
+
341
expect(instance.state.x).toBe(0);
342
+ expect(container.firstChild.textContent).toBe('0');
343
344
let innerCallbackRun = false;
282
- ReactDOM.unstable_batchedUpdates(function () {
345
+ await act(() => {
346
instance.setState({x: 1}, function () {
347
instance.setState({x: 2}, function () {
285
- expect(this).toBe(instance);
348
innerCallbackRun = true;
349
expect(instance.state.x).toBe(2);
288
- expect(updateCount).toBe(2);
350
+ expect(container.firstChild.textContent).toBe('2');
351
+ assertLog(['Update']);
352
});
353
expect(instance.state.x).toBe(1);
291
- expect(updateCount).toBe(1);
354
+ expect(container.firstChild.textContent).toBe('1');
355
+ assertLog(['Update']);
356
});
357
expect(instance.state.x).toBe(0);
294
- expect(updateCount).toBe(0);
358
+ expect(container.firstChild.textContent).toBe('0');
359
+ assertLog([]);
360
});
361
297
- expect(innerCallbackRun).toBeTruthy();
362
+ assertLog([]);
363
expect(instance.state.x).toBe(2);
299
- expect(updateCount).toBe(2);
364
+ expect(innerCallbackRun).toBeTruthy();
365
+ expect(container.firstChild.textContent).toBe('2');
366
});
367
302
- it('should batch forceUpdate together', () => {
368
+ it('should batch forceUpdate together', async () => {
369
+ let instance;
370
let shouldUpdateCount = 0;
304
- let updateCount = 0;
305
-
371
class Component extends React.Component {
372
state = {x: 0};
373
374
+ constructor(props) {
375
+ super(props);
376
+ instance = this;
377
+ }
378
shouldComponentUpdate() {
379
shouldUpdateCount++;
380
}
381
382
componentDidUpdate() {
314
- updateCount++;
383
+ Scheduler.log('Update');
384
}
385
386
render() {
@@ -319,80 +388,82 @@ describe('ReactUpdates', () => {
388
}
389
}
390
322
- const instance = ReactTestUtils.renderIntoDocument(<Component />);
391
+ const container = document.createElement('div');
392
+ const root = ReactDOMClient.createRoot(container);
393
+ await act(() => {
394
+ root.render(<Component />);
395
+ });
396
+
397
+ assertLog([]);
398
expect(instance.state.x).toBe(0);
399
325
- let callbacksRun = 0;
326
- ReactDOM.unstable_batchedUpdates(function () {
400
+ await act(() => {
401
instance.setState({x: 1}, function () {
328
- callbacksRun++;
402
+ Scheduler.log('callback');
403
});
404
instance.forceUpdate(function () {
331
- callbacksRun++;
405
+ Scheduler.log('forceUpdate');
406
});
407
+ assertLog([]);
408
expect(instance.state.x).toBe(0);
334
- expect(updateCount).toBe(0);
409
+ expect(container.firstChild.textContent).toBe('0');
410
});
411
337
- expect(callbacksRun).toBe(2);
412
// shouldComponentUpdate shouldn't be called since we're forcing
413
expect(shouldUpdateCount).toBe(0);
414
+ assertLog(['Update', 'callback', 'forceUpdate']);
415
expect(instance.state.x).toBe(1);
341
- expect(updateCount).toBe(1);
416
+ expect(container.firstChild.textContent).toBe('1');
417
});
418
344
- it('should update children even if parent blocks updates', () => {
345
- let parentRenderCount = 0;
346
- let childRenderCount = 0;
347
-
419
+ it('should update children even if parent blocks updates', async () => {
420
+ let instance;
421
class Parent extends React.Component {
422
childRef = React.createRef();
423
424
+ constructor(props) {
425
+ super(props);
426
+ instance = this;
427
+ }
428
shouldComponentUpdate() {
429
return false;
430
}
431
432
render() {
356
- parentRenderCount++;
433
+ Scheduler.log('Parent render');
434
return <Child ref={this.childRef} />;
435
}
436
}
437
438
class Child extends React.Component {
439
render() {
363
- childRenderCount++;
440
+ Scheduler.log('Child render');
441
return <div />;
442
}
443
}
444
368
- expect(parentRenderCount).toBe(0);
369
- expect(childRenderCount).toBe(0);
370
-
371
- let instance = <Parent />;
372
- instance = ReactTestUtils.renderIntoDocument(instance);
445
+ const container = document.createElement('div');
446
+ const root = ReactDOMClient.createRoot(container);
447
+ await act(() => {
448
+ root.render(<Parent />);
449
+ });
450
374
- expect(parentRenderCount).toBe(1);
375
- expect(childRenderCount).toBe(1);
451
+ assertLog(['Parent render', 'Child render']);
452
377
- ReactDOM.unstable_batchedUpdates(function () {
453
+ await act(() => {
454
instance.setState({x: 1});
455
});
456
381
- expect(parentRenderCount).toBe(1);
382
- expect(childRenderCount).toBe(1);
457
+ assertLog([]);
458
384
- ReactDOM.unstable_batchedUpdates(function () {
459
+ await act(() => {
460
instance.childRef.current.setState({x: 1});
461
});
462
388
- expect(parentRenderCount).toBe(1);
389
- expect(childRenderCount).toBe(2);
463
+ assertLog(['Child render']);
464
});
465
392
- it('should not reconcile children passed via props', () => {
393
- let numMiddleRenders = 0;
394
- let numBottomRenders = 0;
395
-
466
+ it('should not reconcile children passed via props', async () => {
467
class Top extends React.Component {
468
render() {
469
return (
@@ -409,26 +480,31 @@ describe('ReactUpdates', () => {
480
}
481
482
render() {
412
- numMiddleRenders++;
483
+ Scheduler.log('Middle');
484
return React.Children.only(this.props.children);
485
}
486
}
487
488
class Bottom extends React.Component {
489
render() {
419
- numBottomRenders++;
490
+ Scheduler.log('Bottom');
491
return null;
492
}
493
}
494
424
- ReactTestUtils.renderIntoDocument(<Top />);
425
- expect(numMiddleRenders).toBe(2);
426
- expect(numBottomRenders).toBe(1);
495
+ const container = document.createElement('div');
496
+ const root = ReactDOMClient.createRoot(container);
497
+ await act(() => {
498
+ root.render(<Top />);
499
+ });
500
+
501
+ assertLog(['Middle', 'Bottom', 'Middle']);
502
});
503
429
- it('should flow updates correctly', () => {
504
+ it('should flow updates correctly', async () => {
505
let willUpdates = [];
506
let didUpdates = [];
507
+ let instance;
508
509
const UpdateLoggingMixin = {
510
UNSAFE_componentWillUpdate: function () {
@@ -482,7 +558,10 @@ describe('ReactUpdates', () => {
558
class App extends React.Component {
559
switcherRef = React.createRef();
560
childRef = React.createRef();
485
-
561
+ constructor(props) {
562
+ super(props);
563
+ instance = this;
564
+ }
565
render() {
566
return (
567
<Switcher ref={this.switcherRef}>
@@ -493,8 +572,10 @@ describe('ReactUpdates', () => {
572
}
573
Object.assign(App.prototype, UpdateLoggingMixin);
574
496
- let root = <App />;
497
- root = ReactTestUtils.renderIntoDocument(root);
575
+ const container = document.createElement('div');
576
+ await act(() => {
577
+ ReactDOMClient.createRoot(container).render(<App />);
578
+ });
579
580
function expectUpdates(desiredWillUpdates, desiredDidUpdates) {
581
let i;
@@ -512,10 +593,14 @@ describe('ReactUpdates', () => {
593
c.setState({x: 1});
594
}
595
515
- function testUpdates(components, desiredWillUpdates, desiredDidUpdates) {
596
+ async function testUpdates(
597
+ components,
598
+ desiredWillUpdates,
599
+ desiredDidUpdates,
600
+ ) {
601
let i;
602
518
- ReactDOM.unstable_batchedUpdates(function () {
603
+ await act(() => {
604
for (i = 0; i < components.length; i++) {
605
triggerUpdate(components[i]);
606
}
@@ -525,7 +610,7 @@ describe('ReactUpdates', () => {
610
611
// Try them in reverse order
612
528
- ReactDOM.unstable_batchedUpdates(function () {
613
+ await act(() => {
614
for (i = components.length - 1; i >= 0; i--) {
615
triggerUpdate(components[i]);
616
}
@@ -533,42 +618,48 @@ describe('ReactUpdates', () => {
618
619
expectUpdates(desiredWillUpdates, desiredDidUpdates);
620
}
536
- testUpdates(
537
- [root.switcherRef.current.boxRef.current, root.switcherRef.current],
621
+ await testUpdates(
622
+ [
623
+ instance.switcherRef.current.boxRef.current,
624
+ instance.switcherRef.current,
625
+ ],
626
// Owner-child relationships have inverse will and did
627
['Switcher', 'Box'],
628
['Box', 'Switcher'],
629
);
630
543
- testUpdates(
544
- [root.childRef.current, root.switcherRef.current.boxRef.current],
631
+ await testUpdates(
632
+ [instance.childRef.current, instance.switcherRef.current.boxRef.current],
633
// Not owner-child so reconcile independently
634
['Box', 'Child'],
635
['Box', 'Child'],
636
);
637
550
- testUpdates(
551
- [root.childRef.current, root.switcherRef.current],
638
+ await testUpdates(
639
+ [instance.childRef.current, instance.switcherRef.current],
640
// Switcher owns Box and Child, Box does not own Child
641
['Switcher', 'Box', 'Child'],
642
['Box', 'Switcher', 'Child'],
643
);
644
});
645
558
- it('should queue mount-ready handlers across different roots', () => {
646
+ it('should queue mount-ready handlers across different roots', async () => {
647
// We'll define two components A and B, then update both of them. When A's
648
// componentDidUpdate handlers is called, B's DOM should already have been
649
// updated.
650
651
const bContainer = document.createElement('div');
564
-
652
+ let a;
653
let b;
654
655
let aUpdated = false;
656
657
class A extends React.Component {
658
state = {x: 0};
571
-
659
+ constructor(props) {
660
+ super(props);
661
+ a = this;
662
+ }
663
componentDidUpdate() {
664
expect(ReactDOM.findDOMNode(b).textContent).toBe('B1');
665
aUpdated = true;
@@ -576,7 +667,6 @@ describe('ReactUpdates', () => {
667
668
render() {
669
let portal = null;
579
- // If we're using Fiber, we use Portals instead to achieve this.
670
portal = ReactDOM.createPortal(<B ref={n => (b = n)} />, bContainer);
671
return (
672
<div>
@@ -595,8 +685,13 @@ describe('ReactUpdates', () => {
685
}
686
}
687
598
- const a = ReactTestUtils.renderIntoDocument(<A />);
599
- ReactDOM.unstable_batchedUpdates(function () {
688
+ const container = document.createElement('div');
689
+ const root = ReactDOMClient.createRoot(container);
690
+ await act(() => {
691
+ root.render(<A />);
692
+ });
693
+
694
+ await act(() => {
695
a.setState({x: 1});
696
b.setState({x: 1});
697
});
@@ -604,13 +699,16 @@ describe('ReactUpdates', () => {
699
expect(aUpdated).toBe(true);
700
});
701
607
- it('should flush updates in the correct order', () => {
702
+ it('should flush updates in the correct order', async () => {
703
const updates = [];
609
-
704
+ let instance;
705
class Outer extends React.Component {
706
state = {x: 0};
707
innerRef = React.createRef();
613
-
708
+ constructor(props) {
709
+ super(props);
710
+ instance = this;
711
+ }
712
render() {
713
updates.push('Outer-render-' + this.state.x);
714
return (
@@ -643,14 +741,20 @@ describe('ReactUpdates', () => {
741
}
742
}
743
646
- const instance = ReactTestUtils.renderIntoDocument(<Outer />);
744
+ const container = document.createElement('div');
745
+ const root = ReactDOMClient.createRoot(container);
746
+ await act(() => {
747
+ root.render(<Outer />);
748
+ });
749
648
- updates.push('Outer-setState-1');
649
- instance.setState({x: 1}, function () {
650
- updates.push('Outer-callback-1');
651
- updates.push('Outer-setState-2');
652
- instance.setState({x: 2}, function () {
653
- updates.push('Outer-callback-2');
750
+ await act(() => {
751
+ updates.push('Outer-setState-1');
752
+ instance.setState({x: 1}, function () {
753
+ updates.push('Outer-callback-1');
754
+ updates.push('Outer-setState-2');
755
+ instance.setState({x: 2}, function () {
756
+ updates.push('Outer-callback-2');
757
+ });
758
});
759
});
760
@@ -686,7 +790,7 @@ describe('ReactUpdates', () => {
790
/* eslint-enable indent */
791
});
792
689
- it('should flush updates in the correct order across roots', () => {
793
+ it('should flush updates in the correct order across roots', async () => {
794
const instances = [];
795
const updates = [];
796
@@ -699,22 +803,26 @@ describe('ReactUpdates', () => {
803
componentDidMount() {
804
instances.push(this);
805
if (this.props.depth < this.props.count) {
702
- ReactDOM.render(
806
+ const root = ReactDOMClient.createRoot(ReactDOM.findDOMNode(this));
807
+ root.render(
808
<MockComponent
809
depth={this.props.depth + 1}
810
count={this.props.count}
811
/>,
707
- ReactDOM.findDOMNode(this),
812
);
813
}
814
}
815
}
816
713
- ReactTestUtils.renderIntoDocument(<MockComponent depth={0} count={2} />);
817
+ const container = document.createElement('div');
818
+ const root = ReactDOMClient.createRoot(container);
819
+ await act(() => {
820
+ root.render(<MockComponent depth={0} count={2} />);
821
+ });
822
823
expect(updates).toEqual([0, 1, 2]);
824
717
- ReactDOM.unstable_batchedUpdates(function () {
825
+ await act(() => {
826
// Simulate update on each component from top to bottom.
827
instances.forEach(function (instance) {
828
instance.forceUpdate();
@@ -777,7 +885,7 @@ describe('ReactUpdates', () => {
885
expect(ReactDOM.findDOMNode(x).textContent).toBe('1');
886
});
887
780
- it('should queue updates from during mount', () => {
888
+ it('should queue updates from during mount', async () => {
889
// See https://github.com/facebook/react/issues/1353
890
let a;
891
@@ -803,8 +911,11 @@ describe('ReactUpdates', () => {
911
}
912
}
913
806
- ReactDOM.unstable_batchedUpdates(function () {
807
- ReactTestUtils.renderIntoDocument(
914
+ const container = document.createElement('div');
915
+ const root = ReactDOMClient.createRoot(container);
916
+
917
+ await act(() => {
918
+ root.render(
919
<div>
920
<A />
921
<B />
@@ -812,13 +923,10 @@ describe('ReactUpdates', () => {
923
);
924
});
925
815
- expect(a.state.x).toBe(1);
816
- expect(ReactDOM.findDOMNode(a).textContent).toBe('A1');
926
+ expect(container.firstChild.textContent).toBe('A1');
927
});
928
819
- it('calls componentWillReceiveProps setState callback properly', () => {
820
- let callbackCount = 0;
821
-
929
+ it('calls componentWillReceiveProps setState callback properly', async () => {
930
class A extends React.Component {
931
state = {x: this.props.x};
932
@@ -827,7 +935,7 @@ describe('ReactUpdates', () => {
935
this.setState({x: newX}, function () {
936
// State should have updated by the time this callback gets called
937
expect(this.state.x).toBe(newX);
830
- callbackCount++;
938
+ Scheduler.log('Callback');
939
});
940
}
941
@@ -837,13 +945,22 @@ describe('ReactUpdates', () => {
945
}
946
947
const container = document.createElement('div');
840
- ReactDOM.render(<A x={1} />, container);
841
- ReactDOM.render(<A x={2} />, container);
842
- expect(callbackCount).toBe(1);
948
+ const root = ReactDOMClient.createRoot(container);
949
+ await act(() => {
950
+ root.render(<A x={1} />);
951
+ });
952
+ assertLog([]);
953
+
954
+ // Needs to be a separate act, or it will be batched.
955
+ await act(() => {
956
+ root.render(<A x={2} />);
957
+ });
958
+
959
+ assertLog(['Callback']);
960
});
961
845
- it('does not call render after a component as been deleted', () => {
846
- let renderCount = 0;
962
+ it('does not call render after a component as been deleted', async () => {
963
+ let componentA = null;
964
let componentB = null;
965
966
class B extends React.Component {
@@ -854,7 +971,7 @@ describe('ReactUpdates', () => {
971
}
972
973
render() {
857
- renderCount++;
974
+ Scheduler.log('B');
975
return <div />;
976
}
977
}
@@ -862,21 +979,29 @@ describe('ReactUpdates', () => {
979
class A extends React.Component {
980
state = {showB: true};
981
982
+ componentDidMount() {
983
+ componentA = this;
984
+ }
985
render() {
986
return this.state.showB ? <B /> : <div />;
987
}
988
}
989
870
- const component = ReactTestUtils.renderIntoDocument(<A />);
990
+ const container = document.createElement('div');
991
+ const root = ReactDOMClient.createRoot(container);
992
+ await act(() => {
993
+ root.render(<A />);
994
+ });
995
+ assertLog(['B']);
996
872
- ReactDOM.unstable_batchedUpdates(function () {
997
+ await act(() => {
998
// B will have scheduled an update but the batching should ensure that its
999
// update never fires.
1000
componentB.setState({updates: 1});
876
- component.setState({showB: false});
1001
+ componentA.setState({showB: false});
1002
});
1003
879
- expect(renderCount).toBe(1);
1004
+ assertLog([]);
1005
});
1006
1007
it('throws in setState if the update callback is not a function', () => {
@@ -965,10 +1090,14 @@ describe('ReactUpdates', () => {
1090
);
1091
});
1092
968
- it('does not update one component twice in a batch (#2410)', () => {
1093
+ it('does not update one component twice in a batch (#2410)', async () => {
1094
+ let parent;
1095
class Parent extends React.Component {
1096
childRef = React.createRef();
1097
1098
+ componentDidMount() {
1099
+ parent = this;
1100
+ }
1101
getChild = () => {
1102
return this.childRef.current;
1103
};
@@ -1009,15 +1138,22 @@ describe('ReactUpdates', () => {
1138
}
1139
}
1140
1012
- const parent = ReactTestUtils.renderIntoDocument(<Parent />);
1141
+ const container = document.createElement('div');
1142
+ const root = ReactDOMClient.createRoot(container);
1143
+ await act(() => {
1144
+ root.render(<Parent />);
1145
+ });
1146
+
1147
const child = parent.getChild();
1014
- ReactDOM.unstable_batchedUpdates(function () {
1148
+ await act(() => {
1149
parent.forceUpdate();
1150
child.forceUpdate();
1151
});
1152
+
1153
+ expect.assertions(6);
1154
});
1155
1020
- it('does not update one component twice in a batch (#6371)', () => {
1156
+ it('does not update one component twice in a batch (#6371)', async () => {
1157
let callbacks = [];
1158
function emitChange() {
1159
callbacks.forEach(c => c());
@@ -1064,34 +1200,23 @@ describe('ReactUpdates', () => {
1200
}
1201
}
1202
1067
- ReactDOM.render(<App />, document.createElement('div'));
1068
- });
1069
-
1070
- it('unstable_batchedUpdates should return value from a callback', () => {
1071
- const result = ReactDOM.unstable_batchedUpdates(function () {
1072
- return 42;
1203
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
1204
+ await act(() => {
1205
+ root.render(<App />);
1206
});
1074
- expect(result).toEqual(42);
1075
- });
1207
1077
- it('unmounts and remounts a root in the same batch', () => {
1078
- const container = document.createElement('div');
1079
- ReactDOM.render(<span>a</span>, container);
1080
- ReactDOM.unstable_batchedUpdates(function () {
1081
- ReactDOM.unmountComponentAtNode(container);
1082
- ReactDOM.render(<span>b</span>, container);
1083
- });
1084
- expect(container.textContent).toBe('b');
1208
+ // Error should not be thrown.
1209
+ expect(true).toBe(true);
1210
});
1211
1087
- it('handles reentrant mounting in synchronous mode', () => {
1088
- let mounts = 0;
1212
+ it('handles reentrant mounting in synchronous mode', async () => {
1213
+ let onChangeCalled = false;
1214
class Editor extends React.Component {
1215
render() {
1216
return <div>{this.props.text}</div>;
1217
}
1218
componentDidMount() {
1094
- mounts++;
1219
+ Scheduler.log('Mount');
1220
// This should be called only once but we guard just in case.
1221
if (!this.props.rendered) {
1222
this.props.onChange({rendered: true});
@@ -1100,163 +1225,57 @@ describe('ReactUpdates', () => {
1225
}
1226
1227
const container = document.createElement('div');
1228
+ const root = ReactDOMClient.createRoot(container);
1229
function render() {
1104
- ReactDOM.render(
1230
+ root.render(
1231
<Editor
1232
onChange={newProps => {
1233
+ onChangeCalled = true;
1234
props = {...props, ...newProps};
1235
render();
1236
}}
1237
{...props}
1238
/>,
1112
- container,
1239
);
1240
}
1241
1242
let props = {text: 'hello', rendered: false};
1117
- render();
1243
+ await act(() => {
1244
+ render();
1245
+ });
1246
+ assertLog(['Mount']);
1247
props = {...props, text: 'goodbye'};
1119
- render();
1248
+ await act(() => {
1249
+ render();
1250
+ });
1251
+
1252
+ assertLog([]);
1253
expect(container.textContent).toBe('goodbye');
1121
- expect(mounts).toBe(1);
1254
+ expect(onChangeCalled).toBeTruthy();
1255
});
1256
1124
- it('mounts and unmounts are sync even in a batch', () => {
1125
- const ops = [];
1257
+ it('mounts and unmounts are batched', async () => {
1258
const container = document.createElement('div');
1127
- ReactDOM.unstable_batchedUpdates(() => {
1128
- ReactDOM.render(<div>Hello</div>, container);
1129
- ops.push(container.textContent);
1130
- ReactDOM.unmountComponentAtNode(container);
1131
- ops.push(container.textContent);
1132
- });
1133
- expect(ops).toEqual(['Hello', '']);
1134
- });
1135
-
1136
- it(
1137
- 'in legacy mode, updates in componentWillUpdate and componentDidUpdate ' +
1138
- 'should both flush in the immediately subsequent commit',
1139
- () => {
1140
- const ops = [];
1141
- class Foo extends React.Component {
1142
- state = {a: false, b: false};
1143
- UNSAFE_componentWillUpdate(_, nextState) {
1144
- if (!nextState.a) {
1145
- this.setState({a: true});
1146
- }
1147
- }
1148
- componentDidUpdate() {
1149
- ops.push('Foo updated');
1150
- if (!this.state.b) {
1151
- this.setState({b: true});
1152
- }
1153
- }
1154
- render() {
1155
- ops.push(`a: ${this.state.a}, b: ${this.state.b}`);
1156
- return null;
1157
- }
1158
- }
1159
-
1160
- const container = document.createElement('div');
1161
- // Mount
1162
- ReactDOM.render(<Foo />, container);
1163
- // Root update
1164
- ReactDOM.render(<Foo />, container);
1165
- expect(ops).toEqual([
1166
- // Mount
1167
- 'a: false, b: false',
1168
- // Root update
1169
- 'a: false, b: false',
1170
- 'Foo updated',
1171
- // Subsequent update (both a and b should have flushed)
1172
- 'a: true, b: true',
1173
- 'Foo updated',
1174
- // There should not be any additional updates
1175
- ]);
1176
- },
1177
- );
1178
-
1179
- it(
1180
- 'in legacy mode, updates in componentWillUpdate and componentDidUpdate ' +
1181
- '(on a sibling) should both flush in the immediately subsequent commit',
1182
- () => {
1183
- const ops = [];
1184
- class Foo extends React.Component {
1185
- state = {a: false};
1186
- UNSAFE_componentWillUpdate(_, nextState) {
1187
- if (!nextState.a) {
1188
- this.setState({a: true});
1189
- }
1190
- }
1191
- componentDidUpdate() {
1192
- ops.push('Foo updated');
1193
- }
1194
- render() {
1195
- ops.push(`a: ${this.state.a}`);
1196
- return null;
1197
- }
1198
- }
1259
+ const root = ReactDOMClient.createRoot(container);
1260
1200
- class Bar extends React.Component {
1201
- state = {b: false};
1202
- componentDidUpdate() {
1203
- ops.push('Bar updated');
1204
- if (!this.state.b) {
1205
- this.setState({b: true});
1206
- }
1207
- }
1208
- render() {
1209
- ops.push(`b: ${this.state.b}`);
1210
- return null;
1211
- }
1212
- }
1261
+ await act(() => {
1262
+ root.render(<div>Hello</div>);
1263
+ expect(container.textContent).toBe('');
1264
+ root.unmount(container);
1265
+ expect(container.textContent).toBe('');
1266
+ });
1267
1214
- const container = document.createElement('div');
1215
- // Mount
1216
- ReactDOM.render(
1217
- <div>
1218
- <Foo />
1219
- <Bar />
1220
- </div>,
1221
- container,
1222
- );
1223
- // Root update
1224
- ReactDOM.render(
1225
- <div>
1226
- <Foo />
1227
- <Bar />
1228
- </div>,
1229
- container,
1230
- );
1231
- expect(ops).toEqual([
1232
- // Mount
1233
- 'a: false',
1234
- 'b: false',
1235
- // Root update
1236
- 'a: false',
1237
- 'b: false',
1238
- 'Foo updated',
1239
- 'Bar updated',
1240
- // Subsequent update (both a and b should have flushed)
1241
- 'a: true',
1242
- 'b: true',
1243
- 'Foo updated',
1244
- 'Bar updated',
1245
- // There should not be any additional updates
1246
- ]);
1247
- },
1248
- );
1249
-
1250
- it('uses correct base state for setState inside render phase', () => {
1251
- const ops = [];
1268
+ expect(container.textContent).toBe('');
1269
+ });
1270
1271
+ it('uses correct base state for setState inside render phase', async () => {
1272
class Foo extends React.Component {
1273
state = {step: 0};
1274
render() {
1275
const memoizedStep = this.state.step;
1276
this.setState(baseState => {
1277
const baseStep = baseState.step;
1259
- ops.push(`base: ${baseStep}, memoized: ${memoizedStep}`);
1278
+ Scheduler.log(`base: ${baseStep}, memoized: ${memoizedStep}`);
1279
return baseStep === 0 ? {step: 1} : null;
1280
});
1281
return null;
@@ -1264,48 +1283,54 @@ describe('ReactUpdates', () => {
1283
}
1284
1285
const container = document.createElement('div');
1267
- expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
1268
- 'Cannot update during an existing state transition',
1269
- );
1270
- expect(ops).toEqual(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
1286
+ const root = ReactDOMClient.createRoot(container);
1287
+ await expect(async () => {
1288
+ await act(() => {
1289
+ root.render(<Foo />);
1290
+ });
1291
+ }).toErrorDev('Cannot update during an existing state transition');
1292
+
1293
+ assertLog(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
1294
});
1295
1273
- it('does not re-render if state update is null', () => {
1296
+ it('does not re-render if state update is null', async () => {
1297
const container = document.createElement('div');
1298
1299
let instance;
1277
- let ops = [];
1300
class Foo extends React.Component {
1301
render() {
1302
instance = this;
1281
- ops.push('render');
1303
+ Scheduler.log('render');
1304
return <div />;
1305
}
1306
}
1285
- ReactDOM.render(<Foo />, container);
1307
+ const root = ReactDOMClient.createRoot(container);
1308
+ await act(() => {
1309
+ root.render(<Foo />);
1310
+ });
1311
1287
- ops = [];
1288
- instance.setState(() => null);
1289
- expect(ops).toEqual([]);
1312
+ assertLog(['render']);
1313
+ await act(() => {
1314
+ instance.setState(() => null);
1315
+ });
1316
+ assertLog([]);
1317
});
1318
1292
- // Will change once we switch to async by default
1293
- it('synchronously renders hidden subtrees', () => {
1319
+ it('synchronously renders hidden subtrees', async () => {
1320
const container = document.createElement('div');
1295
- let ops = [];
1321
1322
function Baz() {
1298
- ops.push('Baz');
1323
+ Scheduler.log('Baz');
1324
return null;
1325
}
1326
1327
function Bar() {
1303
- ops.push('Bar');
1328
+ Scheduler.log('Bar');
1329
return null;
1330
}
1331
1332
function Foo() {
1308
- ops.push('Foo');
1333
+ Scheduler.log('Foo');
1334
return (
1335
<div>
1336
<div hidden={true}>
@@ -1316,14 +1341,18 @@ describe('ReactUpdates', () => {
1341
);
1342
}
1343
1319
- // Mount
1320
- ReactDOM.render(<Foo />, container);
1321
- expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
1322
- ops = [];
1344
+ const root = ReactDOMClient.createRoot(container);
1345
+ await act(() => {
1346
+ // Mount
1347
+ root.render(<Foo />);
1348
+ });
1349
+ assertLog(['Foo', 'Bar', 'Baz']);
1350
1324
- // Update
1325
- ReactDOM.render(<Foo />, container);
1326
- expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
1351
+ await act(() => {
1352
+ // Update
1353
+ root.render(<Foo />);
1354
+ });
1355
+ assertLog(['Foo', 'Bar', 'Baz']);
1356
});
1357
1358
// @gate www
@@ -1383,18 +1412,33 @@ describe('ReactUpdates', () => {
1412
expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
1413
});
1414
1386
- it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
1415
+ it('can render ridiculously large number of roots without triggering infinite update loop error', async () => {
1416
+ function Component({trigger}) {
1417
+ const [state, setState] = React.useState(0);
1418
+
1419
+ React.useEffect(() => {
1420
+ if (trigger) {
1421
+ Scheduler.log('Trigger');
1422
+ setState(c => c + 1);
1423
+ }
1424
+ }, [trigger]);
1425
+
1426
+ return <div>{state}</div>;
1427
+ }
1428
+
1429
class Foo extends React.Component {
1430
componentDidMount() {
1431
const limit = 1200;
1432
for (let i = 0; i < limit; i++) {
1433
if (i < limit - 1) {
1392
- ReactDOM.render(<div />, document.createElement('div'));
1434
+ ReactDOMClient.createRoot(document.createElement('div')).render(
1435
+ <Component />,
1436
+ );
1437
} else {
1394
- ReactDOM.render(<div />, document.createElement('div'), () => {
1395
- // The "nested update limit" error isn't thrown until setState
1396
- this.setState({});
1397
- });
1438
+ // The "nested update limit" error isn't thrown until setState
1439
+ ReactDOMClient.createRoot(document.createElement('div')).render(
1440
+ <Component trigger={true} />,
1441
+ );
1442
}
1443
}
1444
}
@@ -1403,11 +1447,16 @@ describe('ReactUpdates', () => {
1447
}
1448
}
1449
1406
- const container = document.createElement('div');
1407
- ReactDOM.render(<Foo />, container);
1450
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
1451
+ await act(() => {
1452
+ root.render(<Foo />);
1453
+ });
1454
+
1455
+ // Make sure the setState trigger runs.
1456
+ assertLog(['Trigger']);
1457
});
1458
1410
- it('resets the update counter for unrelated updates', () => {
1459
+ it('resets the update counter for unrelated updates', async () => {
1460
const container = document.createElement('div');
1461
const ref = React.createRef();
1462
@@ -1427,22 +1476,35 @@ describe('ReactUpdates', () => {
1476
}
1477
1478
let limit = 55;
1479
+ const root = ReactDOMClient.createRoot(container);
1480
expect(() => {
1431
- ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1481
+ ReactDOM.flushSync(() => {
1482
+ root.render(<EventuallyTerminating ref={ref} />);
1483
+ });
1484
}).toThrow('Maximum');
1485
1486
// Verify that we don't go over the limit if these updates are unrelated.
1487
limit -= 10;
1436
- ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1488
+ await act(() => {
1489
+ root.render(<EventuallyTerminating ref={ref} />);
1490
+ });
1491
expect(container.textContent).toBe(limit.toString());
1438
- ref.current.setState({step: 0});
1492
+
1493
+ await act(() => {
1494
+ ref.current.setState({step: 0});
1495
+ });
1496
expect(container.textContent).toBe(limit.toString());
1440
- ref.current.setState({step: 0});
1497
+
1498
+ await act(() => {
1499
+ ref.current.setState({step: 0});
1500
+ });
1501
expect(container.textContent).toBe(limit.toString());
1502
1503
limit += 10;
1504
expect(() => {
1445
- ref.current.setState({step: 0});
1505
+ ReactDOM.flushSync(() => {
1506
+ ref.current.setState({step: 0});
1507
+ });
1508
}).toThrow('Maximum');
1509
expect(ref.current).toBe(null);
1510
});
@@ -1450,12 +1512,15 @@ describe('ReactUpdates', () => {
1512
it('does not fall into an infinite update loop', () => {
1513
class NonTerminating extends React.Component {
1514
state = {step: 0};
1515
+
1516
componentDidMount() {
1517
this.setState({step: 1});
1518
}
1456
- UNSAFE_componentWillUpdate() {
1519
+
1520
+ componentDidUpdate() {
1521
this.setState({step: 2});
1522
}
1523
+
1524
render() {
1525
return (
1526
<div>
@@ -1467,8 +1532,12 @@ describe('ReactUpdates', () => {
1532
}
1533
1534
const container = document.createElement('div');
1535
+ const root = ReactDOMClient.createRoot(container);
1536
+
1537
expect(() => {
1471
- ReactDOM.render(<NonTerminating />, container);
1538
+ ReactDOM.flushSync(() => {
1539
+ root.render(<NonTerminating />);
1540
+ });
1541
}).toThrow('Maximum');
1542
});
1543
@@ -1482,12 +1551,15 @@ describe('ReactUpdates', () => {
1551
}
1552
1553
const container = document.createElement('div');
1554
+ const root = ReactDOMClient.createRoot(container);
1555
expect(() => {
1486
- ReactDOM.render(<NonTerminating />, container);
1556
+ ReactDOM.flushSync(() => {
1557
+ root.render(<NonTerminating />);
1558
+ });
1559
}).toThrow('Maximum');
1560
});
1561
1490
- it('can recover after falling into an infinite update loop', () => {
1562
+ it('can recover after falling into an infinite update loop', async () => {
1563
class NonTerminating extends React.Component {
1564
state = {step: 0};
1565
componentDidMount() {
@@ -1512,27 +1584,36 @@ describe('ReactUpdates', () => {
1584
}
1585
1586
const container = document.createElement('div');
1587
+ const root = ReactDOMClient.createRoot(container);
1588
expect(() => {
1516
- ReactDOM.render(<NonTerminating />, container);
1589
+ ReactDOM.flushSync(() => {
1590
+ root.render(<NonTerminating />);
1591
+ });
1592
}).toThrow('Maximum');
1593
1519
- ReactDOM.render(<Terminating />, container);
1594
+ await act(() => {
1595
+ root.render(<Terminating />);
1596
+ });
1597
expect(container.textContent).toBe('1');
1598
1599
expect(() => {
1523
- ReactDOM.render(<NonTerminating />, container);
1600
+ ReactDOM.flushSync(() => {
1601
+ root.render(<NonTerminating />);
1602
+ });
1603
}).toThrow('Maximum');
1525
-
1526
- ReactDOM.render(<Terminating />, container);
1604
+ await act(() => {
1605
+ root.render(<Terminating />);
1606
+ });
1607
expect(container.textContent).toBe('1');
1608
});
1609
1610
it('does not fall into mutually recursive infinite update loop with same container', () => {
1611
// Note: this test would fail if there were two or more different roots.
1532
-
1612
+ const container = document.createElement('div');
1613
+ const root = ReactDOMClient.createRoot(container);
1614
class A extends React.Component {
1615
componentDidMount() {
1535
- ReactDOM.render(<B />, container);
1616
+ root.render(<B />);
1617
}
1618
render() {
1619
return null;
@@ -1541,16 +1622,17 @@ describe('ReactUpdates', () => {
1622
1623
class B extends React.Component {
1624
componentDidMount() {
1544
- ReactDOM.render(<A />, container);
1625
+ root.render(<A />);
1626
}
1627
render() {
1628
return null;
1629
}
1630
}
1631
1551
- const container = document.createElement('div');
1632
expect(() => {
1553
- ReactDOM.render(<A />, container);
1633
+ ReactDOM.flushSync(() => {
1634
+ root.render(<A />);
1635
+ });
1636
}).toThrow('Maximum');
1637
});
1638
@@ -1582,14 +1664,17 @@ describe('ReactUpdates', () => {
1664
}
1665
1666
const container = document.createElement('div');
1667
+ const root = ReactDOMClient.createRoot(container);
1668
expect(() => {
1586
- ReactDOM.render(<NonTerminating />, container);
1669
+ ReactDOM.flushSync(() => {
1670
+ root.render(<NonTerminating />);
1671
+ });
1672
}).toThrow('Maximum');
1673
});
1674
1590
- it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', () => {
1675
+ it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', async () => {
1676
const subscribers = [];
1592
-
1677
+ const limit = 1200;
1678
class Child extends React.Component {
1679
state = {value: 'initial'};
1680
componentDidMount() {
@@ -1603,7 +1688,7 @@ describe('ReactUpdates', () => {
1688
class App extends React.Component {
1689
render() {
1690
const children = [];
1606
- for (let i = 0; i < 1200; i++) {
1691
+ for (let i = 0; i < limit; i++) {
1692
children.push(<Child key={i} />);
1693
}
1694
return children;
@@ -1611,13 +1696,18 @@ describe('ReactUpdates', () => {
1696
}
1697
1698
const container = document.createElement('div');
1614
- ReactDOM.render(<App />, container);
1699
+ const root = ReactDOMClient.createRoot(container);
1700
+ await act(() => {
1701
+ root.render(<App />);
1702
+ });
1703
1616
- ReactDOM.unstable_batchedUpdates(() => {
1704
+ await act(() => {
1705
subscribers.forEach(s => {
1706
s.setState({value: 'update'});
1707
});
1708
});
1709
+
1710
+ expect(subscribers.length).toBe(limit);
1711
});
1712
1713
// TODO: Replace this branch with @gate pragmas
@@ -1673,8 +1763,9 @@ describe('ReactUpdates', () => {
1763
}
1764
1765
const container = document.createElement('div');
1766
+ const root = ReactDOMClient.createRoot(container);
1767
await act(() => {
1677
- ReactDOM.render(<Terminating />, container);
1768
+ root.render(<Terminating />);
1769
});
1770
expect(container.textContent).toBe('50');
1771
await act(() => {
@@ -1696,8 +1787,9 @@ describe('ReactUpdates', () => {
1787
}
1788
1789
const container = document.createElement('div');
1790
+ const root = ReactDOMClient.createRoot(container);
1791
await act(() => {
1700
- ReactDOM.render(<Terminating />, container);
1792
+ root.render(<Terminating />);
1793
});
1794
1795
assertLog(['Done']);