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