main
js 1,413 lines 37.6 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 ChildUpdates;
13 let MorphingComponent;
14 let React;
15 let ReactDOM;
16 let ReactDOMClient;
17 let ReactSharedInternals;
18 let Scheduler;
19 let assertLog;
20 let act;
21 let assertConsoleErrorDev;
22
23 describe('ReactCompositeComponent', () => {
24 const hasOwnProperty = Object.prototype.hasOwnProperty;
25
26 /**
27 * Performs equality by iterating through keys on an object and returning false
28 * when any key has values which are not strictly equal between the arguments.
29 * Returns true when the values of all keys are strictly equal.
30 */
31 function shallowEqual(objA: mixed, objB: mixed): boolean {
32 if (Object.is(objA, objB)) {
33 return true;
34 }
35 if (
36 typeof objA !== 'object' ||
37 objA === null ||
38 typeof objB !== 'object' ||
39 objB === null
40 ) {
41 return false;
42 }
43 const keysA = Object.keys(objA);
44 const keysB = Object.keys(objB);
45 if (keysA.length !== keysB.length) {
46 return false;
47 }
48 for (let i = 0; i < keysA.length; i++) {
49 if (
50 !hasOwnProperty.call(objB, keysA[i]) ||
51 !Object.is(objA[keysA[i]], objB[keysA[i]])
52 ) {
53 return false;
54 }
55 }
56 return true;
57 }
58
59 function shallowCompare(instance, nextProps, nextState) {
60 return (
61 !shallowEqual(instance.props, nextProps) ||
62 !shallowEqual(instance.state, nextState)
63 );
64 }
65
66 beforeEach(() => {
67 jest.resetModules();
68 React = require('react');
69 ReactDOM = require('react-dom');
70 ReactDOMClient = require('react-dom/client');
71 ReactSharedInternals =
72 require('react').__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
73 Scheduler = require('scheduler');
74 assertLog = require('internal-test-utils').assertLog;
75 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
76 });
77
78 describe('MorphingComponent', () => {
79 let instance;
80 let childInstance;
81
82 beforeEach(() => {
83 MorphingComponent = class extends React.Component {
84 state = {activated: false};
85 xRef = React.createRef();
86
87 componentDidMount() {
88 instance = this;
89 }
90
91 _toggleActivatedState = () => {
92 this.setState({activated: !this.state.activated});
93 };
94
95 render() {
96 const toggleActivatedState = this._toggleActivatedState;
97 return !this.state.activated ? (
98 <a ref={this.xRef} onClick={toggleActivatedState} />
99 ) : (
100 <b ref={this.xRef} onClick={toggleActivatedState} />
101 );
102 }
103 };
104
105 /**
106 * We'll use this to ensure that an old version is not cached when it is
107 * reallocated again.
108 */
109 ChildUpdates = class extends React.Component {
110 anchorRef = React.createRef();
111
112 componentDidMount() {
113 childInstance = this;
114 }
115
116 getAnchor = () => {
117 return this.anchorRef.current;
118 };
119
120 render() {
121 const className = this.props.anchorClassOn ? 'anchorClass' : '';
122 return this.props.renderAnchor ? (
123 <a ref={this.anchorRef} className={className} />
124 ) : (
125 <b />
126 );
127 }
128 };
129 });
130 it('should support rendering to different child types over time', async () => {
131 const root = ReactDOMClient.createRoot(document.createElement('div'));
132 await act(() => {
133 root.render(<MorphingComponent />);
134 });
135 expect(instance.xRef.current.tagName).toBe('A');
136
137 await act(() => {
138 instance._toggleActivatedState();
139 });
140 expect(instance.xRef.current.tagName).toBe('B');
141
142 await act(() => {
143 instance._toggleActivatedState();
144 });
145 expect(instance.xRef.current.tagName).toBe('A');
146 });
147
148 it('should react to state changes from callbacks', async () => {
149 const container = document.createElement('div');
150 document.body.appendChild(container);
151 const root = ReactDOMClient.createRoot(container);
152 try {
153 await act(() => {
154 root.render(<MorphingComponent />);
155 });
156 expect(instance.xRef.current.tagName).toBe('A');
157 await act(() => {
158 instance.xRef.current.click();
159 });
160 expect(instance.xRef.current.tagName).toBe('B');
161 } finally {
162 document.body.removeChild(container);
163 root.unmount();
164 }
165 });
166
167 it('should rewire refs when rendering to different child types', async () => {
168 const container = document.createElement('div');
169 const root = ReactDOMClient.createRoot(container);
170 await act(() => {
171 root.render(<MorphingComponent />);
172 });
173 expect(instance.xRef.current.tagName).toBe('A');
174
175 await act(() => {
176 instance._toggleActivatedState();
177 });
178 expect(instance.xRef.current.tagName).toBe('B');
179
180 await act(() => {
181 instance._toggleActivatedState();
182 });
183 expect(instance.xRef.current.tagName).toBe('A');
184 });
185
186 it('should not cache old DOM nodes when switching constructors', async () => {
187 const container = document.createElement('div');
188 const root = ReactDOMClient.createRoot(container);
189 await act(() => {
190 root.render(<ChildUpdates renderAnchor={true} anchorClassOn={false} />);
191 });
192 await act(() => {
193 root.render(
194 // Warm any cache
195 <ChildUpdates renderAnchor={true} anchorClassOn={true} />,
196 );
197 });
198 await act(() => {
199 root.render(
200 // Clear out the anchor
201 <ChildUpdates renderAnchor={false} anchorClassOn={true} />,
202 );
203 });
204 await act(() => {
205 root.render(
206 // rerender
207 <ChildUpdates renderAnchor={true} anchorClassOn={false} />,
208 );
209 });
210 expect(childInstance.getAnchor().className).toBe('');
211 });
212 });
213
214 it('should not support module pattern components', async () => {
215 function Child({test}) {
216 return {
217 render() {
218 return <div>{test}</div>;
219 },
220 };
221 }
222
223 const el = document.createElement('div');
224 const root = ReactDOMClient.createRoot(el);
225 await expect(async () => {
226 await act(() => {
227 root.render(<Child test="test" />);
228 });
229 }).rejects.toThrow(
230 'Objects are not valid as a React child (found: object with keys {render}).',
231 );
232
233 expect(el.textContent).toBe('');
234 });
235
236 it('should use default values for undefined props', async () => {
237 class Component extends React.Component {
238 static defaultProps = {prop: 'testKey'};
239
240 render() {
241 return <span />;
242 }
243 }
244
245 function refFn1(ref) {
246 instance1 = ref;
247 }
248
249 function refFn2(ref) {
250 instance2 = ref;
251 }
252
253 function refFn3(ref) {
254 instance3 = ref;
255 }
256
257 let instance1;
258 let instance2;
259 let instance3;
260 const root = ReactDOMClient.createRoot(document.createElement('div'));
261 await act(() => {
262 root.render(<Component ref={refFn1} />);
263 });
264 expect(instance1.props).toEqual({prop: 'testKey'});
265
266 await act(() => {
267 root.render(<Component ref={refFn2} prop={undefined} />);
268 });
269 expect(instance2.props).toEqual({prop: 'testKey'});
270
271 await act(() => {
272 root.render(<Component ref={refFn3} prop={null} />);
273 });
274 expect(instance3.props).toEqual({prop: null});
275 });
276
277 it('should not mutate passed-in props object', async () => {
278 class Component extends React.Component {
279 static defaultProps = {prop: 'testKey'};
280
281 render() {
282 return <span />;
283 }
284 }
285
286 const inputProps = {};
287 let instance1;
288 const root = ReactDOMClient.createRoot(document.createElement('div'));
289 await act(() => {
290 root.render(<Component {...inputProps} ref={ref => (instance1 = ref)} />);
291 });
292 expect(instance1.props.prop).toBe('testKey');
293
294 // We don't mutate the input, just in case the caller wants to do something
295 // with it after using it to instantiate a component
296 expect(inputProps.prop).not.toBeDefined();
297 });
298
299 it('should warn about `forceUpdate` on not-yet-mounted components', async () => {
300 class MyComponent extends React.Component {
301 constructor(props) {
302 super(props);
303 this.forceUpdate();
304 }
305 render() {
306 return <div>foo</div>;
307 }
308 }
309
310 const container = document.createElement('div');
311 const root = ReactDOMClient.createRoot(container);
312 ReactDOM.flushSync(() => {
313 root.render(<MyComponent />);
314 });
315 assertConsoleErrorDev([
316 "Can't call forceUpdate on a component that is not yet mounted. " +
317 'This is a no-op, but it might indicate a bug in your application. ' +
318 'Instead, assign to `this.state` directly or define a `state = {};` ' +
319 'class property with the desired state in the MyComponent component.\n' +
320 ' in MyComponent (at **)',
321 ]);
322
323 // No additional warning should be recorded
324 const container2 = document.createElement('div');
325 const root2 = ReactDOMClient.createRoot(container2);
326 await act(() => {
327 root2.render(<MyComponent />);
328 });
329 expect(container2.firstChild.textContent).toBe('foo');
330 });
331
332 it('should warn about `setState` on not-yet-mounted components', async () => {
333 class MyComponent extends React.Component {
334 constructor(props) {
335 super(props);
336 this.setState();
337 }
338 render() {
339 return <div>foo</div>;
340 }
341 }
342
343 const container = document.createElement('div');
344 const root = ReactDOMClient.createRoot(container);
345
346 ReactDOM.flushSync(() => {
347 root.render(<MyComponent />);
348 });
349 assertConsoleErrorDev([
350 "Can't call setState on a component that is not yet mounted. " +
351 'This is a no-op, but it might indicate a bug in your application. ' +
352 'Instead, assign to `this.state` directly or define a `state = {};` ' +
353 'class property with the desired state in the MyComponent component.\n' +
354 ' in MyComponent (at **)',
355 ]);
356
357 // No additional warning should be recorded
358 const container2 = document.createElement('div');
359 const root2 = ReactDOMClient.createRoot(container2);
360 await act(() => {
361 root2.render(<MyComponent />);
362 });
363 expect(container2.firstChild.textContent).toBe('foo');
364 });
365
366 it('should not warn about `forceUpdate` on unmounted components', async () => {
367 const container = document.createElement('div');
368 document.body.appendChild(container);
369
370 let instance;
371 class Component extends React.Component {
372 componentDidMount() {
373 instance = this;
374 }
375
376 render() {
377 return <div />;
378 }
379 }
380
381 const component = <Component />;
382 expect(component.forceUpdate).not.toBeDefined();
383 const root = ReactDOMClient.createRoot(container);
384 await act(() => {
385 root.render(component);
386 });
387
388 instance.forceUpdate();
389
390 root.unmount(container);
391
392 instance.forceUpdate();
393 instance.forceUpdate();
394 });
395
396 it('should not warn about `setState` on unmounted components', async () => {
397 const container = document.createElement('div');
398 document.body.appendChild(container);
399
400 class Component extends React.Component {
401 state = {value: 0};
402
403 render() {
404 Scheduler.log('render ' + this.state.value);
405 return <div />;
406 }
407 }
408
409 let ref;
410 const root = ReactDOMClient.createRoot(container);
411 await act(() => {
412 root.render(
413 <div>
414 <span>
415 <Component ref={c => (ref = c || ref)} />
416 </span>
417 </div>,
418 );
419 });
420
421 assertLog(['render 0']);
422
423 await act(() => {
424 ref.setState({value: 1});
425 });
426 assertLog(['render 1']);
427
428 await act(() => {
429 root.render(<div />);
430 });
431
432 await act(() => {
433 ref.setState({value: 2});
434 });
435 // setState on an unmounted component is a noop.
436 assertLog([]);
437 });
438
439 it('should silently allow `setState`, not call cb on unmounting components', async () => {
440 let cbCalled = false;
441 const container = document.createElement('div');
442 document.body.appendChild(container);
443
444 class Component extends React.Component {
445 state = {value: 0};
446
447 componentWillUnmount() {
448 expect(() => {
449 this.setState({value: 2}, function () {
450 cbCalled = true;
451 });
452 }).not.toThrow();
453 }
454
455 render() {
456 return <div />;
457 }
458 }
459 let instance;
460 const root = ReactDOMClient.createRoot(container);
461 await act(() => {
462 root.render(<Component ref={c => (instance = c)} />);
463 });
464 await act(() => {
465 instance.setState({value: 1});
466 });
467 instance.setState({value: 1});
468
469 root.unmount();
470 expect(cbCalled).toBe(false);
471 });
472
473 it('should warn when rendering a class with a render method that does not extend React.Component', async () => {
474 const container = document.createElement('div');
475 class ClassWithRenderNotExtended {
476 render() {
477 return <div />;
478 }
479 }
480 const root = ReactDOMClient.createRoot(container);
481 await expect(async () => {
482 await act(() => {
483 root.render(<ClassWithRenderNotExtended />);
484 });
485 }).rejects.toThrow(TypeError);
486 assertConsoleErrorDev([
487 'The <ClassWithRenderNotExtended /> component appears to have a render method, ' +
488 "but doesn't extend React.Component. This is likely to cause errors. " +
489 'Change ClassWithRenderNotExtended to extend React.Component instead.\n' +
490 ' in ClassWithRenderNotExtended (at **)',
491 ]);
492
493 // Test deduplication
494 await expect(async () => {
495 await act(() => {
496 root.render(<ClassWithRenderNotExtended />);
497 });
498 }).rejects.toThrow(TypeError);
499 });
500
501 it('should warn about `setState` in render', async () => {
502 const container = document.createElement('div');
503
504 class Component extends React.Component {
505 state = {value: 0};
506
507 render() {
508 Scheduler.log('render ' + this.state.value);
509 if (this.state.value === 0) {
510 this.setState({value: 1});
511 }
512 return <div>foo {this.state.value}</div>;
513 }
514 }
515
516 let instance;
517 const root = ReactDOMClient.createRoot(container);
518 ReactDOM.flushSync(() => {
519 root.render(<Component ref={ref => (instance = ref)} />);
520 });
521 assertConsoleErrorDev([
522 'Cannot update during an existing state transition (such as within ' +
523 '`render`). Render methods should be a pure function of props and state.\n' +
524 ' in Component (at **)',
525 ]);
526
527 // The setState call is queued and then executed as a second pass. This
528 // behavior is undefined though so we're free to change it to suit the
529 // implementation details.
530 assertLog(['render 0', 'render 1']);
531 expect(instance.state.value).toBe(1);
532
533 // Forcing a rerender anywhere will cause the update to happen.
534 await act(() => {
535 root.render(<Component prop={123} />);
536 });
537 assertLog(['render 1']);
538 });
539
540 it('should cleanup even if render() fatals', async () => {
541 const ownerEnabled = __DEV__;
542
543 let stashedDispatcher;
544 class BadComponent extends React.Component {
545 render() {
546 // Stash the dispatcher that was available in render so we can check
547 // that its internals also reset.
548 stashedDispatcher = ReactSharedInternals.A;
549 throw new Error();
550 }
551 }
552
553 const instance = <BadComponent />;
554 expect(ReactSharedInternals.A).toBe(null);
555
556 const root = ReactDOMClient.createRoot(document.createElement('div'));
557 await expect(async () => {
558 await act(() => {
559 root.render(instance);
560 });
561 }).rejects.toThrow();
562
563 expect(ReactSharedInternals.A).toBe(null);
564 if (ownerEnabled) {
565 expect(stashedDispatcher.getOwner()).toBe(null);
566 } else {
567 expect(stashedDispatcher.getOwner).toBe(undefined);
568 }
569 });
570
571 it('should call componentWillUnmount before unmounting', async () => {
572 const container = document.createElement('div');
573 let innerUnmounted = false;
574
575 class Component extends React.Component {
576 render() {
577 return (
578 <div>
579 <Inner />
580 Text
581 </div>
582 );
583 }
584 }
585
586 class Inner extends React.Component {
587 componentWillUnmount() {
588 innerUnmounted = true;
589 }
590
591 render() {
592 return <div />;
593 }
594 }
595
596 const root = ReactDOMClient.createRoot(container);
597 await act(() => {
598 root.render(<Component />);
599 });
600 root.unmount();
601 expect(innerUnmounted).toBe(true);
602 });
603
604 it('should warn when shouldComponentUpdate() returns undefined', async () => {
605 class ClassComponent extends React.Component {
606 state = {bogus: false};
607
608 shouldComponentUpdate() {
609 return undefined;
610 }
611
612 render() {
613 return <div />;
614 }
615 }
616 let instance;
617 const root = ReactDOMClient.createRoot(document.createElement('div'));
618 await act(() => {
619 root.render(<ClassComponent ref={ref => (instance = ref)} />);
620 });
621
622 ReactDOM.flushSync(() => {
623 instance.setState({bogus: true});
624 });
625 assertConsoleErrorDev([
626 'ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' +
627 'boolean value. Make sure to return true or false.\n' +
628 ' in ClassComponent (at **)',
629 ]);
630 });
631
632 it('should warn when componentDidUnmount method is defined', async () => {
633 class Component extends React.Component {
634 componentDidUnmount = () => {};
635
636 render() {
637 return <div />;
638 }
639 }
640
641 const root = ReactDOMClient.createRoot(document.createElement('div'));
642 ReactDOM.flushSync(() => {
643 root.render(<Component />);
644 });
645 assertConsoleErrorDev([
646 'Component has a method called ' +
647 'componentDidUnmount(). But there is no such lifecycle method. ' +
648 'Did you mean componentWillUnmount()?\n' +
649 ' in Component (at **)',
650 ]);
651 });
652
653 it('should warn when componentDidReceiveProps method is defined', () => {
654 class Component extends React.Component {
655 componentDidReceiveProps = () => {};
656
657 render() {
658 return <div />;
659 }
660 }
661
662 const root = ReactDOMClient.createRoot(document.createElement('div'));
663
664 ReactDOM.flushSync(() => {
665 root.render(<Component />);
666 });
667 assertConsoleErrorDev([
668 'Component has a method called ' +
669 'componentDidReceiveProps(). But there is no such lifecycle method. ' +
670 'If you meant to update the state in response to changing props, ' +
671 'use componentWillReceiveProps(). If you meant to fetch data or ' +
672 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().\n' +
673 ' in Component (at **)',
674 ]);
675 });
676
677 it('should warn when defaultProps was defined as an instance property', () => {
678 class Component extends React.Component {
679 constructor(props) {
680 super(props);
681 this.defaultProps = {name: 'Abhay'};
682 }
683
684 render() {
685 return <div />;
686 }
687 }
688 const root = ReactDOMClient.createRoot(document.createElement('div'));
689
690 ReactDOM.flushSync(() => {
691 root.render(<Component />);
692 });
693 assertConsoleErrorDev([
694 'Setting defaultProps as an instance property on Component is not supported ' +
695 'and will be ignored. Instead, define defaultProps as a static property on Component.\n' +
696 ' in Component (at **)',
697 ]);
698 });
699
700 it('should skip update when rerendering element in container', async () => {
701 class Parent extends React.Component {
702 render() {
703 return <div>{this.props.children}</div>;
704 }
705 }
706
707 class Child extends React.Component {
708 render() {
709 Scheduler.log('Child render');
710 return <div />;
711 }
712 }
713
714 const container = document.createElement('div');
715 const child = <Child />;
716 const root = ReactDOMClient.createRoot(container);
717 await act(() => {
718 root.render(<Parent>{child}</Parent>);
719 });
720 assertLog(['Child render']);
721
722 await act(() => {
723 root.render(<Parent>{child}</Parent>);
724 });
725 assertLog([]);
726 });
727
728 it('should disallow nested render calls', () => {
729 const root = ReactDOMClient.createRoot(document.createElement('div'));
730 class Inner extends React.Component {
731 render() {
732 return <div />;
733 }
734 }
735
736 class Outer extends React.Component {
737 render() {
738 root.render(<Inner />);
739 return <div />;
740 }
741 }
742
743 ReactDOM.flushSync(() => {
744 root.render(<Outer />);
745 });
746 assertConsoleErrorDev([
747 'Render methods should be a pure function of props and state; ' +
748 'triggering nested component updates from render is not allowed. If ' +
749 'necessary, trigger nested updates in componentDidUpdate.\n\n' +
750 'Check the render method of Outer.\n' +
751 ' in Outer (at **)',
752 ]);
753 });
754
755 it('only renders once if updated in componentWillReceiveProps', async () => {
756 let renders = 0;
757
758 class Component extends React.Component {
759 state = {updated: false};
760
761 UNSAFE_componentWillReceiveProps(props) {
762 expect(props.update).toBe(1);
763 expect(renders).toBe(1);
764 this.setState({updated: true});
765 expect(renders).toBe(1);
766 }
767
768 render() {
769 renders++;
770 return <div />;
771 }
772 }
773
774 const container = document.createElement('div');
775 const root = ReactDOMClient.createRoot(container);
776 let instance;
777
778 await act(() => {
779 root.render(<Component update={0} ref={ref => (instance = ref)} />);
780 });
781 expect(renders).toBe(1);
782 expect(instance.state.updated).toBe(false);
783
784 await act(() => {
785 root.render(<Component update={1} ref={ref => (instance = ref)} />);
786 });
787 expect(renders).toBe(2);
788 expect(instance.state.updated).toBe(true);
789 });
790
791 it('only renders once if updated in componentWillReceiveProps when batching', async () => {
792 let renders = 0;
793
794 class Component extends React.Component {
795 state = {updated: false};
796
797 UNSAFE_componentWillReceiveProps(props) {
798 expect(props.update).toBe(1);
799 expect(renders).toBe(1);
800 this.setState({updated: true});
801 expect(renders).toBe(1);
802 }
803
804 render() {
805 renders++;
806 return <div />;
807 }
808 }
809
810 const container = document.createElement('div');
811 const root = ReactDOMClient.createRoot(container);
812 let instance;
813 await act(() => {
814 root.render(<Component update={0} ref={ref => (instance = ref)} />);
815 });
816 expect(renders).toBe(1);
817 expect(instance.state.updated).toBe(false);
818 await act(() => {
819 root.render(<Component update={1} ref={ref => (instance = ref)} />);
820 });
821 expect(renders).toBe(2);
822 expect(instance.state.updated).toBe(true);
823 });
824
825 it('should warn when mutated props are passed', async () => {
826 const container = document.createElement('div');
827
828 class Foo extends React.Component {
829 constructor(props) {
830 const _props = {idx: props.idx + '!'};
831 super(_props);
832 }
833
834 render() {
835 return <span />;
836 }
837 }
838
839 const root = ReactDOMClient.createRoot(container);
840 ReactDOM.flushSync(() => {
841 root.render(<Foo idx="qwe" />);
842 });
843 assertConsoleErrorDev([
844 'When calling super() in `Foo`, make sure to pass ' +
845 "up the same props that your component's constructor was passed.\n" +
846 ' in Foo (at **)',
847 ]);
848 });
849
850 it('should only call componentWillUnmount once', async () => {
851 let app;
852 let count = 0;
853
854 class App extends React.Component {
855 render() {
856 if (this.props.stage === 1) {
857 return <UnunmountableComponent />;
858 } else {
859 return null;
860 }
861 }
862 }
863
864 class UnunmountableComponent extends React.Component {
865 componentWillUnmount() {
866 app.setState({});
867 count++;
868 throw Error('always fails');
869 }
870
871 render() {
872 return <div>Hello {this.props.name}</div>;
873 }
874 }
875
876 const container = document.createElement('div');
877
878 const setRef = ref => {
879 if (ref) {
880 app = ref;
881 }
882 };
883
884 const root = ReactDOMClient.createRoot(container);
885 await expect(async () => {
886 await act(() => {
887 root.render(<App ref={setRef} stage={1} />);
888 });
889 await act(() => {
890 root.render(<App ref={setRef} stage={2} />);
891 });
892 }).rejects.toThrow();
893 expect(count).toBe(1);
894 });
895
896 it('prepares new child before unmounting old', async () => {
897 class Spy extends React.Component {
898 UNSAFE_componentWillMount() {
899 Scheduler.log(this.props.name + ' componentWillMount');
900 }
901 render() {
902 Scheduler.log(this.props.name + ' render');
903 return <div />;
904 }
905 componentDidMount() {
906 Scheduler.log(this.props.name + ' componentDidMount');
907 }
908 componentWillUnmount() {
909 Scheduler.log(this.props.name + ' componentWillUnmount');
910 }
911 }
912
913 class Wrapper extends React.Component {
914 render() {
915 return <Spy key={this.props.name} name={this.props.name} />;
916 }
917 }
918
919 const container = document.createElement('div');
920 const root = ReactDOMClient.createRoot(container);
921 await act(() => {
922 root.render(<Wrapper name="A" />);
923 });
924
925 assertLog(['A componentWillMount', 'A render', 'A componentDidMount']);
926 await act(() => {
927 root.render(<Wrapper name="B" />);
928 });
929
930 assertLog([
931 'B componentWillMount',
932 'B render',
933 'A componentWillUnmount',
934 'B componentDidMount',
935 ]);
936 });
937
938 it('respects a shallow shouldComponentUpdate implementation', async () => {
939 class PlasticWrap extends React.Component {
940 constructor(props, context) {
941 super(props, context);
942 this.state = {
943 color: 'green',
944 };
945 this.appleRef = React.createRef();
946 }
947
948 render() {
949 return <Apple color={this.state.color} ref={this.appleRef} />;
950 }
951 }
952
953 class Apple extends React.Component {
954 state = {
955 cut: false,
956 slices: 1,
957 };
958
959 shouldComponentUpdate(nextProps, nextState) {
960 return shallowCompare(this, nextProps, nextState);
961 }
962
963 cut() {
964 this.setState({
965 cut: true,
966 slices: 10,
967 });
968 }
969
970 eatSlice() {
971 this.setState({
972 slices: this.state.slices - 1,
973 });
974 }
975
976 render() {
977 const {color} = this.props;
978 const {cut, slices} = this.state;
979
980 Scheduler.log(`${color} ${cut} ${slices}`);
981 return <div />;
982 }
983 }
984
985 const container = document.createElement('div');
986 const root = ReactDOMClient.createRoot(container);
987 let instance;
988 await act(() => {
989 root.render(<PlasticWrap ref={ref => (instance = ref)} />);
990 });
991 assertLog(['green false 1']);
992
993 // Do not re-render based on props
994 await act(() => {
995 instance.setState({color: 'green'});
996 });
997 assertLog([]);
998
999 // Re-render based on props
1000 await act(() => {
1001 instance.setState({color: 'red'});
1002 });
1003 assertLog(['red false 1']);
1004
1005 // Re-render base on state
1006 await act(() => {
1007 instance.appleRef.current.cut();
1008 });
1009 assertLog(['red true 10']);
1010
1011 // No re-render based on state
1012 await act(() => {
1013 instance.appleRef.current.cut();
1014 });
1015 assertLog([]);
1016
1017 // Re-render based on state again
1018 await act(() => {
1019 instance.appleRef.current.eatSlice();
1020 });
1021 assertLog(['red true 9']);
1022 });
1023
1024 it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', async () => {
1025 function getInitialState() {
1026 return {
1027 foo: [1, 2, 3],
1028 bar: {a: 4, b: 5, c: 6},
1029 };
1030 }
1031
1032 const initialSettings = getInitialState();
1033
1034 class Component extends React.Component {
1035 state = initialSettings;
1036
1037 shouldComponentUpdate(nextProps, nextState) {
1038 return shallowCompare(this, nextProps, nextState);
1039 }
1040
1041 render() {
1042 const {foo, bar} = this.state;
1043 Scheduler.log(`{foo:[${foo}],bar:{a:${bar.a},b:${bar.b},c:${bar.c}}`);
1044 return <div />;
1045 }
1046 }
1047
1048 const container = document.createElement('div');
1049 const root = ReactDOMClient.createRoot(container);
1050 let instance;
1051 await act(() => {
1052 root.render(<Component ref={ref => (instance = ref)} />);
1053 });
1054 assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
1055
1056 // Do not re-render if state is equal
1057 const settings = {
1058 foo: initialSettings.foo,
1059 bar: initialSettings.bar,
1060 };
1061 await act(() => {
1062 instance.setState(settings);
1063 });
1064 assertLog([]);
1065
1066 // Re-render because one field changed
1067 initialSettings.foo = [1, 2, 3];
1068 await act(() => {
1069 instance.setState(initialSettings);
1070 });
1071 assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
1072
1073 // Re-render because the object changed
1074 await act(() => {
1075 instance.setState(getInitialState());
1076 });
1077 assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
1078 });
1079
1080 it('should call setState callback with no arguments', async () => {
1081 let mockArgs;
1082 class Component extends React.Component {
1083 componentDidMount() {
1084 this.setState({}, (...args) => (mockArgs = args));
1085 }
1086 render() {
1087 return false;
1088 }
1089 }
1090 const root = ReactDOMClient.createRoot(document.createElement('div'));
1091 await act(() => {
1092 root.render(<Component />);
1093 });
1094
1095 expect(mockArgs.length).toEqual(0);
1096 });
1097
1098 it('this.state should be updated on setState callback inside componentWillMount', async () => {
1099 const div = document.createElement('div');
1100 let stateSuccessfullyUpdated = false;
1101
1102 class Component extends React.Component {
1103 constructor(props, context) {
1104 super(props, context);
1105 this.state = {
1106 hasUpdatedState: false,
1107 };
1108 }
1109
1110 UNSAFE_componentWillMount() {
1111 this.setState(
1112 {hasUpdatedState: true},
1113 () => (stateSuccessfullyUpdated = this.state.hasUpdatedState),
1114 );
1115 }
1116
1117 render() {
1118 return <div>{this.props.children}</div>;
1119 }
1120 }
1121
1122 const root = ReactDOMClient.createRoot(div);
1123 await act(() => {
1124 root.render(<Component />);
1125 });
1126
1127 expect(stateSuccessfullyUpdated).toBe(true);
1128 });
1129
1130 it('should call the setState callback even if shouldComponentUpdate = false', async () => {
1131 const mockFn = jest.fn().mockReturnValue(false);
1132 const div = document.createElement('div');
1133
1134 class Component extends React.Component {
1135 constructor(props, context) {
1136 super(props, context);
1137 this.state = {
1138 hasUpdatedState: false,
1139 };
1140 }
1141
1142 UNSAFE_componentWillMount() {
1143 instance = this;
1144 }
1145
1146 shouldComponentUpdate() {
1147 return mockFn();
1148 }
1149
1150 render() {
1151 return <div>{this.state.hasUpdatedState}</div>;
1152 }
1153 }
1154
1155 const root = ReactDOMClient.createRoot(div);
1156 let instance;
1157 await act(() => {
1158 root.render(<Component ref={ref => (instance = ref)} />);
1159 });
1160
1161 expect(instance).toBeDefined();
1162 expect(mockFn).not.toHaveBeenCalled();
1163
1164 await act(() => {
1165 instance.setState({hasUpdatedState: true}, () => {
1166 expect(mockFn).toHaveBeenCalled();
1167 expect(instance.state.hasUpdatedState).toBe(true);
1168 Scheduler.log('setState callback called');
1169 });
1170 });
1171
1172 assertLog(['setState callback called']);
1173 });
1174
1175 it('should return a meaningful warning when constructor is returned', async () => {
1176 class RenderTextInvalidConstructor extends React.Component {
1177 constructor(props) {
1178 super(props);
1179 return {something: false};
1180 }
1181
1182 render() {
1183 return <div />;
1184 }
1185 }
1186
1187 const root = ReactDOMClient.createRoot(document.createElement('div'));
1188 await expect(async () => {
1189 await act(() => {
1190 root.render(<RenderTextInvalidConstructor />);
1191 });
1192 }).rejects.toThrow();
1193 assertConsoleErrorDev([
1194 'No `render` method found on the RenderTextInvalidConstructor instance: ' +
1195 'did you accidentally return an object from the constructor?\n' +
1196 ' in RenderTextInvalidConstructor (at **)',
1197 'No `render` method found on the RenderTextInvalidConstructor instance: ' +
1198 'did you accidentally return an object from the constructor?\n' +
1199 ' in RenderTextInvalidConstructor (at **)',
1200 ]);
1201 });
1202
1203 it('should warn about reassigning this.props while rendering', () => {
1204 class Bad extends React.Component {
1205 componentDidMount() {}
1206 componentDidUpdate() {}
1207 render() {
1208 this.props = {...this.props};
1209 return null;
1210 }
1211 }
1212
1213 const container = document.createElement('div');
1214 const root = ReactDOMClient.createRoot(container);
1215 ReactDOM.flushSync(() => {
1216 root.render(<Bad />);
1217 });
1218 assertConsoleErrorDev([
1219 'It looks like Bad is reassigning its own `this.props` while rendering. ' +
1220 'This is not supported and can lead to confusing bugs.\n' +
1221 ' in Bad (at **)',
1222 ]);
1223 });
1224
1225 it('should return error if render is not defined', async () => {
1226 class RenderTestUndefinedRender extends React.Component {}
1227
1228 const root = ReactDOMClient.createRoot(document.createElement('div'));
1229 await expect(async () => {
1230 await act(() => {
1231 root.render(<RenderTestUndefinedRender />);
1232 });
1233 }).rejects.toThrow();
1234 assertConsoleErrorDev([
1235 'No `render` method found on the RenderTestUndefinedRender instance: ' +
1236 'you may have forgotten to define `render`.\n' +
1237 ' in RenderTestUndefinedRender (at **)',
1238 'No `render` method found on the RenderTestUndefinedRender instance: ' +
1239 'you may have forgotten to define `render`.\n' +
1240 ' in RenderTestUndefinedRender (at **)',
1241 ]);
1242 });
1243
1244 // Regression test for accidental breaking change
1245 // https://github.com/facebook/react/issues/13580
1246 it('should support classes shadowing isReactComponent', async () => {
1247 class Shadow extends React.Component {
1248 isReactComponent() {}
1249 render() {
1250 return <div />;
1251 }
1252 }
1253 const container = document.createElement('div');
1254 const root = ReactDOMClient.createRoot(container);
1255 await act(() => {
1256 root.render(<Shadow />);
1257 });
1258 expect(container.firstChild.tagName).toBe('DIV');
1259 });
1260
1261 it('should not warn on updating function component from componentWillMount', async () => {
1262 let setState;
1263 let ref;
1264 function A() {
1265 const [state, _setState] = React.useState(null);
1266 setState = _setState;
1267 return <div ref={r => (ref = r)}>{state}</div>;
1268 }
1269 class B extends React.Component {
1270 UNSAFE_componentWillMount() {
1271 setState(1);
1272 }
1273 render() {
1274 return null;
1275 }
1276 }
1277 function Parent() {
1278 return (
1279 <div>
1280 <A />
1281 <B />
1282 </div>
1283 );
1284 }
1285 const container = document.createElement('div');
1286 const root = ReactDOMClient.createRoot(container);
1287 await act(() => {
1288 root.render(<Parent />);
1289 });
1290
1291 expect(ref.textContent).toBe('1');
1292 });
1293
1294 it('should not warn on updating function component from componentWillUpdate', async () => {
1295 let setState;
1296 let ref;
1297 function A() {
1298 const [state, _setState] = React.useState();
1299 setState = _setState;
1300 return <div ref={r => (ref = r)}>{state}</div>;
1301 }
1302 class B extends React.Component {
1303 UNSAFE_componentWillUpdate() {
1304 setState(1);
1305 }
1306 render() {
1307 return null;
1308 }
1309 }
1310 function Parent() {
1311 return (
1312 <div>
1313 <A />
1314 <B />
1315 </div>
1316 );
1317 }
1318 const container = document.createElement('div');
1319 const root = ReactDOMClient.createRoot(container);
1320 await act(() => {
1321 root.render(<Parent />);
1322 });
1323 await act(() => {
1324 root.render(<Parent />);
1325 });
1326
1327 expect(ref.textContent).toBe('1');
1328 });
1329
1330 it('should not warn on updating function component from componentWillReceiveProps', async () => {
1331 let setState;
1332 let ref;
1333 function A() {
1334 const [state, _setState] = React.useState();
1335 setState = _setState;
1336 return <div ref={r => (ref = r)}>{state}</div>;
1337 }
1338
1339 class B extends React.Component {
1340 UNSAFE_componentWillReceiveProps() {
1341 setState(1);
1342 }
1343 render() {
1344 return null;
1345 }
1346 }
1347 function Parent() {
1348 return (
1349 <div>
1350 <A />
1351 <B />
1352 </div>
1353 );
1354 }
1355 const container = document.createElement('div');
1356 const root = ReactDOMClient.createRoot(container);
1357 await act(() => {
1358 root.render(<Parent />);
1359 });
1360 await act(() => {
1361 root.render(<Parent />);
1362 });
1363
1364 expect(ref.textContent).toBe('1');
1365 });
1366
1367 it('should warn on updating function component from render', () => {
1368 let setState;
1369 let ref;
1370 function A() {
1371 const [state, _setState] = React.useState(0);
1372 setState = _setState;
1373 return <div ref={r => (ref = r)}>{state}</div>;
1374 }
1375
1376 class B extends React.Component {
1377 render() {
1378 setState(c => c + 1);
1379 return null;
1380 }
1381 }
1382 function Parent() {
1383 return (
1384 <div>
1385 <A />
1386 <B />
1387 </div>
1388 );
1389 }
1390 const container = document.createElement('div');
1391 const root = ReactDOMClient.createRoot(container);
1392 ReactDOM.flushSync(() => {
1393 root.render(<Parent />);
1394 });
1395 assertConsoleErrorDev([
1396 'Cannot update a component (`A`) while rendering a different component (`B`). ' +
1397 'To locate the bad setState() call inside `B`, ' +
1398 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1399 ' in Parent (at **)',
1400 ]);
1401
1402 // We error, but still update the state.
1403 expect(ref.textContent).toBe('1');
1404
1405 // Dedupe.
1406 ReactDOM.flushSync(() => {
1407 root.render(<Parent />);
1408 });
1409
1410 // We error, but still update the state.
1411 expect(ref.textContent).toBe('2');
1412 });
1413 });