main
js 890 lines 29.4 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 let assertConsoleErrorDev;
14 let assertConsoleWarnDev;
15
16 let PropTypes;
17 let React;
18 let ReactDOMClient;
19 let createReactClass;
20
21 describe('create-react-class-integration', () => {
22 beforeEach(() => {
23 jest.resetModules();
24 ({
25 act,
26 assertConsoleErrorDev,
27 assertConsoleWarnDev,
28 } = require('internal-test-utils'));
29 PropTypes = require('prop-types');
30 React = require('react');
31 ReactDOMClient = require('react-dom/client');
32 createReactClass = require('create-react-class/factory')(
33 React.Component,
34 React.isValidElement,
35 new React.Component().updater,
36 );
37 });
38
39 it('should throw when `render` is not specified', () => {
40 expect(function () {
41 createReactClass({});
42 }).toThrow('Class specification must implement a `render` method.');
43 });
44
45 it('should copy prop types onto the Constructor', () => {
46 const propValidator = jest.fn();
47 const TestComponent = createReactClass({
48 propTypes: {
49 value: propValidator,
50 },
51 render: function () {
52 return <div />;
53 },
54 });
55
56 expect(TestComponent.propTypes).toBeDefined();
57 expect(TestComponent.propTypes.value).toBe(propValidator);
58 });
59
60 it('should warn on invalid prop types', () => {
61 createReactClass({
62 displayName: 'Component',
63 propTypes: {
64 prop: null,
65 },
66 render: function () {
67 return <span>{this.props.prop}</span>;
68 },
69 });
70 assertConsoleErrorDev([
71 'Warning: Component: prop type `prop` is invalid; ' +
72 'it must be a function, usually from React.PropTypes.',
73 ]);
74 });
75
76 it('should warn on invalid context types', () => {
77 createReactClass({
78 displayName: 'Component',
79 contextTypes: {
80 prop: null,
81 },
82 render: function () {
83 return <span>{this.props.prop}</span>;
84 },
85 });
86 assertConsoleErrorDev([
87 'Warning: Component: context type `prop` is invalid; ' +
88 'it must be a function, usually from React.PropTypes.',
89 ]);
90 });
91
92 it('should throw on invalid child context types', () => {
93 createReactClass({
94 displayName: 'Component',
95 childContextTypes: {
96 prop: null,
97 },
98 render: function () {
99 return <span>{this.props.prop}</span>;
100 },
101 });
102 assertConsoleErrorDev([
103 'Warning: Component: child context type `prop` is invalid; it must be a function, usually from React.PropTypes.',
104 ]);
105 });
106
107 it('should warn when misspelling shouldComponentUpdate', () => {
108 createReactClass({
109 componentShouldUpdate: function () {
110 return false;
111 },
112 render: function () {
113 return <div />;
114 },
115 });
116 assertConsoleErrorDev([
117 'Warning: A component has a method called componentShouldUpdate(). Did you ' +
118 'mean shouldComponentUpdate()? The name is phrased as a question ' +
119 'because the function is expected to return a value.',
120 ]);
121
122 createReactClass({
123 displayName: 'NamedComponent',
124 componentShouldUpdate: function () {
125 return false;
126 },
127 render: function () {
128 return <div />;
129 },
130 });
131 assertConsoleErrorDev([
132 'Warning: NamedComponent has a method called componentShouldUpdate(). Did you ' +
133 'mean shouldComponentUpdate()? The name is phrased as a question ' +
134 'because the function is expected to return a value.',
135 ]);
136 });
137
138 it('should warn when misspelling componentWillReceiveProps', () => {
139 createReactClass({
140 componentWillRecieveProps: function () {
141 return false;
142 },
143 render: function () {
144 return <div />;
145 },
146 });
147 assertConsoleErrorDev([
148 'Warning: A component has a method called componentWillRecieveProps(). Did you ' +
149 'mean componentWillReceiveProps()?',
150 ]);
151 });
152
153 it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
154 createReactClass({
155 UNSAFE_componentWillRecieveProps: function () {
156 return false;
157 },
158 render: function () {
159 return <div />;
160 },
161 });
162 assertConsoleErrorDev([
163 'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' +
164 'Did you mean UNSAFE_componentWillReceiveProps()?',
165 ]);
166 });
167
168 it('should throw if a reserved property is in statics', () => {
169 expect(function () {
170 createReactClass({
171 statics: {
172 getDefaultProps: function () {
173 return {
174 foo: 0,
175 };
176 },
177 },
178
179 render: function () {
180 return <span />;
181 },
182 });
183 }).toThrow(
184 'ReactClass: You are attempting to define a reserved property, ' +
185 '`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' +
186 'it as an instance property instead; it will still be accessible on ' +
187 'the constructor.',
188 );
189 });
190
191 // TODO: Consider actually moving these to statics or drop this unit test.
192 // eslint-disable-next-line jest/no-disabled-tests
193 it.skip('should warn when using deprecated non-static spec keys', () => {
194 createReactClass({
195 mixins: [{}],
196 propTypes: {
197 foo: PropTypes.string,
198 },
199 contextTypes: {
200 foo: PropTypes.string,
201 },
202 childContextTypes: {
203 foo: PropTypes.string,
204 },
205 render: function () {
206 return <div />;
207 },
208 });
209 assertConsoleErrorDev([
210 '`mixins` is now a static property and should ' +
211 'be defined inside "statics".',
212 '`propTypes` is now a static property and should ' +
213 'be defined inside "statics".',
214 '`contextTypes` is now a static property and ' +
215 'should be defined inside "statics".',
216 '`childContextTypes` is now a static property and ' +
217 'should be defined inside "statics".',
218 ]);
219 });
220
221 it('should support statics', async () => {
222 const Component = createReactClass({
223 statics: {
224 abc: 'def',
225 def: 0,
226 ghi: null,
227 jkl: 'mno',
228 pqr: function () {
229 return this;
230 },
231 },
232
233 render: function () {
234 return <span />;
235 },
236 });
237 const container = document.createElement('div');
238 const root = ReactDOMClient.createRoot(container);
239 let instance;
240 await act(() => {
241 root.render(<Component ref={current => (instance = current)} />);
242 });
243
244 expect(instance.constructor.abc).toBe('def');
245 expect(Component.abc).toBe('def');
246 expect(instance.constructor.def).toBe(0);
247 expect(Component.def).toBe(0);
248 expect(instance.constructor.ghi).toBe(null);
249 expect(Component.ghi).toBe(null);
250 expect(instance.constructor.jkl).toBe('mno');
251 expect(Component.jkl).toBe('mno');
252 expect(instance.constructor.pqr()).toBe(Component);
253 expect(Component.pqr()).toBe(Component);
254 });
255
256 it('should work with object getInitialState() return values', async () => {
257 const Component = createReactClass({
258 getInitialState: function () {
259 return {
260 occupation: 'clown',
261 };
262 },
263 render: function () {
264 return <span />;
265 },
266 });
267 const container = document.createElement('div');
268 const root = ReactDOMClient.createRoot(container);
269 let instance;
270 await act(() => {
271 root.render(<Component ref={current => (instance = current)} />);
272 });
273
274 expect(instance.state.occupation).toEqual('clown');
275 });
276
277 it('should work with getDerivedStateFromProps() return values', async () => {
278 const Component = createReactClass({
279 getInitialState() {
280 return {};
281 },
282 render: function () {
283 return <span />;
284 },
285 });
286 Component.getDerivedStateFromProps = () => {
287 return {occupation: 'clown'};
288 };
289 let instance;
290 const container = document.createElement('div');
291 const root = ReactDOMClient.createRoot(container);
292 await act(() => {
293 root.render(<Component ref={current => (instance = current)} />);
294 });
295 expect(instance.state.occupation).toEqual('clown');
296 });
297
298 // @gate !disableLegacyContext
299 it('renders based on context getInitialState', async () => {
300 const Foo = createReactClass({
301 contextTypes: {
302 className: PropTypes.string,
303 },
304 getInitialState() {
305 return {className: this.context.className};
306 },
307 render() {
308 return <span className={this.state.className} />;
309 },
310 });
311
312 const Outer = createReactClass({
313 childContextTypes: {
314 className: PropTypes.string,
315 },
316 getChildContext() {
317 return {className: 'foo'};
318 },
319 render() {
320 return <Foo />;
321 },
322 });
323
324 const container = document.createElement('div');
325 const root = ReactDOMClient.createRoot(container);
326 await act(() => {
327 root.render(<Outer />);
328 });
329 assertConsoleErrorDev([
330 'Component uses the legacy childContextTypes API which will soon be removed. ' +
331 'Use React.createContext() instead. (https://react.dev/link/legacy-context)',
332 'Component uses the legacy contextTypes API which will soon be removed. ' +
333 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)' +
334 '\n in ReactClassComponent (at **)',
335 ]);
336 expect(container.firstChild.className).toBe('foo');
337 });
338
339 it('should throw with non-object getInitialState() return values', async () => {
340 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
341 for (const state of [['an array'], 'a string', 1234]) {
342 const Component = createReactClass({
343 getInitialState: function () {
344 return state;
345 },
346 render: function () {
347 return <span />;
348 },
349 });
350 const container = document.createElement('div');
351 const root = ReactDOMClient.createRoot(container);
352 await expect(
353 act(() => {
354 root.render(<Component />);
355 }),
356 ).rejects.toThrow(
357 'Component.getInitialState(): must return an object or null',
358 );
359 }
360 });
361
362 it('should work with a null getInitialState() return value', async () => {
363 const Component = createReactClass({
364 getInitialState: function () {
365 return null;
366 },
367 render: function () {
368 return <span />;
369 },
370 });
371 const container = document.createElement('div');
372 const root = ReactDOMClient.createRoot(container);
373 await expect(
374 act(() => {
375 root.render(<Component />);
376 }),
377 ).resolves.not.toThrow();
378 });
379
380 it('should throw when using legacy factories', () => {
381 const Component = createReactClass({
382 render() {
383 return <div />;
384 },
385 });
386
387 expect(() => Component()).toThrow();
388 assertConsoleErrorDev([
389 'Warning: Something is calling a React component directly. Use a ' +
390 'factory or JSX instead. See: https://fb.me/react-legacyfactory',
391 ]);
392 });
393
394 it('replaceState and callback works', async () => {
395 const ops = [];
396 const Component = createReactClass({
397 getInitialState() {
398 return {step: 0};
399 },
400 render() {
401 ops.push('Render: ' + this.state.step);
402 return <div />;
403 },
404 });
405
406 const container = document.createElement('div');
407 const root = ReactDOMClient.createRoot(container);
408 let instance;
409 await act(() => {
410 root.render(<Component ref={current => (instance = current)} />);
411 });
412
413 await act(() => {
414 instance.replaceState({step: 1}, () => {
415 ops.push('Callback: ' + instance.state.step);
416 });
417 });
418
419 expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']);
420 });
421
422 it('getDerivedStateFromProps updates state when props change', async () => {
423 const Component = createReactClass({
424 getInitialState() {
425 return {
426 count: 1,
427 };
428 },
429 render() {
430 return <div>count:{this.state.count}</div>;
431 },
432 });
433 Component.getDerivedStateFromProps = (nextProps, prevState) => ({
434 count: prevState.count + nextProps.incrementBy,
435 });
436
437 const container = document.createElement('div');
438 const root = ReactDOMClient.createRoot(container);
439 await act(() => {
440 root.render(
441 <div>
442 <Component incrementBy={0} />
443 </div>,
444 );
445 });
446 expect(container.firstChild.textContent).toEqual('count:1');
447 await act(() => {
448 root.render(
449 <div>
450 <Component incrementBy={2} />
451 </div>,
452 );
453 });
454 expect(container.firstChild.textContent).toEqual('count:3');
455 });
456
457 it('should support the new static getDerivedStateFromProps method', async () => {
458 let instance;
459 const Component = createReactClass({
460 statics: {
461 getDerivedStateFromProps: function () {
462 return {foo: 'bar'};
463 },
464 },
465
466 getInitialState() {
467 return {};
468 },
469
470 render: function () {
471 instance = this;
472 return null;
473 },
474 });
475 const root = ReactDOMClient.createRoot(document.createElement('div'));
476 await act(() => {
477 root.render(<Component />);
478 });
479 expect(instance.state.foo).toBe('bar');
480 });
481
482 it('warns if getDerivedStateFromProps is not static', async () => {
483 const Foo = createReactClass({
484 displayName: 'Foo',
485 getDerivedStateFromProps() {
486 return {};
487 },
488 render() {
489 return <div />;
490 },
491 });
492 const root = ReactDOMClient.createRoot(document.createElement('div'));
493 await act(() => {
494 root.render(<Foo foo="foo" />);
495 });
496 assertConsoleErrorDev([
497 'Foo: getDerivedStateFromProps() is defined as an instance method ' +
498 'and will be ignored. Instead, declare it as a static method.\n' +
499 ' in Foo (at **)',
500 ]);
501 });
502
503 it('warns if getDerivedStateFromError is not static', async () => {
504 const Foo = createReactClass({
505 displayName: 'Foo',
506 getDerivedStateFromError() {
507 return {};
508 },
509 render() {
510 return <div />;
511 },
512 });
513 const root = ReactDOMClient.createRoot(document.createElement('div'));
514 await act(() => {
515 root.render(<Foo foo="foo" />);
516 });
517 assertConsoleErrorDev([
518 'Foo: getDerivedStateFromError() is defined as an instance method ' +
519 'and will be ignored. Instead, declare it as a static method.\n' +
520 ' in Foo (at **)',
521 ]);
522 });
523
524 it('warns if getSnapshotBeforeUpdate is static', async () => {
525 const Foo = createReactClass({
526 displayName: 'Foo',
527 statics: {
528 getSnapshotBeforeUpdate: function () {
529 return null;
530 },
531 },
532 render() {
533 return <div />;
534 },
535 });
536 const root = ReactDOMClient.createRoot(document.createElement('div'));
537 await act(() => {
538 root.render(<Foo foo="foo" />);
539 });
540 assertConsoleErrorDev([
541 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
542 'and will be ignored. Instead, declare it as an instance method.\n' +
543 ' in Foo (at **)',
544 ]);
545 });
546
547 it('should warn if state is not properly initialized before getDerivedStateFromProps', async () => {
548 const Component = createReactClass({
549 displayName: 'Component',
550 statics: {
551 getDerivedStateFromProps: function () {
552 return null;
553 },
554 },
555 render: function () {
556 return null;
557 },
558 });
559 const root = ReactDOMClient.createRoot(document.createElement('div'));
560 await act(() => {
561 root.render(<Component />);
562 });
563 assertConsoleErrorDev([
564 '`Component` uses `getDerivedStateFromProps` but its initial state is ' +
565 'null. This is not recommended. Instead, define the initial state by ' +
566 'assigning an object to `this.state` in the constructor of `Component`. ' +
567 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
568 ' in Component (at **)',
569 ]);
570 });
571
572 it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => {
573 const Component = createReactClass({
574 statics: {
575 getDerivedStateFromProps: function () {
576 return null;
577 },
578 },
579 componentWillMount: function () {
580 throw Error('unexpected');
581 },
582 componentWillReceiveProps: function () {
583 throw Error('unexpected');
584 },
585 componentWillUpdate: function () {
586 throw Error('unexpected');
587 },
588 getInitialState: function () {
589 return {};
590 },
591 render: function () {
592 return null;
593 },
594 });
595 Component.displayName = 'Component';
596
597 const root = ReactDOMClient.createRoot(document.createElement('div'));
598 await act(() => {
599 root.render(<Component />);
600 });
601 assertConsoleErrorDev([
602 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
603 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
604 ' componentWillMount\n' +
605 ' componentWillReceiveProps\n' +
606 ' componentWillUpdate\n\n' +
607 'The above lifecycles should be removed. Learn more about this warning here:\n' +
608 'https://react.dev/link/unsafe-component-lifecycles\n' +
609 ' in Component (at **)',
610 ]);
611 assertConsoleWarnDev([
612 'componentWillMount has been renamed, and is not recommended for use. ' +
613 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
614 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
615 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' +
616 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
617 'To rename all deprecated lifecycles to their new names, you can run ' +
618 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
619 '\nPlease update the following components: Component',
620 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
621 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
622 '* Move data fetching code or side effects to componentDidUpdate.\n' +
623 "* If you're updating state whenever props change, refactor your " +
624 'code to use memoization techniques or move it to ' +
625 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' +
626 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' +
627 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
628 'To rename all deprecated lifecycles to their new names, you can run ' +
629 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
630 '\nPlease update the following components: Component',
631 'componentWillUpdate has been renamed, and is not recommended for use. ' +
632 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
633 '* Move data fetching code or side effects to componentDidUpdate.\n' +
634 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' +
635 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
636 'To rename all deprecated lifecycles to their new names, you can run ' +
637 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
638 '\nPlease update the following components: Component',
639 ]);
640 await act(() => {
641 root.render(<Component foo={1} />);
642 });
643 });
644
645 it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => {
646 const Component = createReactClass({
647 getSnapshotBeforeUpdate: function () {
648 return null;
649 },
650 componentWillMount: function () {
651 throw Error('unexpected');
652 },
653 componentWillReceiveProps: function () {
654 throw Error('unexpected');
655 },
656 componentWillUpdate: function () {
657 throw Error('unexpected');
658 },
659 componentDidUpdate: function () {},
660 render: function () {
661 return null;
662 },
663 });
664 Component.displayName = 'Component';
665
666 const root = ReactDOMClient.createRoot(document.createElement('div'));
667 await act(() => {
668 root.render(<Component />);
669 });
670 assertConsoleErrorDev([
671 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
672 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
673 ' componentWillMount\n' +
674 ' componentWillReceiveProps\n' +
675 ' componentWillUpdate\n\n' +
676 'The above lifecycles should be removed. Learn more about this warning here:\n' +
677 'https://react.dev/link/unsafe-component-lifecycles\n' +
678 ' in Component (at **)',
679 ]);
680 assertConsoleWarnDev([
681 'componentWillMount has been renamed, and is not recommended for use. ' +
682 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
683 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
684 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' +
685 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
686 'To rename all deprecated lifecycles to their new names, you can run ' +
687 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
688 '\nPlease update the following components: Component',
689 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
690 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
691 '* Move data fetching code or side effects to componentDidUpdate.\n' +
692 "* If you're updating state whenever props change, refactor your " +
693 'code to use memoization techniques or move it to ' +
694 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' +
695 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' +
696 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
697 'To rename all deprecated lifecycles to their new names, you can run ' +
698 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
699 '\nPlease update the following components: Component',
700 'componentWillUpdate has been renamed, and is not recommended for use. ' +
701 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
702 '* Move data fetching code or side effects to componentDidUpdate.\n' +
703 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' +
704 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
705 'To rename all deprecated lifecycles to their new names, you can run ' +
706 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
707 '\nPlease update the following components: Component',
708 ]);
709 await act(() => {
710 const root2 = ReactDOMClient.createRoot(document.createElement('div'));
711 root2.render(<Component foo={1} />);
712 });
713 });
714
715 it('should invoke both deprecated and new lifecycles if both are present', async () => {
716 const log = [];
717
718 const Component = createReactClass({
719 mixins: [
720 {
721 componentWillMount: function () {
722 log.push('componentWillMount');
723 },
724 componentWillReceiveProps: function () {
725 log.push('componentWillReceiveProps');
726 },
727 componentWillUpdate: function () {
728 log.push('componentWillUpdate');
729 },
730 },
731 ],
732 UNSAFE_componentWillMount: function () {
733 log.push('UNSAFE_componentWillMount');
734 },
735 UNSAFE_componentWillReceiveProps: function () {
736 log.push('UNSAFE_componentWillReceiveProps');
737 },
738 UNSAFE_componentWillUpdate: function () {
739 log.push('UNSAFE_componentWillUpdate');
740 },
741 render: function () {
742 return null;
743 },
744 });
745
746 const root = ReactDOMClient.createRoot(document.createElement('div'));
747
748 await act(() => {
749 root.render(<Component foo="bar" />);
750 });
751 assertConsoleWarnDev([
752 'componentWillMount has been renamed, and is not recommended for use. ' +
753 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
754 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
755 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' +
756 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
757 'To rename all deprecated lifecycles to their new names, you can run ' +
758 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
759 '\nPlease update the following components: Component',
760 'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
761 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
762 '* Move data fetching code or side effects to componentDidUpdate.\n' +
763 "* If you're updating state whenever props change, refactor your " +
764 'code to use memoization techniques or move it to ' +
765 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' +
766 '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' +
767 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
768 'To rename all deprecated lifecycles to their new names, you can run ' +
769 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
770 '\nPlease update the following components: Component',
771 'componentWillUpdate has been renamed, and is not recommended for use. ' +
772 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
773 '* Move data fetching code or side effects to componentDidUpdate.\n' +
774 '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' +
775 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
776 'To rename all deprecated lifecycles to their new names, you can run ' +
777 '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
778 '\nPlease update the following components: Component',
779 ]);
780 expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
781
782 log.length = 0;
783
784 await act(() => {
785 root.render(<Component foo="baz" />);
786 });
787 expect(log).toEqual([
788 'componentWillReceiveProps',
789 'UNSAFE_componentWillReceiveProps',
790 'componentWillUpdate',
791 'UNSAFE_componentWillUpdate',
792 ]);
793 });
794
795 it('isMounted works', async () => {
796 const ops = [];
797 let instance;
798 const Component = createReactClass({
799 displayName: 'MyComponent',
800 mixins: [
801 {
802 UNSAFE_componentWillMount() {
803 this.log('mixin.componentWillMount');
804 },
805 componentDidMount() {
806 this.log('mixin.componentDidMount');
807 },
808 UNSAFE_componentWillUpdate() {
809 this.log('mixin.componentWillUpdate');
810 },
811 componentDidUpdate() {
812 this.log('mixin.componentDidUpdate');
813 },
814 componentWillUnmount() {
815 this.log('mixin.componentWillUnmount');
816 },
817 },
818 ],
819 log(name) {
820 ops.push(`${name}: ${this.isMounted()}`);
821 },
822 getInitialState() {
823 this.log('getInitialState');
824 return {};
825 },
826 UNSAFE_componentWillMount() {
827 this.log('componentWillMount');
828 },
829 componentDidMount() {
830 this.log('componentDidMount');
831 },
832 UNSAFE_componentWillUpdate() {
833 this.log('componentWillUpdate');
834 },
835 componentDidUpdate() {
836 this.log('componentDidUpdate');
837 },
838 componentWillUnmount() {
839 this.log('componentWillUnmount');
840 },
841 render() {
842 instance = this;
843 this.log('render');
844 return <div />;
845 },
846 });
847
848 const root = ReactDOMClient.createRoot(document.createElement('div'));
849
850 await act(() => {
851 root.render(<Component />);
852 });
853 assertConsoleErrorDev(
854 [
855 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' +
856 'clean up subscriptions and pending requests in componentWillUnmount ' +
857 'to prevent memory leaks.\n' +
858 ' in MyComponent (at **)',
859 ],
860 // This now has a component stack even though it's part of a third-party library.
861 );
862
863 // Dedupe
864
865 await act(() => {
866 root.render(<Component />);
867 });
868
869 await act(() => {
870 root.unmount();
871 });
872 instance.log('after unmount');
873 expect(ops).toEqual([
874 'getInitialState: false',
875 'mixin.componentWillMount: false',
876 'componentWillMount: false',
877 'render: false',
878 'mixin.componentDidMount: true',
879 'componentDidMount: true',
880 'mixin.componentWillUpdate: true',
881 'componentWillUpdate: true',
882 'render: true',
883 'mixin.componentDidUpdate: true',
884 'componentDidUpdate: true',
885 'mixin.componentWillUnmount: true',
886 'componentWillUnmount: true',
887 'after unmount: false',
888 ]);
889 });
890 });