main
js 2,305 lines 59.2 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 ReactDOMClient;
16 let act;
17 let Scheduler;
18 let waitForAll;
19 let waitFor;
20 let assertLog;
21 let assertConsoleErrorDev;
22
23 function normalizeCodeLocInfo(str) {
24 return (
25 str &&
26 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
27 const dot = name.lastIndexOf('.');
28 if (dot !== -1) {
29 name = name.slice(dot + 1);
30 }
31 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
32 })
33 );
34 }
35
36 describe('ReactUpdates', () => {
37 beforeEach(() => {
38 jest.resetModules();
39 React = require('react');
40 ReactDOM = require('react-dom');
41 ReactDOMClient = require('react-dom/client');
42 findDOMNode =
43 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
44 .findDOMNode;
45 act = require('internal-test-utils').act;
46 assertConsoleErrorDev =
47 require('internal-test-utils').assertConsoleErrorDev;
48 Scheduler = require('scheduler');
49
50 const InternalTestUtils = require('internal-test-utils');
51 waitForAll = InternalTestUtils.waitForAll;
52 waitFor = InternalTestUtils.waitFor;
53 assertLog = InternalTestUtils.assertLog;
54 });
55
56 // Note: This is based on a similar component we use in www. We can delete
57 // once the extra div wrapper is no longer necessary.
58 function LegacyHiddenDiv({children, mode}) {
59 return (
60 <div hidden={mode === 'hidden'}>
61 <React.unstable_LegacyHidden
62 mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
63 {children}
64 </React.unstable_LegacyHidden>
65 </div>
66 );
67 }
68
69 it('should batch state when updating state twice', async () => {
70 let componentState;
71 let setState;
72
73 function Component() {
74 const [state, _setState] = React.useState(0);
75 componentState = state;
76 setState = _setState;
77 React.useLayoutEffect(() => {
78 Scheduler.log('Commit');
79 });
80
81 return <div>{state}</div>;
82 }
83
84 const container = document.createElement('div');
85 const root = ReactDOMClient.createRoot(container);
86 await act(() => {
87 root.render(<Component />);
88 });
89
90 assertLog(['Commit']);
91 expect(container.firstChild.textContent).toBe('0');
92
93 await act(() => {
94 setState(1);
95 setState(2);
96 expect(componentState).toBe(0);
97 expect(container.firstChild.textContent).toBe('0');
98 assertLog([]);
99 });
100
101 expect(componentState).toBe(2);
102 assertLog(['Commit']);
103 expect(container.firstChild.textContent).toBe('2');
104 });
105
106 it('should batch state when updating two different states', async () => {
107 let componentStateA;
108 let componentStateB;
109 let setStateA;
110 let setStateB;
111
112 function Component() {
113 const [stateA, _setStateA] = React.useState(0);
114 const [stateB, _setStateB] = React.useState(0);
115 componentStateA = stateA;
116 componentStateB = stateB;
117 setStateA = _setStateA;
118 setStateB = _setStateB;
119
120 React.useLayoutEffect(() => {
121 Scheduler.log('Commit');
122 });
123
124 return (
125 <div>
126 {stateA} {stateB}
127 </div>
128 );
129 }
130
131 const container = document.createElement('div');
132 const root = ReactDOMClient.createRoot(container);
133 await act(() => {
134 root.render(<Component />);
135 });
136
137 assertLog(['Commit']);
138 expect(container.firstChild.textContent).toBe('0 0');
139
140 await act(() => {
141 setStateA(1);
142 setStateB(2);
143 expect(componentStateA).toBe(0);
144 expect(componentStateB).toBe(0);
145 expect(container.firstChild.textContent).toBe('0 0');
146 assertLog([]);
147 });
148
149 expect(componentStateA).toBe(1);
150 expect(componentStateB).toBe(2);
151 assertLog(['Commit']);
152 expect(container.firstChild.textContent).toBe('1 2');
153 });
154
155 it('should batch state and props together', async () => {
156 let setState;
157 let componentProp;
158 let componentState;
159
160 function Component({prop}) {
161 const [state, _setState] = React.useState(0);
162 componentProp = prop;
163 componentState = state;
164 setState = _setState;
165
166 React.useLayoutEffect(() => {
167 Scheduler.log('Commit');
168 });
169
170 return (
171 <div>
172 {prop} {state}
173 </div>
174 );
175 }
176
177 const container = document.createElement('div');
178 const root = ReactDOMClient.createRoot(container);
179 await act(() => {
180 root.render(<Component prop={0} />);
181 });
182
183 assertLog(['Commit']);
184 expect(container.firstChild.textContent).toBe('0 0');
185
186 await act(() => {
187 root.render(<Component prop={1} />);
188 setState(2);
189 expect(componentProp).toBe(0);
190 expect(componentState).toBe(0);
191 expect(container.firstChild.textContent).toBe('0 0');
192 assertLog([]);
193 });
194
195 expect(componentProp).toBe(1);
196 expect(componentState).toBe(2);
197 assertLog(['Commit']);
198 expect(container.firstChild.textContent).toBe('1 2');
199 });
200
201 it('should batch parent/child state updates together', async () => {
202 let childRef;
203 let parentState;
204 let childState;
205 let setParentState;
206 let setChildState;
207
208 function Parent() {
209 const [state, _setState] = React.useState(0);
210 parentState = state;
211 setParentState = _setState;
212
213 React.useLayoutEffect(() => {
214 Scheduler.log('Parent Commit');
215 });
216
217 return (
218 <div>
219 <Child prop={state} />
220 </div>
221 );
222 }
223
224 function Child({prop}) {
225 const [state, _setState] = React.useState(0);
226 childState = state;
227 setChildState = _setState;
228
229 React.useLayoutEffect(() => {
230 Scheduler.log('Child Commit');
231 });
232
233 return (
234 <div
235 ref={ref => {
236 childRef = ref;
237 }}>
238 {prop} {state}
239 </div>
240 );
241 }
242
243 const container = document.createElement('div');
244 const root = ReactDOMClient.createRoot(container);
245 await act(() => {
246 root.render(<Parent />);
247 });
248
249 assertLog(['Child Commit', 'Parent Commit']);
250 expect(childRef.textContent).toBe('0 0');
251
252 await act(() => {
253 // Parent update first.
254 setParentState(1);
255 setChildState(2);
256 expect(parentState).toBe(0);
257 expect(childState).toBe(0);
258 expect(childRef.textContent).toBe('0 0');
259 assertLog([]);
260 });
261
262 expect(parentState).toBe(1);
263 expect(childState).toBe(2);
264 expect(childRef.textContent).toBe('1 2');
265 assertLog(['Child Commit', 'Parent Commit']);
266 });
267
268 it('should batch child/parent state updates together', async () => {
269 let childRef;
270 let parentState;
271 let childState;
272 let setParentState;
273 let setChildState;
274
275 function Parent() {
276 const [state, _setState] = React.useState(0);
277 parentState = state;
278 setParentState = _setState;
279
280 React.useLayoutEffect(() => {
281 Scheduler.log('Parent Commit');
282 });
283
284 return (
285 <div>
286 <Child prop={state} />
287 </div>
288 );
289 }
290
291 function Child({prop}) {
292 const [state, _setState] = React.useState(0);
293 childState = state;
294 setChildState = _setState;
295
296 React.useLayoutEffect(() => {
297 Scheduler.log('Child Commit');
298 });
299
300 return (
301 <div
302 ref={ref => {
303 childRef = ref;
304 }}>
305 {prop} {state}
306 </div>
307 );
308 }
309
310 const container = document.createElement('div');
311 const root = ReactDOMClient.createRoot(container);
312 await act(() => {
313 root.render(<Parent />);
314 });
315
316 assertLog(['Child Commit', 'Parent Commit']);
317 expect(childRef.textContent).toBe('0 0');
318
319 await act(() => {
320 // Child update first.
321 setChildState(2);
322 setParentState(1);
323 expect(parentState).toBe(0);
324 expect(childState).toBe(0);
325 expect(childRef.textContent).toBe('0 0');
326 assertLog([]);
327 });
328
329 expect(parentState).toBe(1);
330 expect(childState).toBe(2);
331 expect(childRef.textContent).toBe('1 2');
332 assertLog(['Child Commit', 'Parent Commit']);
333 });
334
335 it('should support chained state updates', async () => {
336 let instance;
337 class Component extends React.Component {
338 state = {x: 0};
339 constructor(props) {
340 super(props);
341 instance = this;
342 }
343
344 componentDidUpdate() {
345 Scheduler.log('Update');
346 }
347
348 render() {
349 return <div>{this.state.x}</div>;
350 }
351 }
352
353 const container = document.createElement('div');
354 const root = ReactDOMClient.createRoot(container);
355 await act(() => {
356 root.render(<Component />);
357 });
358
359 expect(instance.state.x).toBe(0);
360 expect(container.firstChild.textContent).toBe('0');
361
362 let innerCallbackRun = false;
363 await act(() => {
364 instance.setState({x: 1}, function () {
365 instance.setState({x: 2}, function () {
366 innerCallbackRun = true;
367 expect(instance.state.x).toBe(2);
368 expect(container.firstChild.textContent).toBe('2');
369 assertLog(['Update']);
370 });
371 expect(instance.state.x).toBe(1);
372 expect(container.firstChild.textContent).toBe('1');
373 assertLog(['Update']);
374 });
375 expect(instance.state.x).toBe(0);
376 expect(container.firstChild.textContent).toBe('0');
377 assertLog([]);
378 });
379
380 assertLog([]);
381 expect(instance.state.x).toBe(2);
382 expect(innerCallbackRun).toBeTruthy();
383 expect(container.firstChild.textContent).toBe('2');
384 });
385
386 it('should batch forceUpdate together', async () => {
387 let instance;
388 let shouldUpdateCount = 0;
389 class Component extends React.Component {
390 state = {x: 0};
391
392 constructor(props) {
393 super(props);
394 instance = this;
395 }
396 shouldComponentUpdate() {
397 shouldUpdateCount++;
398 }
399
400 componentDidUpdate() {
401 Scheduler.log('Update');
402 }
403
404 render() {
405 return <div>{this.state.x}</div>;
406 }
407 }
408
409 const container = document.createElement('div');
410 const root = ReactDOMClient.createRoot(container);
411 await act(() => {
412 root.render(<Component />);
413 });
414
415 assertLog([]);
416 expect(instance.state.x).toBe(0);
417
418 await act(() => {
419 instance.setState({x: 1}, function () {
420 Scheduler.log('callback');
421 });
422 instance.forceUpdate(function () {
423 Scheduler.log('forceUpdate');
424 });
425 assertLog([]);
426 expect(instance.state.x).toBe(0);
427 expect(container.firstChild.textContent).toBe('0');
428 });
429
430 // shouldComponentUpdate shouldn't be called since we're forcing
431 expect(shouldUpdateCount).toBe(0);
432 assertLog(['Update', 'callback', 'forceUpdate']);
433 expect(instance.state.x).toBe(1);
434 expect(container.firstChild.textContent).toBe('1');
435 });
436
437 it('should update children even if parent blocks updates', async () => {
438 let instance;
439 class Parent extends React.Component {
440 childRef = React.createRef();
441
442 constructor(props) {
443 super(props);
444 instance = this;
445 }
446 shouldComponentUpdate() {
447 return false;
448 }
449
450 render() {
451 Scheduler.log('Parent render');
452 return <Child ref={this.childRef} />;
453 }
454 }
455
456 class Child extends React.Component {
457 render() {
458 Scheduler.log('Child render');
459 return <div />;
460 }
461 }
462
463 const container = document.createElement('div');
464 const root = ReactDOMClient.createRoot(container);
465 await act(() => {
466 root.render(<Parent />);
467 });
468
469 assertLog(['Parent render', 'Child render']);
470
471 await act(() => {
472 instance.setState({x: 1});
473 });
474
475 assertLog([]);
476
477 await act(() => {
478 instance.childRef.current.setState({x: 1});
479 });
480
481 assertLog(['Child render']);
482 });
483
484 it('should not reconcile children passed via props', async () => {
485 class Top extends React.Component {
486 render() {
487 return (
488 <Middle>
489 <Bottom />
490 </Middle>
491 );
492 }
493 }
494
495 class Middle extends React.Component {
496 componentDidMount() {
497 this.forceUpdate();
498 }
499
500 render() {
501 Scheduler.log('Middle');
502 return React.Children.only(this.props.children);
503 }
504 }
505
506 class Bottom extends React.Component {
507 render() {
508 Scheduler.log('Bottom');
509 return null;
510 }
511 }
512
513 const container = document.createElement('div');
514 const root = ReactDOMClient.createRoot(container);
515 await act(() => {
516 root.render(<Top />);
517 });
518
519 assertLog(['Middle', 'Bottom', 'Middle']);
520 });
521
522 it('should flow updates correctly', async () => {
523 let willUpdates = [];
524 let didUpdates = [];
525 let instance;
526
527 const UpdateLoggingMixin = {
528 UNSAFE_componentWillUpdate: function () {
529 willUpdates.push(this.constructor.displayName);
530 },
531 componentDidUpdate: function () {
532 didUpdates.push(this.constructor.displayName);
533 },
534 };
535
536 class Box extends React.Component {
537 boxDivRef = React.createRef();
538
539 render() {
540 return <div ref={this.boxDivRef}>{this.props.children}</div>;
541 }
542 }
543 Object.assign(Box.prototype, UpdateLoggingMixin);
544
545 class Child extends React.Component {
546 spanRef = React.createRef();
547
548 render() {
549 return <span ref={this.spanRef}>child</span>;
550 }
551 }
552 Object.assign(Child.prototype, UpdateLoggingMixin);
553
554 class Switcher extends React.Component {
555 state = {tabKey: 'hello'};
556 boxRef = React.createRef();
557 switcherDivRef = React.createRef();
558 render() {
559 const child = this.props.children;
560
561 return (
562 <Box ref={this.boxRef}>
563 <div
564 ref={this.switcherDivRef}
565 style={{
566 display: this.state.tabKey === child.key ? '' : 'none',
567 }}>
568 {child}
569 </div>
570 </Box>
571 );
572 }
573 }
574 Object.assign(Switcher.prototype, UpdateLoggingMixin);
575
576 class App extends React.Component {
577 switcherRef = React.createRef();
578 childRef = React.createRef();
579 constructor(props) {
580 super(props);
581 instance = this;
582 }
583 render() {
584 return (
585 <Switcher ref={this.switcherRef}>
586 <Child key="hello" ref={this.childRef} />
587 </Switcher>
588 );
589 }
590 }
591 Object.assign(App.prototype, UpdateLoggingMixin);
592
593 const container = document.createElement('div');
594 await act(() => {
595 ReactDOMClient.createRoot(container).render(<App />);
596 });
597
598 function expectUpdates(desiredWillUpdates, desiredDidUpdates) {
599 let i;
600 for (i = 0; i < desiredWillUpdates; i++) {
601 expect(willUpdates).toContain(desiredWillUpdates[i]);
602 }
603 for (i = 0; i < desiredDidUpdates; i++) {
604 expect(didUpdates).toContain(desiredDidUpdates[i]);
605 }
606 willUpdates = [];
607 didUpdates = [];
608 }
609
610 function triggerUpdate(c) {
611 c.setState({x: 1});
612 }
613
614 async function testUpdates(
615 components,
616 desiredWillUpdates,
617 desiredDidUpdates,
618 ) {
619 let i;
620
621 await act(() => {
622 for (i = 0; i < components.length; i++) {
623 triggerUpdate(components[i]);
624 }
625 });
626
627 expectUpdates(desiredWillUpdates, desiredDidUpdates);
628
629 // Try them in reverse order
630
631 await act(() => {
632 for (i = components.length - 1; i >= 0; i--) {
633 triggerUpdate(components[i]);
634 }
635 });
636
637 expectUpdates(desiredWillUpdates, desiredDidUpdates);
638 }
639 await testUpdates(
640 [
641 instance.switcherRef.current.boxRef.current,
642 instance.switcherRef.current,
643 ],
644 // Owner-child relationships have inverse will and did
645 ['Switcher', 'Box'],
646 ['Box', 'Switcher'],
647 );
648
649 await testUpdates(
650 [instance.childRef.current, instance.switcherRef.current.boxRef.current],
651 // Not owner-child so reconcile independently
652 ['Box', 'Child'],
653 ['Box', 'Child'],
654 );
655
656 await testUpdates(
657 [instance.childRef.current, instance.switcherRef.current],
658 // Switcher owns Box and Child, Box does not own Child
659 ['Switcher', 'Box', 'Child'],
660 ['Box', 'Switcher', 'Child'],
661 );
662 });
663
664 it('should queue mount-ready handlers across different roots', async () => {
665 // We'll define two components A and B, then update both of them. When A's
666 // componentDidUpdate handlers is called, B's DOM should already have been
667 // updated.
668
669 const bContainer = document.createElement('div');
670 let a;
671 let b;
672
673 let aUpdated = false;
674
675 class A extends React.Component {
676 state = {x: 0};
677 constructor(props) {
678 super(props);
679 a = this;
680 }
681 componentDidUpdate() {
682 expect(findDOMNode(b).textContent).toBe('B1');
683 aUpdated = true;
684 }
685
686 render() {
687 let portal = null;
688 portal = ReactDOM.createPortal(<B ref={n => (b = n)} />, bContainer);
689 return (
690 <div>
691 A{this.state.x}
692 {portal}
693 </div>
694 );
695 }
696 }
697
698 class B extends React.Component {
699 state = {x: 0};
700
701 render() {
702 return <div>B{this.state.x}</div>;
703 }
704 }
705
706 const container = document.createElement('div');
707 const root = ReactDOMClient.createRoot(container);
708 await act(() => {
709 root.render(<A />);
710 });
711
712 await act(() => {
713 a.setState({x: 1});
714 b.setState({x: 1});
715 });
716
717 expect(aUpdated).toBe(true);
718 });
719
720 it('should flush updates in the correct order', async () => {
721 const updates = [];
722 let instance;
723 class Outer extends React.Component {
724 state = {x: 0};
725 innerRef = React.createRef();
726 constructor(props) {
727 super(props);
728 instance = this;
729 }
730 render() {
731 updates.push('Outer-render-' + this.state.x);
732 return (
733 <div>
734 <Inner x={this.state.x} ref={this.innerRef} />
735 </div>
736 );
737 }
738
739 componentDidUpdate() {
740 const x = this.state.x;
741 updates.push('Outer-didUpdate-' + x);
742 updates.push('Inner-setState-' + x);
743 this.innerRef.current.setState({x: x}, function () {
744 updates.push('Inner-callback-' + x);
745 });
746 }
747 }
748
749 class Inner extends React.Component {
750 state = {x: 0};
751
752 render() {
753 updates.push('Inner-render-' + this.props.x + '-' + this.state.x);
754 return <div />;
755 }
756
757 componentDidUpdate() {
758 updates.push('Inner-didUpdate-' + this.props.x + '-' + this.state.x);
759 }
760 }
761
762 const container = document.createElement('div');
763 const root = ReactDOMClient.createRoot(container);
764 await act(() => {
765 root.render(<Outer />);
766 });
767
768 await act(() => {
769 updates.push('Outer-setState-1');
770 instance.setState({x: 1}, function () {
771 updates.push('Outer-callback-1');
772 updates.push('Outer-setState-2');
773 instance.setState({x: 2}, function () {
774 updates.push('Outer-callback-2');
775 });
776 });
777 });
778
779 expect(updates).toEqual([
780 'Outer-render-0',
781 'Inner-render-0-0',
782
783 'Outer-setState-1',
784 'Outer-render-1',
785 'Inner-render-1-0',
786 'Inner-didUpdate-1-0',
787 'Outer-didUpdate-1',
788 // Happens in a batch, so don't re-render yet
789 'Inner-setState-1',
790 'Outer-callback-1',
791
792 // Happens in a batch
793 'Outer-setState-2',
794
795 // Flush batched updates all at once
796 'Outer-render-2',
797 'Inner-render-2-1',
798 'Inner-didUpdate-2-1',
799 'Inner-callback-1',
800 'Outer-didUpdate-2',
801 'Inner-setState-2',
802 'Outer-callback-2',
803 'Inner-render-2-2',
804 'Inner-didUpdate-2-2',
805 'Inner-callback-2',
806 ]);
807 });
808
809 it('should flush updates in the correct order across roots', async () => {
810 const instances = [];
811 const updates = [];
812
813 class MockComponent extends React.Component {
814 render() {
815 updates.push(this.props.depth);
816 return <div />;
817 }
818
819 componentDidMount() {
820 instances.push(this);
821 if (this.props.depth < this.props.count) {
822 const root = ReactDOMClient.createRoot(findDOMNode(this));
823 root.render(
824 <MockComponent
825 depth={this.props.depth + 1}
826 count={this.props.count}
827 />,
828 );
829 }
830 }
831 }
832
833 const container = document.createElement('div');
834 const root = ReactDOMClient.createRoot(container);
835 await act(() => {
836 root.render(<MockComponent depth={0} count={2} />);
837 });
838
839 expect(updates).toEqual([0, 1, 2]);
840
841 await act(() => {
842 // Simulate update on each component from top to bottom.
843 instances.forEach(function (instance) {
844 instance.forceUpdate();
845 });
846 });
847
848 expect(updates).toEqual([0, 1, 2, 0, 1, 2]);
849 });
850
851 it('should queue nested updates', async () => {
852 // See https://github.com/facebook/react/issues/1147
853
854 class X extends React.Component {
855 state = {s: 0};
856
857 render() {
858 if (this.state.s === 0) {
859 return (
860 <div>
861 <span>0</span>
862 </div>
863 );
864 } else {
865 return <div>1</div>;
866 }
867 }
868
869 go = () => {
870 this.setState({s: 1});
871 this.setState({s: 0});
872 this.setState({s: 1});
873 };
874 }
875
876 class Y extends React.Component {
877 render() {
878 return (
879 <div>
880 <Z />
881 </div>
882 );
883 }
884 }
885
886 class Z extends React.Component {
887 render() {
888 return <div />;
889 }
890
891 UNSAFE_componentWillUpdate() {
892 x.go();
893 }
894 }
895
896 let container = document.createElement('div');
897 let root = ReactDOMClient.createRoot(container);
898 let x;
899 await act(() => {
900 root.render(<X ref={current => (x = current)} />);
901 });
902
903 container = document.createElement('div');
904 root = ReactDOMClient.createRoot(container);
905 let y;
906 await act(() => {
907 root.render(<Y ref={current => (y = current)} />);
908 });
909
910 expect(findDOMNode(x).textContent).toBe('0');
911
912 await act(() => {
913 y.forceUpdate();
914 });
915 expect(findDOMNode(x).textContent).toBe('1');
916 });
917
918 it('should queue updates from during mount', async () => {
919 // See https://github.com/facebook/react/issues/1353
920 let a;
921
922 class A extends React.Component {
923 state = {x: 0};
924
925 UNSAFE_componentWillMount() {
926 a = this;
927 }
928
929 render() {
930 return <div>A{this.state.x}</div>;
931 }
932 }
933
934 class B extends React.Component {
935 UNSAFE_componentWillMount() {
936 a.setState({x: 1});
937 }
938
939 render() {
940 return <div />;
941 }
942 }
943
944 const container = document.createElement('div');
945 const root = ReactDOMClient.createRoot(container);
946
947 await act(() => {
948 root.render(
949 <div>
950 <A />
951 <B />
952 </div>,
953 );
954 });
955
956 expect(container.firstChild.textContent).toBe('A1');
957 });
958
959 it('calls componentWillReceiveProps setState callback properly', async () => {
960 class A extends React.Component {
961 state = {x: this.props.x};
962
963 UNSAFE_componentWillReceiveProps(nextProps) {
964 const newX = nextProps.x;
965 this.setState({x: newX}, function () {
966 // State should have updated by the time this callback gets called
967 expect(this.state.x).toBe(newX);
968 Scheduler.log('Callback');
969 });
970 }
971
972 render() {
973 return <div>{this.state.x}</div>;
974 }
975 }
976
977 const container = document.createElement('div');
978 const root = ReactDOMClient.createRoot(container);
979 await act(() => {
980 root.render(<A x={1} />);
981 });
982 assertLog([]);
983
984 // Needs to be a separate act, or it will be batched.
985 await act(() => {
986 root.render(<A x={2} />);
987 });
988
989 assertLog(['Callback']);
990 });
991
992 it('does not call render after a component as been deleted', async () => {
993 let componentA = null;
994 let componentB = null;
995
996 class B extends React.Component {
997 state = {updates: 0};
998
999 componentDidMount() {
1000 componentB = this;
1001 }
1002
1003 render() {
1004 Scheduler.log('B');
1005 return <div />;
1006 }
1007 }
1008
1009 class A extends React.Component {
1010 state = {showB: true};
1011
1012 componentDidMount() {
1013 componentA = this;
1014 }
1015 render() {
1016 return this.state.showB ? <B /> : <div />;
1017 }
1018 }
1019
1020 const container = document.createElement('div');
1021 const root = ReactDOMClient.createRoot(container);
1022 await act(() => {
1023 root.render(<A />);
1024 });
1025 assertLog(['B']);
1026
1027 await act(() => {
1028 // B will have scheduled an update but the batching should ensure that its
1029 // update never fires.
1030 componentB.setState({updates: 1});
1031 componentA.setState({showB: false});
1032 });
1033
1034 assertLog([]);
1035 });
1036
1037 it('throws in setState if the update callback is not a function', async () => {
1038 function Foo() {
1039 this.a = 1;
1040 this.b = 2;
1041 }
1042
1043 class A extends React.Component {
1044 state = {};
1045
1046 render() {
1047 return <div />;
1048 }
1049 }
1050
1051 let container = document.createElement('div');
1052 let root = ReactDOMClient.createRoot(container);
1053 let component;
1054 await act(() => {
1055 root.render(<A ref={current => (component = current)} />);
1056 });
1057
1058 await expect(async () => {
1059 await act(() => {
1060 component.setState({}, 'no');
1061 });
1062 }).rejects.toThrow(
1063 'Invalid argument passed as callback. Expected a function. Instead ' +
1064 'received: no',
1065 );
1066 assertConsoleErrorDev([
1067 'Expected the last optional `callback` argument to be ' +
1068 'a function. Instead received: no.',
1069 ]);
1070 container = document.createElement('div');
1071 root = ReactDOMClient.createRoot(container);
1072 await act(() => {
1073 root.render(<A ref={current => (component = current)} />);
1074 });
1075
1076 await expect(async () => {
1077 await act(() => {
1078 component.setState({}, {foo: 'bar'});
1079 });
1080 }).rejects.toThrow(
1081 'Invalid argument passed as callback. Expected a function. Instead ' +
1082 'received: [object Object]',
1083 );
1084 assertConsoleErrorDev([
1085 'Expected the last optional `callback` argument to be ' +
1086 "a function. Instead received: { foo: 'bar' }.",
1087 ]);
1088 container = document.createElement('div');
1089 root = ReactDOMClient.createRoot(container);
1090 await act(() => {
1091 root.render(<A ref={current => (component = current)} />);
1092 });
1093
1094 await expect(
1095 act(() => {
1096 component.setState({}, new Foo());
1097 }),
1098 ).rejects.toThrow(
1099 'Invalid argument passed as callback. Expected a function. Instead ' +
1100 'received: [object Object]',
1101 );
1102 });
1103
1104 it('throws in forceUpdate if the update callback is not a function', async () => {
1105 function Foo() {
1106 this.a = 1;
1107 this.b = 2;
1108 }
1109
1110 class A extends React.Component {
1111 state = {};
1112
1113 render() {
1114 return <div />;
1115 }
1116 }
1117
1118 let container = document.createElement('div');
1119 let root = ReactDOMClient.createRoot(container);
1120 let component;
1121 await act(() => {
1122 root.render(<A ref={current => (component = current)} />);
1123 });
1124
1125 await expect(async () => {
1126 await act(() => {
1127 component.forceUpdate('no');
1128 });
1129 }).rejects.toThrow(
1130 'Invalid argument passed as callback. Expected a function. Instead ' +
1131 'received: no',
1132 );
1133 assertConsoleErrorDev([
1134 'Expected the last optional `callback` argument to be ' +
1135 'a function. Instead received: no.',
1136 ]);
1137 container = document.createElement('div');
1138 root = ReactDOMClient.createRoot(container);
1139 await act(() => {
1140 root.render(<A ref={current => (component = current)} />);
1141 });
1142
1143 await expect(async () => {
1144 await act(() => {
1145 component.forceUpdate({foo: 'bar'});
1146 });
1147 }).rejects.toThrow(
1148 'Invalid argument passed as callback. Expected a function. Instead ' +
1149 'received: [object Object]',
1150 );
1151 assertConsoleErrorDev([
1152 'Expected the last optional `callback` argument to be ' +
1153 "a function. Instead received: { foo: 'bar' }.",
1154 ]);
1155 // Make sure the warning is deduplicated and doesn't fire again
1156 container = document.createElement('div');
1157 root = ReactDOMClient.createRoot(container);
1158 await act(() => {
1159 root.render(<A ref={current => (component = current)} />);
1160 });
1161
1162 await expect(
1163 act(() => {
1164 component.forceUpdate(new Foo());
1165 }),
1166 ).rejects.toThrow(
1167 'Invalid argument passed as callback. Expected a function. Instead ' +
1168 'received: [object Object]',
1169 );
1170 });
1171
1172 it('does not update one component twice in a batch (#2410)', async () => {
1173 let parent;
1174 class Parent extends React.Component {
1175 childRef = React.createRef();
1176
1177 componentDidMount() {
1178 parent = this;
1179 }
1180 getChild = () => {
1181 return this.childRef.current;
1182 };
1183
1184 render() {
1185 return <Child ref={this.childRef} />;
1186 }
1187 }
1188
1189 let renderCount = 0;
1190 let postRenderCount = 0;
1191 let once = false;
1192
1193 class Child extends React.Component {
1194 state = {updated: false};
1195
1196 UNSAFE_componentWillUpdate() {
1197 if (!once) {
1198 once = true;
1199 this.setState({updated: true});
1200 }
1201 }
1202
1203 componentDidMount() {
1204 expect(renderCount).toBe(postRenderCount + 1);
1205 postRenderCount++;
1206 }
1207
1208 componentDidUpdate() {
1209 expect(renderCount).toBe(postRenderCount + 1);
1210 postRenderCount++;
1211 }
1212
1213 render() {
1214 expect(renderCount).toBe(postRenderCount);
1215 renderCount++;
1216 return <div />;
1217 }
1218 }
1219
1220 const container = document.createElement('div');
1221 const root = ReactDOMClient.createRoot(container);
1222 await act(() => {
1223 root.render(<Parent />);
1224 });
1225
1226 const child = parent.getChild();
1227 await act(() => {
1228 parent.forceUpdate();
1229 child.forceUpdate();
1230 });
1231
1232 expect.assertions(6);
1233 });
1234
1235 it('does not update one component twice in a batch (#6371)', async () => {
1236 let callbacks = [];
1237 function emitChange() {
1238 callbacks.forEach(c => c());
1239 }
1240
1241 class App extends React.Component {
1242 constructor(props) {
1243 super(props);
1244 this.state = {showChild: true};
1245 }
1246 componentDidMount() {
1247 this.setState({showChild: false});
1248 }
1249 render() {
1250 return (
1251 <div>
1252 <ForceUpdatesOnChange />
1253 {this.state.showChild && <EmitsChangeOnUnmount />}
1254 </div>
1255 );
1256 }
1257 }
1258
1259 class EmitsChangeOnUnmount extends React.Component {
1260 componentWillUnmount() {
1261 emitChange();
1262 }
1263 render() {
1264 return null;
1265 }
1266 }
1267
1268 class ForceUpdatesOnChange extends React.Component {
1269 componentDidMount() {
1270 this.onChange = () => this.forceUpdate();
1271 this.onChange();
1272 callbacks.push(this.onChange);
1273 }
1274 componentWillUnmount() {
1275 callbacks = callbacks.filter(c => c !== this.onChange);
1276 }
1277 render() {
1278 return <div key={Math.random()} onClick={function () {}} />;
1279 }
1280 }
1281
1282 const root = ReactDOMClient.createRoot(document.createElement('div'));
1283 await act(() => {
1284 root.render(<App />);
1285 });
1286
1287 // Error should not be thrown.
1288 expect(true).toBe(true);
1289 });
1290
1291 it('handles reentrant mounting in synchronous mode', async () => {
1292 let onChangeCalled = false;
1293 class Editor extends React.Component {
1294 render() {
1295 return <div>{this.props.text}</div>;
1296 }
1297 componentDidMount() {
1298 Scheduler.log('Mount');
1299 // This should be called only once but we guard just in case.
1300 if (!this.props.rendered) {
1301 this.props.onChange({rendered: true});
1302 }
1303 }
1304 }
1305
1306 const container = document.createElement('div');
1307 const root = ReactDOMClient.createRoot(container);
1308 function render() {
1309 root.render(
1310 <Editor
1311 onChange={newProps => {
1312 onChangeCalled = true;
1313 props = {...props, ...newProps};
1314 render();
1315 }}
1316 {...props}
1317 />,
1318 );
1319 }
1320
1321 let props = {text: 'hello', rendered: false};
1322 await act(() => {
1323 render();
1324 });
1325 assertLog(['Mount']);
1326 props = {...props, text: 'goodbye'};
1327 await act(() => {
1328 render();
1329 });
1330
1331 assertLog([]);
1332 expect(container.textContent).toBe('goodbye');
1333 expect(onChangeCalled).toBeTruthy();
1334 });
1335
1336 it('mounts and unmounts are batched', async () => {
1337 const container = document.createElement('div');
1338 const root = ReactDOMClient.createRoot(container);
1339
1340 await act(() => {
1341 root.render(<div>Hello</div>);
1342 expect(container.textContent).toBe('');
1343 root.unmount(container);
1344 expect(container.textContent).toBe('');
1345 });
1346
1347 expect(container.textContent).toBe('');
1348 });
1349
1350 it('uses correct base state for setState inside render phase', async () => {
1351 class Foo extends React.Component {
1352 state = {step: 0};
1353 render() {
1354 const memoizedStep = this.state.step;
1355 this.setState(baseState => {
1356 const baseStep = baseState.step;
1357 Scheduler.log(`base: ${baseStep}, memoized: ${memoizedStep}`);
1358 return baseStep === 0 ? {step: 1} : null;
1359 });
1360 return null;
1361 }
1362 }
1363
1364 const container = document.createElement('div');
1365 const root = ReactDOMClient.createRoot(container);
1366 await act(() => {
1367 root.render(<Foo />);
1368 });
1369 assertConsoleErrorDev([
1370 'Cannot update during an existing state transition (such as within `render`). ' +
1371 'Render methods should be a pure function of props and state.\n' +
1372 ' in Foo (at **)',
1373 ]);
1374
1375 assertLog(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
1376 });
1377
1378 it('does not re-render if state update is null', async () => {
1379 const container = document.createElement('div');
1380
1381 let instance;
1382 class Foo extends React.Component {
1383 render() {
1384 instance = this;
1385 Scheduler.log('render');
1386 return <div />;
1387 }
1388 }
1389 const root = ReactDOMClient.createRoot(container);
1390 await act(() => {
1391 root.render(<Foo />);
1392 });
1393
1394 assertLog(['render']);
1395 await act(() => {
1396 instance.setState(() => null);
1397 });
1398 assertLog([]);
1399 });
1400
1401 it('synchronously renders hidden subtrees', async () => {
1402 const container = document.createElement('div');
1403
1404 function Baz() {
1405 Scheduler.log('Baz');
1406 return null;
1407 }
1408
1409 function Bar() {
1410 Scheduler.log('Bar');
1411 return null;
1412 }
1413
1414 function Foo() {
1415 Scheduler.log('Foo');
1416 return (
1417 <div>
1418 <div hidden={true}>
1419 <Bar />
1420 </div>
1421 <Baz />
1422 </div>
1423 );
1424 }
1425
1426 const root = ReactDOMClient.createRoot(container);
1427 await act(() => {
1428 // Mount
1429 root.render(<Foo />);
1430 });
1431 assertLog(['Foo', 'Bar', 'Baz']);
1432
1433 await act(() => {
1434 // Update
1435 root.render(<Foo />);
1436 });
1437 assertLog(['Foo', 'Bar', 'Baz']);
1438 });
1439
1440 // @gate www
1441 it('delays sync updates inside hidden subtrees in Concurrent Mode', async () => {
1442 const container = document.createElement('div');
1443
1444 function Baz() {
1445 Scheduler.log('Baz');
1446 return <p>baz</p>;
1447 }
1448
1449 let setCounter;
1450 function Bar() {
1451 const [counter, _setCounter] = React.useState(0);
1452 setCounter = _setCounter;
1453 Scheduler.log('Bar');
1454 return <p>bar {counter}</p>;
1455 }
1456
1457 function Foo() {
1458 Scheduler.log('Foo');
1459 React.useEffect(() => {
1460 Scheduler.log('Foo#effect');
1461 });
1462 return (
1463 <div>
1464 <LegacyHiddenDiv mode="hidden">
1465 <Bar />
1466 </LegacyHiddenDiv>
1467 <Baz />
1468 </div>
1469 );
1470 }
1471
1472 const root = ReactDOMClient.createRoot(container);
1473 let hiddenDiv;
1474 await act(async () => {
1475 root.render(<Foo />);
1476 await waitFor(['Foo', 'Baz', 'Foo#effect']);
1477 hiddenDiv = container.firstChild.firstChild;
1478 expect(hiddenDiv.hidden).toBe(true);
1479 expect(hiddenDiv.innerHTML).toBe('');
1480 // Run offscreen update
1481 await waitForAll(['Bar']);
1482 expect(hiddenDiv.hidden).toBe(true);
1483 expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1484 });
1485
1486 ReactDOM.flushSync(() => {
1487 setCounter(1);
1488 });
1489 // Should not flush yet
1490 expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1491
1492 // Run offscreen update
1493 await waitForAll(['Bar']);
1494 expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
1495 });
1496
1497 it('can render ridiculously large number of roots without triggering infinite update loop error', async () => {
1498 function Component({trigger}) {
1499 const [state, setState] = React.useState(0);
1500
1501 React.useEffect(() => {
1502 if (trigger) {
1503 Scheduler.log('Trigger');
1504 setState(c => c + 1);
1505 }
1506 }, [trigger]);
1507
1508 return <div>{state}</div>;
1509 }
1510
1511 class Foo extends React.Component {
1512 componentDidMount() {
1513 const limit = 1200;
1514 for (let i = 0; i < limit; i++) {
1515 if (i < limit - 1) {
1516 ReactDOMClient.createRoot(document.createElement('div')).render(
1517 <Component />,
1518 );
1519 } else {
1520 // The "nested update limit" error isn't thrown until setState
1521 ReactDOMClient.createRoot(document.createElement('div')).render(
1522 <Component trigger={true} />,
1523 );
1524 }
1525 }
1526 }
1527 render() {
1528 return null;
1529 }
1530 }
1531
1532 const root = ReactDOMClient.createRoot(document.createElement('div'));
1533 await act(() => {
1534 root.render(<Foo />);
1535 });
1536
1537 // Make sure the setState trigger runs.
1538 assertLog(['Trigger']);
1539 });
1540
1541 it('resets the update counter for unrelated updates', async () => {
1542 const container = document.createElement('div');
1543 const ref = React.createRef();
1544
1545 class EventuallyTerminating extends React.Component {
1546 state = {step: 0};
1547 componentDidMount() {
1548 this.setState({step: 1});
1549 }
1550 componentDidUpdate() {
1551 if (this.state.step < limit) {
1552 this.setState({step: this.state.step + 1});
1553 }
1554 }
1555 render() {
1556 return this.state.step;
1557 }
1558 }
1559
1560 let limit = 55;
1561 const root = ReactDOMClient.createRoot(container);
1562 await expect(async () => {
1563 await act(() => {
1564 root.render(<EventuallyTerminating ref={ref} />);
1565 });
1566 }).rejects.toThrow('Maximum');
1567
1568 // Verify that we don't go over the limit if these updates are unrelated.
1569 limit -= 10;
1570 await act(() => {
1571 root.render(<EventuallyTerminating ref={ref} />);
1572 });
1573 expect(container.textContent).toBe(limit.toString());
1574
1575 await act(() => {
1576 ref.current.setState({step: 0});
1577 });
1578 expect(container.textContent).toBe(limit.toString());
1579
1580 await act(() => {
1581 ref.current.setState({step: 0});
1582 });
1583 expect(container.textContent).toBe(limit.toString());
1584
1585 limit += 10;
1586 await expect(async () => {
1587 await act(() => {
1588 ref.current.setState({step: 0});
1589 });
1590 }).rejects.toThrow('Maximum');
1591 expect(ref.current).toBe(null);
1592 });
1593
1594 it('does not fall into an infinite update loop', async () => {
1595 class NonTerminating extends React.Component {
1596 state = {step: 0};
1597
1598 componentDidMount() {
1599 this.setState({step: 1});
1600 }
1601
1602 componentDidUpdate() {
1603 this.setState({step: 2});
1604 }
1605
1606 render() {
1607 return (
1608 <div>
1609 Hello {this.props.name}
1610 {this.state.step}
1611 </div>
1612 );
1613 }
1614 }
1615
1616 const container = document.createElement('div');
1617 const root = ReactDOMClient.createRoot(container);
1618
1619 await expect(async () => {
1620 await act(() => {
1621 root.render(<NonTerminating />);
1622 });
1623 }).rejects.toThrow('Maximum');
1624 });
1625
1626 it('does not fall into an infinite update loop with useLayoutEffect', async () => {
1627 function NonTerminating() {
1628 const [step, setStep] = React.useState(0);
1629 React.useLayoutEffect(() => {
1630 setStep(x => x + 1);
1631 });
1632 return step;
1633 }
1634
1635 const container = document.createElement('div');
1636 const root = ReactDOMClient.createRoot(container);
1637 await expect(async () => {
1638 await act(() => {
1639 root.render(<NonTerminating />);
1640 });
1641 }).rejects.toThrow('Maximum');
1642 });
1643
1644 it('can recover after falling into an infinite update loop', async () => {
1645 class NonTerminating extends React.Component {
1646 state = {step: 0};
1647 componentDidMount() {
1648 this.setState({step: 1});
1649 }
1650 componentDidUpdate() {
1651 this.setState({step: 2});
1652 }
1653 render() {
1654 return this.state.step;
1655 }
1656 }
1657
1658 class Terminating extends React.Component {
1659 state = {step: 0};
1660 componentDidMount() {
1661 this.setState({step: 1});
1662 }
1663 render() {
1664 return this.state.step;
1665 }
1666 }
1667
1668 const container = document.createElement('div');
1669 const root = ReactDOMClient.createRoot(container);
1670 await expect(async () => {
1671 await act(() => {
1672 root.render(<NonTerminating />);
1673 });
1674 }).rejects.toThrow('Maximum');
1675
1676 await act(() => {
1677 root.render(<Terminating />);
1678 });
1679 expect(container.textContent).toBe('1');
1680
1681 await expect(async () => {
1682 await act(() => {
1683 root.render(<NonTerminating />);
1684 });
1685 }).rejects.toThrow('Maximum');
1686 await act(() => {
1687 root.render(<Terminating />);
1688 });
1689 expect(container.textContent).toBe('1');
1690 });
1691
1692 it('does not fall into mutually recursive infinite update loop with same container', async () => {
1693 // Note: this test would fail if there were two or more different roots.
1694 const container = document.createElement('div');
1695 const root = ReactDOMClient.createRoot(container);
1696 class A extends React.Component {
1697 componentDidMount() {
1698 root.render(<B />);
1699 }
1700 render() {
1701 return null;
1702 }
1703 }
1704
1705 class B extends React.Component {
1706 componentDidMount() {
1707 root.render(<A />);
1708 }
1709 render() {
1710 return null;
1711 }
1712 }
1713
1714 await expect(async () => {
1715 await act(() => {
1716 root.render(<A />);
1717 });
1718 }).rejects.toThrow('Maximum');
1719 });
1720
1721 it('does not fall into an infinite error loop', async () => {
1722 function BadRender() {
1723 throw new Error('error');
1724 }
1725
1726 class ErrorBoundary extends React.Component {
1727 componentDidCatch() {
1728 // Schedule a no-op state update to avoid triggering a DEV warning in the test.
1729 this.setState({});
1730
1731 this.props.parent.remount();
1732 }
1733 render() {
1734 return <BadRender />;
1735 }
1736 }
1737
1738 class NonTerminating extends React.Component {
1739 state = {step: 0};
1740 remount() {
1741 this.setState(state => ({step: state.step + 1}));
1742 }
1743 render() {
1744 return <ErrorBoundary key={this.state.step} parent={this} />;
1745 }
1746 }
1747
1748 const container = document.createElement('div');
1749 const root = ReactDOMClient.createRoot(container);
1750 await expect(async () => {
1751 await act(() => {
1752 root.render(<NonTerminating />);
1753 });
1754 }).rejects.toThrow('Maximum');
1755 });
1756
1757 it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', async () => {
1758 const subscribers = [];
1759 const limit = 1200;
1760 class Child extends React.Component {
1761 state = {value: 'initial'};
1762 componentDidMount() {
1763 subscribers.push(this);
1764 }
1765 render() {
1766 return null;
1767 }
1768 }
1769
1770 class App extends React.Component {
1771 render() {
1772 const children = [];
1773 for (let i = 0; i < limit; i++) {
1774 children.push(<Child key={i} />);
1775 }
1776 return children;
1777 }
1778 }
1779
1780 const container = document.createElement('div');
1781 const root = ReactDOMClient.createRoot(container);
1782 await act(() => {
1783 root.render(<App />);
1784 });
1785
1786 await act(() => {
1787 subscribers.forEach(s => {
1788 s.setState({value: 'update'});
1789 });
1790 });
1791
1792 expect(subscribers.length).toBe(limit);
1793 });
1794
1795 it("warns about potential infinite loop if there's a synchronous render phase update on another component", async () => {
1796 if (
1797 !__DEV__ ||
1798 gate(
1799 flags =>
1800 !flags.enableInfiniteRenderLoopDetection ||
1801 flags.enableInfiniteRenderLoopDetectionForceThrow,
1802 )
1803 ) {
1804 return;
1805 }
1806 let setState;
1807 function App() {
1808 const [, _setState] = React.useState(0);
1809 setState = _setState;
1810 return <Child />;
1811 }
1812
1813 function Child(step) {
1814 // This will cause an infinite update loop, and a warning in dev.
1815 setState(n => n + 1);
1816 return null;
1817 }
1818
1819 const originalConsoleError = console.error;
1820 console.error = e => {
1821 if (
1822 typeof e === 'string' &&
1823 e.startsWith(
1824 'Maximum update depth exceeded. This could be an infinite loop.',
1825 )
1826 ) {
1827 Scheduler.log('stop');
1828 }
1829 };
1830 try {
1831 const container = document.createElement('div');
1832 const root = ReactDOMClient.createRoot(container);
1833 root.render(<App />);
1834 await waitFor(['stop']);
1835 } finally {
1836 console.error = originalConsoleError;
1837 }
1838 });
1839
1840 it("warns about potential infinite loop if there's an async render phase update on another component", async () => {
1841 if (
1842 !__DEV__ ||
1843 gate(
1844 flags =>
1845 !flags.enableInfiniteRenderLoopDetection ||
1846 flags.enableInfiniteRenderLoopDetectionForceThrow,
1847 )
1848 ) {
1849 return;
1850 }
1851 let setState;
1852 function App() {
1853 const [, _setState] = React.useState(0);
1854 setState = _setState;
1855 return <Child />;
1856 }
1857
1858 function Child(step) {
1859 // This will cause an infinite update loop, and a warning in dev.
1860 setState(n => n + 1);
1861 return null;
1862 }
1863
1864 const originalConsoleError = console.error;
1865 console.error = e => {
1866 if (
1867 typeof e === 'string' &&
1868 e.startsWith(
1869 'Maximum update depth exceeded. This could be an infinite loop.',
1870 )
1871 ) {
1872 Scheduler.log('stop');
1873 }
1874 };
1875 try {
1876 const container = document.createElement('div');
1877 const root = ReactDOMClient.createRoot(container);
1878 React.startTransition(() => root.render(<App />));
1879 await waitFor(['stop']);
1880 } finally {
1881 console.error = originalConsoleError;
1882 }
1883 });
1884
1885 // TODO: Replace this branch with @gate pragmas
1886 if (__DEV__) {
1887 it('warns about a deferred infinite update loop with useEffect', async () => {
1888 function NonTerminating() {
1889 const [step, setStep] = React.useState(0);
1890 React.useEffect(function myEffect() {
1891 setStep(x => x + 1);
1892 });
1893 return step;
1894 }
1895
1896 function App() {
1897 return <NonTerminating />;
1898 }
1899
1900 let error = null;
1901 let ownerStack = null;
1902 let debugStack = null;
1903 const originalConsoleError = console.error;
1904 console.error = e => {
1905 error = e;
1906 ownerStack = React.captureOwnerStack();
1907 debugStack = new Error().stack;
1908 Scheduler.log('stop');
1909 };
1910 try {
1911 const container = document.createElement('div');
1912 const root = ReactDOMClient.createRoot(container);
1913 root.render(<App />);
1914 await waitFor(['stop']);
1915 } finally {
1916 console.error = originalConsoleError;
1917 }
1918
1919 expect(error).toContain('Maximum update depth exceeded');
1920 // The currently executing effect should be on the native stack
1921 expect(debugStack).toContain('at myEffect');
1922 expect(ownerStack).toContain('at App');
1923 });
1924
1925 it('can have nested updates if they do not cross the limit', async () => {
1926 let _setStep;
1927 const LIMIT = 50;
1928
1929 function Terminating() {
1930 const [step, setStep] = React.useState(0);
1931 _setStep = setStep;
1932 React.useEffect(() => {
1933 if (step < LIMIT) {
1934 setStep(x => x + 1);
1935 }
1936 });
1937 Scheduler.log(step);
1938 return step;
1939 }
1940
1941 const container = document.createElement('div');
1942 const root = ReactDOMClient.createRoot(container);
1943 await act(() => {
1944 root.render(<Terminating />);
1945 });
1946
1947 assertLog(Array.from({length: LIMIT + 1}, (_, k) => k));
1948 expect(container.textContent).toBe('50');
1949 await act(() => {
1950 _setStep(0);
1951 });
1952 expect(container.textContent).toBe('50');
1953 });
1954
1955 it('can have many updates inside useEffect without triggering a warning', async () => {
1956 function Terminating() {
1957 const [step, setStep] = React.useState(0);
1958 React.useEffect(() => {
1959 for (let i = 0; i < 1000; i++) {
1960 setStep(x => x + 1);
1961 }
1962 Scheduler.log('Done');
1963 }, []);
1964 return step;
1965 }
1966
1967 const container = document.createElement('div');
1968 const root = ReactDOMClient.createRoot(container);
1969 await act(() => {
1970 root.render(<Terminating />);
1971 });
1972
1973 assertLog(['Done']);
1974 expect(container.textContent).toBe('1000');
1975 });
1976 }
1977
1978 it('prevents infinite update loop triggered by synchronous updates in useEffect', async () => {
1979 // Ignore flushSync warning
1980 spyOnDev(console, 'error').mockImplementation(() => {});
1981
1982 function NonTerminating() {
1983 const [step, setStep] = React.useState(0);
1984 React.useEffect(() => {
1985 // Other examples of synchronous updates in useEffect are imperative
1986 // event dispatches like `el.focus`, or `useSyncExternalStore`, which
1987 // may schedule a synchronous update upon subscribing if it detects
1988 // that the store has been mutated since the initial render.
1989 //
1990 // (Originally I wrote this test using `el.focus` but those errors
1991 // get dispatched in a JSDOM event and I don't know how to "catch" those
1992 // so that they don't fail the test.)
1993 ReactDOM.flushSync(() => {
1994 setStep(step + 1);
1995 });
1996 }, [step]);
1997 return step;
1998 }
1999
2000 const container = document.createElement('div');
2001 const errors = [];
2002 const root = ReactDOMClient.createRoot(container, {
2003 onUncaughtError: (error, errorInfo) => {
2004 errors.push(
2005 `${error.message}${normalizeCodeLocInfo(errorInfo.componentStack)}`,
2006 );
2007 },
2008 });
2009 await act(() => {
2010 ReactDOM.flushSync(() => {
2011 root.render(<NonTerminating />);
2012 });
2013 });
2014
2015 expect(errors).toEqual([
2016 'Maximum update depth exceeded. ' +
2017 'This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ' +
2018 'React limits the number of nested updates to prevent infinite loops.' +
2019 '\n in NonTerminating (at **)',
2020 ]);
2021 });
2022
2023 it('warns instead of throwing when infinite Suspense ping loop is detected via enableInfiniteRenderLoopDetection during commit phase', async () => {
2024 if (
2025 !__DEV__ ||
2026 gate(
2027 flags =>
2028 !flags.enableInfiniteRenderLoopDetection ||
2029 flags.enableInfiniteRenderLoopDetectionForceThrow,
2030 )
2031 ) {
2032 return;
2033 }
2034
2035 // When a Suspense child throws a thenable, React registers two listeners:
2036 // 1. ping (attachPingListener, render) → pingSuspendedRoot → markRootPinged
2037 // 2. retry (attachSuspenseRetryListeners, commit) → resolveRetryWakeable
2038 //
2039 // The ping path calls throwIfInfiniteUpdateLoopDetected(true) via
2040 // markRootPinged WITHOUT a prior getRootForUpdatedFiber(false) check.
2041 // When this fires during CommitContext (not RenderContext),
2042 // the isFromInfiniteRenderLoopDetectionInstrumentation=true parameter
2043 // ensures we warn instead of throw.
2044 //
2045 // Without the fix (passing false), the condition
2046 // false || (executionContext & RenderContext && ...)
2047 // evaluates to false in CommitContext, causing a throw.
2048 let currentResolve = null;
2049 let shouldStop = false;
2050
2051 function App() {
2052 const [, setState] = React.useState(0);
2053
2054 React.useLayoutEffect(() => {
2055 if (shouldStop) {
2056 return;
2057 }
2058 // Resolve the suspended thenable during commit phase (CommitContext).
2059 // The ping callback (registered first during render) fires first,
2060 // triggering markRootPinged → throwIfInfiniteUpdateLoopDetected(true).
2061 if (currentResolve !== null) {
2062 const resolve = currentResolve;
2063 currentResolve = null;
2064 resolve();
2065 }
2066 // Schedule a sync update to ensure nestedUpdateKind is
2067 // NESTED_UPDATE_SYNC_LANE at commitRootImpl epilogue.
2068 setState(n => n + 1);
2069 });
2070
2071 return (
2072 <React.Suspense fallback="loading">
2073 <SuspendingChild />
2074 </React.Suspense>
2075 );
2076 }
2077
2078 function SuspendingChild() {
2079 if (shouldStop) {
2080 return null;
2081 }
2082 // Each render throws a new thenable. React calls .then() on it twice
2083 // (ping during render, retry during commit). We collect all callbacks
2084 // so resolve() fires them in registration order: ping first.
2085 const callbacks = [];
2086 const thenable = {
2087 then(onFulfilled) {
2088 callbacks.push(onFulfilled);
2089 currentResolve = () => {
2090 for (let i = 0; i < callbacks.length; i++) {
2091 callbacks[i]();
2092 }
2093 };
2094 },
2095 };
2096
2097 throw thenable;
2098 }
2099
2100 const container = document.createElement('div');
2101 const errors = [];
2102 const root = ReactDOMClient.createRoot(container, {
2103 onUncaughtError: error => {
2104 errors.push(error.message);
2105 },
2106 });
2107
2108 const originalConsoleError = console.error;
2109 console.error = e => {
2110 if (
2111 typeof e === 'string' &&
2112 e.startsWith(
2113 'Maximum update depth exceeded. This could be an infinite loop.',
2114 )
2115 ) {
2116 // Stop the loop after the first warning so act() can finish.
2117 shouldStop = true;
2118 }
2119 };
2120
2121 try {
2122 await act(() => {
2123 root.render(<App />);
2124 });
2125 } finally {
2126 console.error = originalConsoleError;
2127 }
2128
2129 // With the fix (throwIfInfiniteUpdateLoopDetected(true) in markRootPinged):
2130 // the loop is discovered via enableInfiniteRenderLoopDetection instrumentation
2131 // and produces a warning.
2132 // Without the fix (throwIfInfiniteUpdateLoopDetected(false)):
2133 // the same check throws because executionContext is CommitContext, not
2134 // RenderContext.
2135 expect(shouldStop).toBe(true);
2136 expect(errors).toEqual([]);
2137 });
2138
2139 // @gate enableInfiniteRenderLoopDetection && enableInfiniteRenderLoopDetectionForceThrow
2140 it('throws when sync render-phase update loop is detected with force-throw enabled', async () => {
2141 // Render-phase setState on another component's hook produces a sync
2142 // recursive update. With ForceThrow enabled this should throw via
2143 // throwForcedInfiniteRenderLoopError instead of only warning in DEV.
2144 let setState;
2145 let shouldStop = false;
2146 function App() {
2147 const [, _setState] = React.useState(0);
2148 setState = _setState;
2149 return <Child />;
2150 }
2151
2152 function Child() {
2153 if (shouldStop) {
2154 return null;
2155 }
2156 setState(n => n + 1);
2157 return null;
2158 }
2159
2160 const container = document.createElement('div');
2161 const errors = [];
2162 const captureError = error => {
2163 errors.push(error.message);
2164 // Stop scheduling new updates so the test (and the gate-off variant
2165 // where the legacy error path is recoverable) can terminate cleanly
2166 // without tripping the babel infinite-loop guard.
2167 shouldStop = true;
2168 };
2169 const root = ReactDOMClient.createRoot(container, {
2170 onUncaughtError: captureError,
2171 onRecoverableError: captureError,
2172 onCaughtError: captureError,
2173 });
2174
2175 // The render-phase setState path also produces a dev-only "Cannot update
2176 // a component while rendering a different component" console.error on
2177 // every recursion. Swallow those so the test framework doesn't require
2178 // us to assert each one.
2179 const originalConsoleError = console.error;
2180 console.error = msg => {
2181 if (
2182 typeof msg === 'string' &&
2183 msg.startsWith('Cannot update a component')
2184 ) {
2185 return;
2186 }
2187 originalConsoleError(msg);
2188 };
2189 try {
2190 await act(() => {
2191 root.render(<App />);
2192 });
2193 } finally {
2194 console.error = originalConsoleError;
2195 }
2196
2197 expect(errors.length).toBeGreaterThanOrEqual(1);
2198 expect(errors[0]).toContain(
2199 'Maximum update depth exceeded. This could be an infinite loop.',
2200 );
2201 });
2202
2203 // @gate enableInfiniteRenderLoopDetection && enableInfiniteRenderLoopDetectionForceThrow
2204 it('throws when phase-spawn update loop is detected with force-throw enabled', async () => {
2205 // Wrapping the initial render in startTransition makes the render-phase
2206 // setState inherit a non-sync transition lane. After commit, the next
2207 // render is non-sync, so the loop detector classifies the recursion as
2208 // NESTED_UPDATE_PHASE_SPAWN (rather than SYNC_LANE). With ForceThrow
2209 // enabled, this branch should throw via throwForcedInfiniteRenderLoopError
2210 // instead of only warning in DEV.
2211 let setState;
2212 let shouldStop = false;
2213 // Hard cap on Child renders. Without enableInfiniteRenderLoopDetection,
2214 // the PHASE_SPAWN branch is gated off entirely, so no throw fires and
2215 // the loop would otherwise run until the babel infinite-loop guard.
2216 let renderCount = 0;
2217 const RENDER_CAP = 100;
2218 function App() {
2219 const [, _setState] = React.useState(0);
2220 setState = _setState;
2221 return <Child />;
2222 }
2223
2224 function Child() {
2225 if (shouldStop || renderCount >= RENDER_CAP) {
2226 return null;
2227 }
2228 renderCount++;
2229 setState(n => n + 1);
2230 return null;
2231 }
2232
2233 const container = document.createElement('div');
2234 const errors = [];
2235 const root = ReactDOMClient.createRoot(container, {
2236 onUncaughtError: error => {
2237 errors.push(error.message);
2238 shouldStop = true;
2239 },
2240 });
2241
2242 const originalConsoleError = console.error;
2243 console.error = msg => {
2244 if (
2245 typeof msg === 'string' &&
2246 msg.startsWith('Cannot update a component')
2247 ) {
2248 return;
2249 }
2250 originalConsoleError(msg);
2251 };
2252 try {
2253 await act(() => {
2254 React.startTransition(() => root.render(<App />));
2255 });
2256 } finally {
2257 console.error = originalConsoleError;
2258 }
2259
2260 expect(errors.length).toBeGreaterThanOrEqual(1);
2261 expect(errors[0]).toContain(
2262 'Maximum update depth exceeded. This could be an infinite loop.',
2263 );
2264 });
2265
2266 it('prevents infinite update loop triggered by too many updates in ref callbacks', async () => {
2267 let scheduleUpdate;
2268 function TooManyRefUpdates() {
2269 const [count, _scheduleUpdate] = React.useReducer(c => c + 1, 0);
2270 scheduleUpdate = _scheduleUpdate;
2271
2272 return (
2273 <div
2274 ref={() => {
2275 for (let i = 0; i < 50; i++) {
2276 scheduleUpdate(1);
2277 }
2278 }}>
2279 {count}
2280 </div>
2281 );
2282 }
2283
2284 const container = document.createElement('div');
2285 const errors = [];
2286 const root = ReactDOMClient.createRoot(container, {
2287 onUncaughtError: (error, errorInfo) => {
2288 errors.push(
2289 `${error.message}${normalizeCodeLocInfo(errorInfo.componentStack)}`,
2290 );
2291 },
2292 });
2293 await act(() => {
2294 root.render(<TooManyRefUpdates />);
2295 });
2296
2297 expect(errors).toEqual([
2298 'Maximum update depth exceeded. ' +
2299 'This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ' +
2300 'React limits the number of nested updates to prevent infinite loops.' +
2301 '\n in div' +
2302 '\n in TooManyRefUpdates (at **)',
2303 ]);
2304 });
2305 });