main
js 1,538 lines 54 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 act;
13
14 let React;
15 let ReactDOMClient;
16 let assertConsoleErrorDev;
17 let assertConsoleWarnDev;
18
19 const clone = function (o) {
20 return JSON.parse(JSON.stringify(o));
21 };
22
23 const GET_INIT_STATE_RETURN_VAL = {
24 hasWillMountCompleted: false,
25 hasRenderCompleted: false,
26 hasDidMountCompleted: false,
27 hasWillUnmountCompleted: false,
28 };
29
30 const INIT_RENDER_STATE = {
31 hasWillMountCompleted: true,
32 hasRenderCompleted: false,
33 hasDidMountCompleted: false,
34 hasWillUnmountCompleted: false,
35 };
36
37 const DID_MOUNT_STATE = {
38 hasWillMountCompleted: true,
39 hasRenderCompleted: true,
40 hasDidMountCompleted: false,
41 hasWillUnmountCompleted: false,
42 };
43
44 const NEXT_RENDER_STATE = {
45 hasWillMountCompleted: true,
46 hasRenderCompleted: true,
47 hasDidMountCompleted: true,
48 hasWillUnmountCompleted: false,
49 };
50
51 const WILL_UNMOUNT_STATE = {
52 hasWillMountCompleted: true,
53 hasDidMountCompleted: true,
54 hasRenderCompleted: true,
55 hasWillUnmountCompleted: false,
56 };
57
58 const POST_WILL_UNMOUNT_STATE = {
59 hasWillMountCompleted: true,
60 hasDidMountCompleted: true,
61 hasRenderCompleted: true,
62 hasWillUnmountCompleted: true,
63 };
64
65 /**
66 * TODO: We should make any setState calls fail in
67 * `getInitialState` and `componentWillMount`. They will usually fail
68 * anyways because `this._renderedComponent` is empty, however, if a component
69 * is *reused*, then that won't be the case and things will appear to work in
70 * some cases. Better to just block all updates in initialization.
71 */
72 describe('ReactComponentLifeCycle', () => {
73 beforeEach(() => {
74 jest.resetModules();
75
76 ({
77 act,
78 assertConsoleErrorDev,
79 assertConsoleWarnDev,
80 } = require('internal-test-utils'));
81
82 React = require('react');
83 ReactDOMClient = require('react-dom/client');
84 });
85
86 it('should not reuse an instance when it has been unmounted', async () => {
87 const container = document.createElement('div');
88
89 class StatefulComponent extends React.Component {
90 state = {};
91
92 render() {
93 return <div />;
94 }
95 }
96
97 const element = <StatefulComponent />;
98 let root = ReactDOMClient.createRoot(container);
99 await act(() => {
100 root.render(element);
101 });
102
103 const firstInstance = container.firstChild;
104 await act(() => {
105 root.unmount();
106 });
107 root = ReactDOMClient.createRoot(container);
108 await act(() => {
109 root.render(element);
110 });
111
112 const secondInstance = container.firstChild;
113 expect(firstInstance).not.toBe(secondInstance);
114 });
115
116 /**
117 * If a state update triggers rerendering that in turn fires an onDOMReady,
118 * that second onDOMReady should not fail.
119 */
120 it('should fire onDOMReady when already in onDOMReady', async () => {
121 const _testJournal = [];
122
123 class Child extends React.Component {
124 componentDidMount() {
125 _testJournal.push('Child:onDOMReady');
126 }
127
128 render() {
129 return <div />;
130 }
131 }
132
133 class SwitcherParent extends React.Component {
134 constructor(props) {
135 super(props);
136 _testJournal.push('SwitcherParent:getInitialState');
137 this.state = {showHasOnDOMReadyComponent: false};
138 }
139
140 componentDidMount() {
141 _testJournal.push('SwitcherParent:onDOMReady');
142 this.switchIt();
143 }
144
145 switchIt = () => {
146 this.setState({showHasOnDOMReadyComponent: true});
147 };
148
149 render() {
150 return (
151 <div>
152 {this.state.showHasOnDOMReadyComponent ? <Child /> : <div />}
153 </div>
154 );
155 }
156 }
157
158 const container = document.createElement('div');
159 const root = ReactDOMClient.createRoot(container);
160
161 await act(() => {
162 root.render(<SwitcherParent />);
163 });
164
165 expect(_testJournal).toEqual([
166 'SwitcherParent:getInitialState',
167 'SwitcherParent:onDOMReady',
168 'Child:onDOMReady',
169 ]);
170 });
171
172 // You could assign state here, but not access members of it, unless you
173 // had provided a getInitialState method.
174 it('throws when accessing state in componentWillMount', async () => {
175 class StatefulComponent extends React.Component {
176 UNSAFE_componentWillMount() {
177 void this.state.yada;
178 }
179
180 render() {
181 return <div />;
182 }
183 }
184
185 const container = document.createElement('div');
186 const root = ReactDOMClient.createRoot(container);
187 await expect(
188 act(() => {
189 root.render(<StatefulComponent />);
190 }),
191 ).rejects.toThrow();
192 });
193
194 it('should allow update state inside of componentWillMount', () => {
195 class StatefulComponent extends React.Component {
196 UNSAFE_componentWillMount() {
197 this.setState({stateField: 'something'});
198 }
199
200 render() {
201 return <div />;
202 }
203 }
204
205 expect(async function () {
206 const container = document.createElement('div');
207 const root = ReactDOMClient.createRoot(container);
208
209 await act(() => {
210 root.render(<StatefulComponent />);
211 });
212 }).not.toThrow();
213 });
214
215 it("warns if setting 'this.state = props'", async () => {
216 class StatefulComponent extends React.Component {
217 constructor(props, context) {
218 super(props, context);
219 this.state = props;
220 }
221 render() {
222 return <div />;
223 }
224 }
225
226 const container = document.createElement('div');
227 const root = ReactDOMClient.createRoot(container);
228 await act(() => {
229 root.render(<StatefulComponent />);
230 });
231 assertConsoleErrorDev([
232 'StatefulComponent: It is not recommended to assign props directly to state ' +
233 "because updates to props won't be reflected in state. " +
234 'In most cases, it is better to use props directly.\n' +
235 ' in StatefulComponent (at **)',
236 ]);
237 });
238
239 it('should not allow update state inside of getInitialState', async () => {
240 class StatefulComponent extends React.Component {
241 constructor(props, context) {
242 super(props, context);
243 this.setState({stateField: 'something'});
244
245 this.state = {stateField: 'somethingelse'};
246 }
247
248 render() {
249 return <div />;
250 }
251 }
252
253 let container = document.createElement('div');
254 let root = ReactDOMClient.createRoot(container);
255 await act(() => {
256 root.render(<StatefulComponent />);
257 });
258 assertConsoleErrorDev([
259 "Can't call setState on a component that is not yet mounted. " +
260 'This is a no-op, but it might indicate a bug in your application. ' +
261 'Instead, assign to `this.state` directly or define a `state = {};` ' +
262 'class property with the desired state in the StatefulComponent component.\n' +
263 ' in StatefulComponent (at **)',
264 ]);
265
266 container = document.createElement('div');
267 root = ReactDOMClient.createRoot(container);
268 await act(() => {
269 root.render(<StatefulComponent />);
270 });
271 });
272
273 it('should carry through each of the phases of setup', async () => {
274 class LifeCycleComponent extends React.Component {
275 constructor(props, context) {
276 super(props, context);
277 this._testJournal = {};
278 const initState = {
279 hasWillMountCompleted: false,
280 hasDidMountCompleted: false,
281 hasRenderCompleted: false,
282 hasWillUnmountCompleted: false,
283 };
284 this._testJournal.returnedFromGetInitialState = clone(initState);
285 this.state = initState;
286 }
287
288 UNSAFE_componentWillMount() {
289 this._testJournal.stateAtStartOfWillMount = clone(this.state);
290 this.state.hasWillMountCompleted = true;
291 }
292
293 componentDidMount() {
294 this._testJournal.stateAtStartOfDidMount = clone(this.state);
295 this.setState({hasDidMountCompleted: true});
296 }
297
298 render() {
299 const isInitialRender = !this.state.hasRenderCompleted;
300 if (isInitialRender) {
301 this._testJournal.stateInInitialRender = clone(this.state);
302 } else {
303 this._testJournal.stateInLaterRender = clone(this.state);
304 }
305 // you would *NEVER* do anything like this in real code!
306 this.state.hasRenderCompleted = true;
307 return <div ref={React.createRef()}>I am the inner DIV</div>;
308 }
309
310 componentWillUnmount() {
311 this._testJournal.stateAtStartOfWillUnmount = clone(this.state);
312 this.state.hasWillUnmountCompleted = true;
313 }
314 }
315
316 // A component that is merely "constructed" (as in "constructor") but not
317 // yet initialized, or rendered.
318 const root = ReactDOMClient.createRoot(document.createElement('div'));
319
320 const instanceRef = React.createRef();
321 await act(() => {
322 root.render(<LifeCycleComponent ref={instanceRef} />);
323 });
324 const instance = instanceRef.current;
325
326 // getInitialState
327 expect(instance._testJournal.returnedFromGetInitialState).toEqual(
328 GET_INIT_STATE_RETURN_VAL,
329 );
330
331 // componentWillMount
332 expect(instance._testJournal.stateAtStartOfWillMount).toEqual(
333 instance._testJournal.returnedFromGetInitialState,
334 );
335
336 // componentDidMount
337 expect(instance._testJournal.stateAtStartOfDidMount).toEqual(
338 DID_MOUNT_STATE,
339 );
340
341 // initial render
342 expect(instance._testJournal.stateInInitialRender).toEqual(
343 INIT_RENDER_STATE,
344 );
345
346 // Now *update the component*
347 instance.forceUpdate();
348
349 // render 2nd time
350 expect(instance._testJournal.stateInLaterRender).toEqual(NEXT_RENDER_STATE);
351
352 await act(() => {
353 root.unmount();
354 });
355
356 expect(instance._testJournal.stateAtStartOfWillUnmount).toEqual(
357 WILL_UNMOUNT_STATE,
358 );
359 // componentWillUnmount called right before unmount.
360
361 // But the current lifecycle of the component is unmounted.
362 expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE);
363 });
364
365 it('should not throw when updating an auxiliary component', async () => {
366 class Tooltip extends React.Component {
367 render() {
368 return <div>{this.props.children}</div>;
369 }
370
371 componentDidMount() {
372 const container = document.createElement('div');
373 this.root = ReactDOMClient.createRoot(container);
374 this.updateTooltip();
375 }
376
377 componentDidUpdate() {
378 this.updateTooltip();
379 }
380
381 updateTooltip = () => {
382 // Even though this.props.tooltip has an owner, updating it shouldn't
383 // throw here because it's mounted as a root component
384 this.root.render(this.props.tooltip, this.container);
385 };
386 }
387
388 class Component extends React.Component {
389 render() {
390 return (
391 <Tooltip
392 ref={React.createRef()}
393 tooltip={<div>{this.props.tooltipText}</div>}>
394 {this.props.text}
395 </Tooltip>
396 );
397 }
398 }
399
400 const root = ReactDOMClient.createRoot(document.createElement('div'));
401 await act(() => {
402 root.render(<Component text="uno" tooltipText="one" />);
403 });
404
405 // Since `instance` is a root component, we can set its props. This also
406 // makes Tooltip rerender the tooltip component, which shouldn't throw.
407 await act(() => {
408 root.render(<Component text="dos" tooltipText="two" />);
409 });
410 });
411
412 it('should allow state updates in componentDidMount', async () => {
413 /**
414 * calls setState in an componentDidMount.
415 */
416 class SetStateInComponentDidMount extends React.Component {
417 state = {
418 stateField: this.props.valueToUseInitially,
419 };
420
421 componentDidMount() {
422 this.setState({stateField: this.props.valueToUseInOnDOMReady});
423 }
424
425 render() {
426 return <div />;
427 }
428 }
429
430 let instance;
431 const container = document.createElement('div');
432 const root = ReactDOMClient.createRoot(container);
433 await act(() => {
434 root.render(
435 <SetStateInComponentDidMount
436 ref={current => (instance = current)}
437 valueToUseInitially="hello"
438 valueToUseInOnDOMReady="goodbye"
439 />,
440 );
441 });
442
443 expect(instance.state.stateField).toBe('goodbye');
444 });
445
446 it('should call nested legacy lifecycle methods in the right order', async () => {
447 let log;
448 const logger = function (msg) {
449 return function () {
450 // return true for shouldComponentUpdate
451 log.push(msg);
452 return true;
453 };
454 };
455 class Outer extends React.Component {
456 UNSAFE_componentWillMount = logger('outer componentWillMount');
457 componentDidMount = logger('outer componentDidMount');
458 UNSAFE_componentWillReceiveProps = logger(
459 'outer componentWillReceiveProps',
460 );
461 shouldComponentUpdate = logger('outer shouldComponentUpdate');
462 UNSAFE_componentWillUpdate = logger('outer componentWillUpdate');
463 componentDidUpdate = logger('outer componentDidUpdate');
464 componentWillUnmount = logger('outer componentWillUnmount');
465 render() {
466 return (
467 <div>
468 <Inner x={this.props.x} />
469 </div>
470 );
471 }
472 }
473
474 class Inner extends React.Component {
475 UNSAFE_componentWillMount = logger('inner componentWillMount');
476 componentDidMount = logger('inner componentDidMount');
477 UNSAFE_componentWillReceiveProps = logger(
478 'inner componentWillReceiveProps',
479 );
480 shouldComponentUpdate = logger('inner shouldComponentUpdate');
481 UNSAFE_componentWillUpdate = logger('inner componentWillUpdate');
482 componentDidUpdate = logger('inner componentDidUpdate');
483 componentWillUnmount = logger('inner componentWillUnmount');
484 render() {
485 return <span>{this.props.x}</span>;
486 }
487 }
488
489 const root = ReactDOMClient.createRoot(document.createElement('div'));
490 log = [];
491 await act(() => {
492 root.render(<Outer x={1} />);
493 });
494 expect(log).toEqual([
495 'outer componentWillMount',
496 'inner componentWillMount',
497 'inner componentDidMount',
498 'outer componentDidMount',
499 ]);
500
501 // Dedup warnings
502 log = [];
503 await act(() => {
504 root.render(<Outer x={2} />);
505 });
506 expect(log).toEqual([
507 'outer componentWillReceiveProps',
508 'outer shouldComponentUpdate',
509 'outer componentWillUpdate',
510 'inner componentWillReceiveProps',
511 'inner shouldComponentUpdate',
512 'inner componentWillUpdate',
513 'inner componentDidUpdate',
514 'outer componentDidUpdate',
515 ]);
516
517 log = [];
518 await act(() => {
519 root.unmount();
520 });
521 expect(log).toEqual([
522 'outer componentWillUnmount',
523 'inner componentWillUnmount',
524 ]);
525 });
526
527 it('should call nested new lifecycle methods in the right order', async () => {
528 let log;
529 const logger = function (msg) {
530 return function () {
531 // return true for shouldComponentUpdate
532 log.push(msg);
533 return true;
534 };
535 };
536 class Outer extends React.Component {
537 state = {};
538 static getDerivedStateFromProps(props, prevState) {
539 log.push('outer getDerivedStateFromProps');
540 return null;
541 }
542 componentDidMount = logger('outer componentDidMount');
543 shouldComponentUpdate = logger('outer shouldComponentUpdate');
544 getSnapshotBeforeUpdate = logger('outer getSnapshotBeforeUpdate');
545 componentDidUpdate = logger('outer componentDidUpdate');
546 componentWillUnmount = logger('outer componentWillUnmount');
547 render() {
548 return (
549 <div>
550 <Inner x={this.props.x} />
551 </div>
552 );
553 }
554 }
555
556 class Inner extends React.Component {
557 state = {};
558 static getDerivedStateFromProps(props, prevState) {
559 log.push('inner getDerivedStateFromProps');
560 return null;
561 }
562 componentDidMount = logger('inner componentDidMount');
563 shouldComponentUpdate = logger('inner shouldComponentUpdate');
564 getSnapshotBeforeUpdate = logger('inner getSnapshotBeforeUpdate');
565 componentDidUpdate = logger('inner componentDidUpdate');
566 componentWillUnmount = logger('inner componentWillUnmount');
567 render() {
568 return <span>{this.props.x}</span>;
569 }
570 }
571
572 const root = ReactDOMClient.createRoot(document.createElement('div'));
573
574 log = [];
575 await act(() => {
576 root.render(<Outer x={1} />);
577 });
578 expect(log).toEqual([
579 'outer getDerivedStateFromProps',
580 'inner getDerivedStateFromProps',
581 'inner componentDidMount',
582 'outer componentDidMount',
583 ]);
584
585 // Dedup warnings
586 log = [];
587 await act(() => {
588 root.render(<Outer x={2} />);
589 });
590 expect(log).toEqual([
591 'outer getDerivedStateFromProps',
592 'outer shouldComponentUpdate',
593 'inner getDerivedStateFromProps',
594 'inner shouldComponentUpdate',
595 'inner getSnapshotBeforeUpdate',
596 'outer getSnapshotBeforeUpdate',
597 'inner componentDidUpdate',
598 'outer componentDidUpdate',
599 ]);
600
601 log = [];
602 await act(() => {
603 root.unmount();
604 });
605 expect(log).toEqual([
606 'outer componentWillUnmount',
607 'inner componentWillUnmount',
608 ]);
609 });
610
611 it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => {
612 class Component extends React.Component {
613 state = {};
614 static getDerivedStateFromProps() {
615 return null;
616 }
617 componentWillMount() {
618 throw Error('unexpected');
619 }
620 componentWillReceiveProps() {
621 throw Error('unexpected');
622 }
623 componentWillUpdate() {
624 throw Error('unexpected');
625 }
626 render() {
627 return null;
628 }
629 }
630
631 const root = ReactDOMClient.createRoot(document.createElement('div'));
632 await act(() => {
633 root.render(<Component />);
634 });
635 assertConsoleErrorDev([
636 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
637 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
638 ' componentWillMount\n' +
639 ' componentWillReceiveProps\n' +
640 ' componentWillUpdate\n\n' +
641 'The above lifecycles should be removed. Learn more about this warning here:\n' +
642 'https://react.dev/link/unsafe-component-lifecycles\n' +
643 ' in Component (at **)',
644 ]);
645 assertConsoleWarnDev([
646 'componentWillMount has been renamed, and is not recommended for use. ' +
647 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
648 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
649 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
650 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
651 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
652 'Please update the following components: Component',
653 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
654 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
655 '* Move data fetching code or side effects to componentDidUpdate.\n' +
656 "* If you're updating state whenever props change, refactor your code to use " +
657 'memoization techniques or move it to static getDerivedStateFromProps. ' +
658 'Learn more at: https://react.dev/link/derived-state\n' +
659 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' +
660 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
661 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
662 'Please update the following components: Component',
663 'componentWillUpdate has been renamed, and is not recommended for use. ' +
664 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
665 '* Move data fetching code or side effects to componentDidUpdate.\n' +
666 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' +
667 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
668 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
669 'Please update the following components: Component',
670 ]);
671 });
672
673 it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => {
674 class Component extends React.Component {
675 state = {};
676 getSnapshotBeforeUpdate() {
677 return null;
678 }
679 componentWillMount() {
680 throw Error('unexpected');
681 }
682 componentWillReceiveProps() {
683 throw Error('unexpected');
684 }
685 componentWillUpdate() {
686 throw Error('unexpected');
687 }
688 componentDidUpdate() {}
689 render() {
690 return null;
691 }
692 }
693
694 const root = ReactDOMClient.createRoot(document.createElement('div'));
695 await act(() => {
696 root.render(<Component value={1} />);
697 });
698 assertConsoleErrorDev([
699 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
700 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
701 ' componentWillMount\n' +
702 ' componentWillReceiveProps\n' +
703 ' componentWillUpdate\n\n' +
704 'The above lifecycles should be removed. Learn more about this warning here:\n' +
705 'https://react.dev/link/unsafe-component-lifecycles\n' +
706 ' in Component (at **)',
707 ]);
708 assertConsoleWarnDev([
709 'componentWillMount has been renamed, and is not recommended for use. ' +
710 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
711 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
712 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
713 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
714 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
715 'Please update the following components: Component',
716 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
717 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
718 '* Move data fetching code or side effects to componentDidUpdate.\n' +
719 "* If you're updating state whenever props change, refactor your code to use " +
720 'memoization techniques or move it to static getDerivedStateFromProps. ' +
721 'Learn more at: https://react.dev/link/derived-state\n' +
722 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' +
723 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
724 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
725 'Please update the following components: Component',
726 'componentWillUpdate has been renamed, and is not recommended for use. ' +
727 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
728 '* Move data fetching code or side effects to componentDidUpdate.\n' +
729 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' +
730 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
731 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
732 'Please update the following components: Component',
733 ]);
734
735 await act(() => {
736 root.render(<Component value={2} />);
737 });
738 });
739
740 it('should not invoke new unsafe lifecycles (cWM/cWRP/cWU) if static gDSFP is present', async () => {
741 class Component extends React.Component {
742 state = {};
743 static getDerivedStateFromProps() {
744 return null;
745 }
746 UNSAFE_componentWillMount() {
747 throw Error('unexpected');
748 }
749 UNSAFE_componentWillReceiveProps() {
750 throw Error('unexpected');
751 }
752 UNSAFE_componentWillUpdate() {
753 throw Error('unexpected');
754 }
755 render() {
756 return null;
757 }
758 }
759
760 const root = ReactDOMClient.createRoot(document.createElement('div'));
761 await act(() => {
762 root.render(<Component value={1} />);
763 });
764 assertConsoleErrorDev([
765 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
766 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
767 ' UNSAFE_componentWillMount\n' +
768 ' UNSAFE_componentWillReceiveProps\n' +
769 ' UNSAFE_componentWillUpdate\n\n' +
770 'The above lifecycles should be removed. Learn more about this warning here:\n' +
771 'https://react.dev/link/unsafe-component-lifecycles\n' +
772 ' in Component (at **)',
773 ]);
774 await act(() => {
775 root.render(<Component value={2} />);
776 });
777 });
778
779 it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => {
780 class AllLegacyLifecycles extends React.Component {
781 state = {};
782 static getDerivedStateFromProps() {
783 return null;
784 }
785 componentWillMount() {}
786 UNSAFE_componentWillReceiveProps() {}
787 componentWillUpdate() {}
788 render() {
789 return null;
790 }
791 }
792
793 const root = ReactDOMClient.createRoot(document.createElement('div'));
794 await act(() => {
795 root.render(<AllLegacyLifecycles />);
796 });
797 assertConsoleErrorDev([
798 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
799 'AllLegacyLifecycles uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
800 ' componentWillMount\n' +
801 ' UNSAFE_componentWillReceiveProps\n' +
802 ' componentWillUpdate\n\n' +
803 'The above lifecycles should be removed. Learn more about this warning here:\n' +
804 'https://react.dev/link/unsafe-component-lifecycles\n' +
805 ' in AllLegacyLifecycles (at **)',
806 ]);
807 assertConsoleWarnDev([
808 'componentWillMount has been renamed, and is not recommended for use. ' +
809 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
810 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
811 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
812 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
813 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
814 'Please update the following components: AllLegacyLifecycles',
815 'componentWillUpdate has been renamed, and is not recommended for use. ' +
816 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
817 '* Move data fetching code or side effects to componentDidUpdate.\n' +
818 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' +
819 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
820 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
821 'Please update the following components: AllLegacyLifecycles',
822 ]);
823
824 class WillMount extends React.Component {
825 state = {};
826 static getDerivedStateFromProps() {
827 return null;
828 }
829 UNSAFE_componentWillMount() {}
830 render() {
831 return null;
832 }
833 }
834
835 await act(() => {
836 root.render(<WillMount />);
837 });
838 assertConsoleErrorDev([
839 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
840 'WillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
841 ' UNSAFE_componentWillMount\n\n' +
842 'The above lifecycles should be removed. Learn more about this warning here:\n' +
843 'https://react.dev/link/unsafe-component-lifecycles\n' +
844 ' in WillMount (at **)',
845 ]);
846
847 class WillMountAndUpdate extends React.Component {
848 state = {};
849 static getDerivedStateFromProps() {
850 return null;
851 }
852 componentWillMount() {}
853 UNSAFE_componentWillUpdate() {}
854 render() {
855 return null;
856 }
857 }
858
859 await act(() => {
860 root.render(<WillMountAndUpdate />);
861 });
862 assertConsoleErrorDev([
863 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
864 'WillMountAndUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
865 ' componentWillMount\n' +
866 ' UNSAFE_componentWillUpdate\n\n' +
867 'The above lifecycles should be removed. Learn more about this warning here:\n' +
868 'https://react.dev/link/unsafe-component-lifecycles\n' +
869 ' in WillMountAndUpdate (at **)',
870 ]);
871 assertConsoleWarnDev([
872 'componentWillMount has been renamed, and is not recommended for use. ' +
873 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
874 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
875 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
876 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
877 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
878 'Please update the following components: WillMountAndUpdate',
879 ]);
880
881 class WillReceiveProps extends React.Component {
882 state = {};
883 static getDerivedStateFromProps() {
884 return null;
885 }
886 componentWillReceiveProps() {}
887 render() {
888 return null;
889 }
890 }
891
892 await act(() => {
893 root.render(<WillReceiveProps />);
894 });
895 assertConsoleErrorDev([
896 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
897 'WillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
898 ' componentWillReceiveProps\n\n' +
899 'The above lifecycles should be removed. Learn more about this warning here:\n' +
900 'https://react.dev/link/unsafe-component-lifecycles\n' +
901 ' in WillReceiveProps (at **)',
902 ]);
903 assertConsoleWarnDev([
904 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
905 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
906 '* Move data fetching code or side effects to componentDidUpdate.\n' +
907 "* If you're updating state whenever props change, refactor your code to use " +
908 'memoization techniques or move it to static getDerivedStateFromProps. ' +
909 'Learn more at: https://react.dev/link/derived-state\n' +
910 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' +
911 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
912 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
913 'Please update the following components: WillReceiveProps',
914 ]);
915 });
916
917 it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => {
918 class AllLegacyLifecycles extends React.Component {
919 state = {};
920 getSnapshotBeforeUpdate() {}
921 componentWillMount() {}
922 UNSAFE_componentWillReceiveProps() {}
923 componentWillUpdate() {}
924 componentDidUpdate() {}
925 render() {
926 return null;
927 }
928 }
929
930 const root = ReactDOMClient.createRoot(document.createElement('div'));
931 await act(() => {
932 root.render(<AllLegacyLifecycles />);
933 });
934 assertConsoleErrorDev([
935 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
936 'AllLegacyLifecycles uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
937 ' componentWillMount\n' +
938 ' UNSAFE_componentWillReceiveProps\n' +
939 ' componentWillUpdate\n\n' +
940 'The above lifecycles should be removed. Learn more about this warning here:\n' +
941 'https://react.dev/link/unsafe-component-lifecycles\n' +
942 ' in AllLegacyLifecycles (at **)',
943 ]);
944 assertConsoleWarnDev([
945 'componentWillMount has been renamed, and is not recommended for use. ' +
946 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
947 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
948 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
949 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
950 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
951 'Please update the following components: AllLegacyLifecycles',
952 'componentWillUpdate has been renamed, and is not recommended for use. ' +
953 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
954 '* Move data fetching code or side effects to componentDidUpdate.\n' +
955 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' +
956 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
957 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
958 'Please update the following components: AllLegacyLifecycles',
959 ]);
960
961 class WillMount extends React.Component {
962 state = {};
963 getSnapshotBeforeUpdate() {}
964 UNSAFE_componentWillMount() {}
965 componentDidUpdate() {}
966 render() {
967 return null;
968 }
969 }
970
971 await act(() => {
972 root.render(<WillMount />);
973 });
974 assertConsoleErrorDev([
975 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
976 'WillMount uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
977 ' UNSAFE_componentWillMount\n\n' +
978 'The above lifecycles should be removed. Learn more about this warning here:\n' +
979 'https://react.dev/link/unsafe-component-lifecycles\n' +
980 ' in WillMount (at **)',
981 ]);
982
983 class WillMountAndUpdate extends React.Component {
984 state = {};
985 getSnapshotBeforeUpdate() {}
986 componentWillMount() {}
987 UNSAFE_componentWillUpdate() {}
988 componentDidUpdate() {}
989 render() {
990 return null;
991 }
992 }
993
994 await act(() => {
995 root.render(<WillMountAndUpdate />);
996 });
997 assertConsoleErrorDev([
998 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
999 'WillMountAndUpdate uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
1000 ' componentWillMount\n' +
1001 ' UNSAFE_componentWillUpdate\n\n' +
1002 'The above lifecycles should be removed. Learn more about this warning here:\n' +
1003 'https://react.dev/link/unsafe-component-lifecycles\n' +
1004 ' in WillMountAndUpdate (at **)',
1005 ]);
1006 assertConsoleWarnDev([
1007 'componentWillMount has been renamed, and is not recommended for use. ' +
1008 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
1009 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
1010 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
1011 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
1012 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
1013 'Please update the following components: WillMountAndUpdate',
1014 ]);
1015
1016 class WillReceiveProps extends React.Component {
1017 state = {};
1018 getSnapshotBeforeUpdate() {}
1019 componentWillReceiveProps() {}
1020 componentDidUpdate() {}
1021 render() {
1022 return null;
1023 }
1024 }
1025
1026 await act(() => {
1027 root.render(<WillReceiveProps />);
1028 });
1029 assertConsoleErrorDev([
1030 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
1031 'WillReceiveProps uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
1032 ' componentWillReceiveProps\n\n' +
1033 'The above lifecycles should be removed. Learn more about this warning here:\n' +
1034 'https://react.dev/link/unsafe-component-lifecycles\n' +
1035 ' in WillReceiveProps (at **)',
1036 ]);
1037 assertConsoleWarnDev([
1038 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
1039 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
1040 '* Move data fetching code or side effects to componentDidUpdate.\n' +
1041 "* If you're updating state whenever props change, refactor your code to use " +
1042 'memoization techniques or move it to static getDerivedStateFromProps. ' +
1043 'Learn more at: https://react.dev/link/derived-state\n' +
1044 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' +
1045 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
1046 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
1047 'Please update the following components: WillReceiveProps',
1048 ]);
1049 });
1050
1051 it('should warn if getDerivedStateFromProps returns undefined', async () => {
1052 class MyComponent extends React.Component {
1053 state = {};
1054 static getDerivedStateFromProps() {}
1055 render() {
1056 return null;
1057 }
1058 }
1059
1060 const root = ReactDOMClient.createRoot(document.createElement('div'));
1061 await act(() => {
1062 root.render(<MyComponent />);
1063 });
1064 assertConsoleErrorDev([
1065 'MyComponent.getDerivedStateFromProps(): A valid state object (or null) must ' +
1066 'be returned. You have returned undefined.\n' +
1067 ' in MyComponent (at **)',
1068 ]);
1069
1070 // De-duped
1071 await act(() => {
1072 root.render(<MyComponent />);
1073 });
1074 });
1075
1076 it('should warn if state is not initialized before getDerivedStateFromProps', async () => {
1077 class MyComponent extends React.Component {
1078 static getDerivedStateFromProps() {
1079 return null;
1080 }
1081 render() {
1082 return null;
1083 }
1084 }
1085
1086 const root = ReactDOMClient.createRoot(document.createElement('div'));
1087 await act(() => {
1088 root.render(<MyComponent />);
1089 });
1090 assertConsoleErrorDev([
1091 '`MyComponent` uses `getDerivedStateFromProps` but its initial state is ' +
1092 'undefined. This is not recommended. Instead, define the initial state by ' +
1093 'assigning an object to `this.state` in the constructor of `MyComponent`. ' +
1094 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
1095 ' in MyComponent (at **)',
1096 ]);
1097
1098 // De-duped
1099 await act(() => {
1100 root.render(<MyComponent />);
1101 });
1102 });
1103
1104 it('should invoke both deprecated and new lifecycles if both are present', async () => {
1105 const log = [];
1106
1107 class MyComponent extends React.Component {
1108 componentWillMount() {
1109 log.push('componentWillMount');
1110 }
1111 componentWillReceiveProps() {
1112 log.push('componentWillReceiveProps');
1113 }
1114 componentWillUpdate() {
1115 log.push('componentWillUpdate');
1116 }
1117 UNSAFE_componentWillMount() {
1118 log.push('UNSAFE_componentWillMount');
1119 }
1120 UNSAFE_componentWillReceiveProps() {
1121 log.push('UNSAFE_componentWillReceiveProps');
1122 }
1123 UNSAFE_componentWillUpdate() {
1124 log.push('UNSAFE_componentWillUpdate');
1125 }
1126 render() {
1127 return null;
1128 }
1129 }
1130
1131 const root = ReactDOMClient.createRoot(document.createElement('div'));
1132 await act(() => {
1133 root.render(<MyComponent foo="bar" />);
1134 });
1135 assertConsoleWarnDev([
1136 'componentWillMount has been renamed, and is not recommended for use. ' +
1137 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
1138 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
1139 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
1140 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
1141 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
1142 'Please update the following components: MyComponent',
1143 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
1144 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
1145 '* Move data fetching code or side effects to componentDidUpdate.\n' +
1146 "* If you're updating state whenever props change, refactor your code to use " +
1147 'memoization techniques or move it to static getDerivedStateFromProps. ' +
1148 'Learn more at: https://react.dev/link/derived-state\n' +
1149 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' +
1150 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
1151 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
1152 'Please update the following components: MyComponent',
1153 'componentWillUpdate has been renamed, and is not recommended for use. ' +
1154 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
1155 '* Move data fetching code or side effects to componentDidUpdate.\n' +
1156 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' +
1157 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' +
1158 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' +
1159 'Please update the following components: MyComponent',
1160 ]);
1161 expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
1162
1163 log.length = 0;
1164
1165 await act(() => {
1166 root.render(<MyComponent foo="baz" />);
1167 });
1168 expect(log).toEqual([
1169 'componentWillReceiveProps',
1170 'UNSAFE_componentWillReceiveProps',
1171 'componentWillUpdate',
1172 'UNSAFE_componentWillUpdate',
1173 ]);
1174 });
1175
1176 it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', async () => {
1177 const divRef = React.createRef();
1178 let childInstance;
1179
1180 class Child extends React.Component {
1181 state = {local: 0};
1182 static getDerivedStateFromProps(nextProps, prevState) {
1183 return {...prevState, remote: nextProps.remote};
1184 }
1185 updateState = () => {
1186 this.setState(state => ({local: state.local + 1}));
1187 this.props.onChange(this.state.remote + 1);
1188 };
1189 render() {
1190 childInstance = this;
1191 return (
1192 <div
1193 onClick={this.updateState}
1194 ref={
1195 divRef
1196 }>{`remote:${this.state.remote}, local:${this.state.local}`}</div>
1197 );
1198 }
1199 }
1200
1201 class Parent extends React.Component {
1202 state = {value: 0};
1203 handleChange = value => {
1204 this.setState({value});
1205 };
1206 render() {
1207 return <Child remote={this.state.value} onChange={this.handleChange} />;
1208 }
1209 }
1210
1211 const container = document.createElement('div');
1212 document.body.appendChild(container);
1213 const root = ReactDOMClient.createRoot(container);
1214
1215 await act(() => {
1216 root.render(<Parent />);
1217 });
1218 expect(divRef.current.textContent).toBe('remote:0, local:0');
1219
1220 // Trigger setState() calls
1221 await act(() => {
1222 childInstance.updateState();
1223 });
1224 expect(divRef.current.textContent).toBe('remote:1, local:1');
1225
1226 // Trigger batched setState() calls
1227 await act(() => {
1228 divRef.current.click();
1229 });
1230 expect(divRef.current.textContent).toBe('remote:2, local:2');
1231 document.body.removeChild(container);
1232 });
1233
1234 it('should pass the return value from getSnapshotBeforeUpdate to componentDidUpdate', async () => {
1235 const log = [];
1236
1237 class MyComponent extends React.Component {
1238 state = {
1239 value: 0,
1240 };
1241 static getDerivedStateFromProps(nextProps, prevState) {
1242 return {
1243 value: prevState.value + 1,
1244 };
1245 }
1246 getSnapshotBeforeUpdate(prevProps, prevState) {
1247 log.push(
1248 `getSnapshotBeforeUpdate() prevProps:${prevProps.value} prevState:${prevState.value}`,
1249 );
1250 return 'abc';
1251 }
1252 componentDidUpdate(prevProps, prevState, snapshot) {
1253 log.push(
1254 `componentDidUpdate() prevProps:${prevProps.value} prevState:${prevState.value} snapshot:${snapshot}`,
1255 );
1256 }
1257 render() {
1258 log.push('render');
1259 return null;
1260 }
1261 }
1262
1263 const root = ReactDOMClient.createRoot(document.createElement('div'));
1264 await act(() => {
1265 root.render(
1266 <div>
1267 <MyComponent value="foo" />
1268 </div>,
1269 );
1270 });
1271 expect(log).toEqual(['render']);
1272 log.length = 0;
1273
1274 await act(() => {
1275 root.render(
1276 <div>
1277 <MyComponent value="bar" />
1278 </div>,
1279 );
1280 });
1281 expect(log).toEqual([
1282 'render',
1283 'getSnapshotBeforeUpdate() prevProps:foo prevState:1',
1284 'componentDidUpdate() prevProps:foo prevState:1 snapshot:abc',
1285 ]);
1286 log.length = 0;
1287
1288 await act(() => {
1289 root.render(
1290 <div>
1291 <MyComponent value="baz" />
1292 </div>,
1293 );
1294 });
1295 expect(log).toEqual([
1296 'render',
1297 'getSnapshotBeforeUpdate() prevProps:bar prevState:2',
1298 'componentDidUpdate() prevProps:bar prevState:2 snapshot:abc',
1299 ]);
1300 log.length = 0;
1301
1302 await act(() => {
1303 root.render(<div />);
1304 });
1305 expect(log).toEqual([]);
1306 });
1307
1308 it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', async () => {
1309 const divRef = React.createRef();
1310 class SimpleComponent extends React.Component {
1311 constructor(props) {
1312 super(props);
1313 this.state = {
1314 value: props.value,
1315 };
1316 }
1317
1318 static getDerivedStateFromProps(nextProps, prevState) {
1319 if (nextProps.value === prevState.value) {
1320 return null;
1321 }
1322 return {value: nextProps.value};
1323 }
1324
1325 shouldComponentUpdate(nextProps, nextState) {
1326 return nextState.value !== this.state.value;
1327 }
1328
1329 render() {
1330 return <div ref={divRef}>value: {this.state.value}</div>;
1331 }
1332 }
1333
1334 const root = ReactDOMClient.createRoot(document.createElement('div'));
1335 await act(() => {
1336 root.render(<SimpleComponent value="initial" />);
1337 });
1338 expect(divRef.current.textContent).toBe('value: initial');
1339 await act(() => {
1340 root.render(<SimpleComponent value="updated" />);
1341 });
1342 expect(divRef.current.textContent).toBe('value: updated');
1343 });
1344
1345 it('should call getSnapshotBeforeUpdate before mutations are committed', async () => {
1346 const log = [];
1347
1348 class MyComponent extends React.Component {
1349 divRef = React.createRef();
1350 getSnapshotBeforeUpdate(prevProps, prevState) {
1351 log.push('getSnapshotBeforeUpdate');
1352 expect(this.divRef.current.textContent).toBe(
1353 `value:${prevProps.value}`,
1354 );
1355 return 'foobar';
1356 }
1357 componentDidUpdate(prevProps, prevState, snapshot) {
1358 log.push('componentDidUpdate');
1359 expect(this.divRef.current.textContent).toBe(
1360 `value:${this.props.value}`,
1361 );
1362 expect(snapshot).toBe('foobar');
1363 }
1364 render() {
1365 log.push('render');
1366 return <div ref={this.divRef}>{`value:${this.props.value}`}</div>;
1367 }
1368 }
1369
1370 const root = ReactDOMClient.createRoot(document.createElement('div'));
1371 await act(() => {
1372 root.render(<MyComponent value="foo" />);
1373 });
1374 expect(log).toEqual(['render']);
1375 log.length = 0;
1376
1377 await act(() => {
1378 root.render(<MyComponent value="bar" />);
1379 });
1380 expect(log).toEqual([
1381 'render',
1382 'getSnapshotBeforeUpdate',
1383 'componentDidUpdate',
1384 ]);
1385 log.length = 0;
1386 });
1387
1388 it('should warn if getSnapshotBeforeUpdate returns undefined', async () => {
1389 class MyComponent extends React.Component {
1390 getSnapshotBeforeUpdate() {}
1391 componentDidUpdate() {}
1392 render() {
1393 return null;
1394 }
1395 }
1396
1397 const root = ReactDOMClient.createRoot(document.createElement('div'));
1398 await act(() => {
1399 root.render(<MyComponent value="foo" />);
1400 });
1401
1402 await act(() => {
1403 root.render(<MyComponent value="bar" />);
1404 });
1405 assertConsoleErrorDev([
1406 'MyComponent.getSnapshotBeforeUpdate(): A snapshot value (or null) must ' +
1407 'be returned. You have returned undefined.\n' +
1408 ' in MyComponent (at **)',
1409 ]);
1410
1411 // De-duped
1412 await act(() => {
1413 root.render(<MyComponent value="baz" />);
1414 });
1415 });
1416
1417 it('should warn if getSnapshotBeforeUpdate is defined with no componentDidUpdate', async () => {
1418 class MyComponent extends React.Component {
1419 getSnapshotBeforeUpdate() {
1420 return null;
1421 }
1422 render() {
1423 return null;
1424 }
1425 }
1426
1427 const root = ReactDOMClient.createRoot(document.createElement('div'));
1428 await act(() => {
1429 root.render(<MyComponent />);
1430 });
1431 assertConsoleErrorDev([
1432 'MyComponent: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' +
1433 'This component defines getSnapshotBeforeUpdate() only.\n' +
1434 ' in MyComponent (at **)',
1435 ]);
1436
1437 // De-duped
1438 await act(() => {
1439 root.render(<MyComponent />);
1440 });
1441 });
1442
1443 it('warns about deprecated unsafe lifecycles', async () => {
1444 class MyComponent extends React.Component {
1445 componentWillMount() {}
1446 componentWillReceiveProps() {}
1447 componentWillUpdate() {}
1448 render() {
1449 return null;
1450 }
1451 }
1452
1453 const root = ReactDOMClient.createRoot(document.createElement('div'));
1454
1455 await act(() => {
1456 root.render(<MyComponent x={1} />);
1457 });
1458 assertConsoleWarnDev([
1459 `componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details.
1460
1461 * Move code with side effects to componentDidMount, and set initial state in the constructor.
1462 * Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run \`npx react-codemod rename-unsafe-lifecycles\` in your project source folder.
1463
1464 Please update the following components: MyComponent`,
1465 `componentWillReceiveProps has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details.
1466
1467 * Move data fetching code or side effects to componentDidUpdate.
1468 * If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state
1469 * Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run \`npx react-codemod rename-unsafe-lifecycles\` in your project source folder.
1470
1471 Please update the following components: MyComponent`,
1472 `componentWillUpdate has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details.
1473
1474 * Move data fetching code or side effects to componentDidUpdate.
1475 * Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run \`npx react-codemod rename-unsafe-lifecycles\` in your project source folder.
1476
1477 Please update the following components: MyComponent`,
1478 ]);
1479
1480 // Dedupe check (update and instantiate new)
1481 await act(() => {
1482 root.render(<MyComponent x={2} />);
1483 });
1484 await act(() => {
1485 root.render(<MyComponent key="new" x={1} />);
1486 });
1487 });
1488
1489 describe('react-lifecycles-compat', () => {
1490 const {polyfill} = require('react-lifecycles-compat');
1491
1492 it('should not warn for components with polyfilled getDerivedStateFromProps', async () => {
1493 class PolyfilledComponent extends React.Component {
1494 state = {};
1495 static getDerivedStateFromProps() {
1496 return null;
1497 }
1498 render() {
1499 return null;
1500 }
1501 }
1502
1503 polyfill(PolyfilledComponent);
1504
1505 const root = ReactDOMClient.createRoot(document.createElement('div'));
1506 await act(() => {
1507 root.render(
1508 <React.StrictMode>
1509 <PolyfilledComponent />
1510 </React.StrictMode>,
1511 );
1512 });
1513 });
1514
1515 it('should not warn for components with polyfilled getSnapshotBeforeUpdate', async () => {
1516 class PolyfilledComponent extends React.Component {
1517 getSnapshotBeforeUpdate() {
1518 return null;
1519 }
1520 componentDidUpdate() {}
1521 render() {
1522 return null;
1523 }
1524 }
1525
1526 polyfill(PolyfilledComponent);
1527
1528 const root = ReactDOMClient.createRoot(document.createElement('div'));
1529 await act(() => {
1530 root.render(
1531 <React.StrictMode>
1532 <PolyfilledComponent />
1533 </React.StrictMode>,
1534 );
1535 });
1536 });
1537 });
1538 });