main
js 390 lines 10.7 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 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13
14 let React;
15 let ReactDOMServer;
16 let assertConsoleErrorDev;
17 let assertConsoleWarnDev;
18
19 function initModules() {
20 // Reset warning cache.
21 jest.resetModules();
22 React = require('react');
23 ReactDOMServer = require('react-dom/server');
24 assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev;
25 assertConsoleWarnDev = require('internal-test-utils').assertConsoleWarnDev;
26
27 // Make them available to the helpers.
28 return {
29 ReactDOMServer,
30 };
31 }
32
33 const {resetModules} = ReactDOMServerIntegrationUtils(initModules);
34
35 describe('ReactDOMServerLifecycles', () => {
36 beforeEach(() => {
37 resetModules();
38 });
39
40 it('should invoke the correct legacy lifecycle hooks', () => {
41 const log = [];
42
43 class Outer extends React.Component {
44 UNSAFE_componentWillMount() {
45 log.push('outer componentWillMount');
46 }
47 render() {
48 log.push('outer render');
49 return <Inner />;
50 }
51 }
52
53 class Inner extends React.Component {
54 UNSAFE_componentWillMount() {
55 log.push('inner componentWillMount');
56 }
57 render() {
58 log.push('inner render');
59 return null;
60 }
61 }
62
63 ReactDOMServer.renderToString(<Outer />);
64 expect(log).toEqual([
65 'outer componentWillMount',
66 'outer render',
67 'inner componentWillMount',
68 'inner render',
69 ]);
70 });
71
72 it('should invoke the correct new lifecycle hooks', () => {
73 const log = [];
74
75 class Outer extends React.Component {
76 state = {};
77 static getDerivedStateFromProps() {
78 log.push('outer getDerivedStateFromProps');
79 return null;
80 }
81 render() {
82 log.push('outer render');
83 return <Inner />;
84 }
85 }
86
87 class Inner extends React.Component {
88 state = {};
89 static getDerivedStateFromProps() {
90 log.push('inner getDerivedStateFromProps');
91 return null;
92 }
93 render() {
94 log.push('inner render');
95 return null;
96 }
97 }
98
99 ReactDOMServer.renderToString(<Outer />);
100 expect(log).toEqual([
101 'outer getDerivedStateFromProps',
102 'outer render',
103 'inner getDerivedStateFromProps',
104 'inner render',
105 ]);
106 });
107
108 it('should not invoke unsafe cWM if static gDSFP is present', () => {
109 class Component extends React.Component {
110 state = {};
111 static getDerivedStateFromProps() {
112 return null;
113 }
114 UNSAFE_componentWillMount() {
115 throw Error('unexpected');
116 }
117 render() {
118 return null;
119 }
120 }
121
122 ReactDOMServer.renderToString(<Component />);
123 assertConsoleErrorDev([
124 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n' +
125 '\n' +
126 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
127 ' UNSAFE_componentWillMount\n' +
128 '\n' +
129 'The above lifecycles should be removed. Learn more about this warning here:\n' +
130 'https://react.dev/link/unsafe-component-lifecycles\n' +
131 ' in Component (at **)',
132 ]);
133 });
134
135 it('should update instance.state with value returned from getDerivedStateFromProps', () => {
136 class Grandparent extends React.Component {
137 state = {
138 foo: 'foo',
139 };
140 render() {
141 return (
142 <div>
143 {`Grandparent: ${this.state.foo}`}
144 <Parent />
145 </div>
146 );
147 }
148 }
149
150 class Parent extends React.Component {
151 state = {
152 bar: 'bar',
153 baz: 'baz',
154 };
155 static getDerivedStateFromProps(props, prevState) {
156 return {
157 bar: `not ${prevState.bar}`,
158 };
159 }
160 render() {
161 return (
162 <div>
163 {`Parent: ${this.state.bar}, ${this.state.baz}`}
164 <Child />;
165 </div>
166 );
167 }
168 }
169
170 class Child extends React.Component {
171 state = {};
172 static getDerivedStateFromProps() {
173 return {
174 qux: 'qux',
175 };
176 }
177 render() {
178 return `Child: ${this.state.qux}`;
179 }
180 }
181
182 const markup = ReactDOMServer.renderToString(<Grandparent />);
183 expect(markup).toContain('Grandparent: foo');
184 expect(markup).toContain('Parent: not bar, baz');
185 expect(markup).toContain('Child: qux');
186 });
187
188 it('should warn if getDerivedStateFromProps returns undefined', () => {
189 class Component extends React.Component {
190 state = {};
191 static getDerivedStateFromProps() {}
192 render() {
193 return null;
194 }
195 }
196
197 ReactDOMServer.renderToString(<Component />);
198 assertConsoleErrorDev([
199 'Component.getDerivedStateFromProps(): A valid state object (or null) must ' +
200 'be returned. You have returned undefined.\n' +
201 ' in Component (at **)',
202 ]);
203
204 // De-duped
205 ReactDOMServer.renderToString(<Component />);
206 });
207
208 it('should warn if state is not initialized before getDerivedStateFromProps', () => {
209 class Component extends React.Component {
210 static getDerivedStateFromProps() {
211 return null;
212 }
213 render() {
214 return null;
215 }
216 }
217
218 ReactDOMServer.renderToString(<Component />);
219 assertConsoleErrorDev([
220 '`Component` uses `getDerivedStateFromProps` but its initial state is ' +
221 'undefined. This is not recommended. Instead, define the initial state by ' +
222 'assigning an object to `this.state` in the constructor of `Component`. ' +
223 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
224 ' in Component (at **)',
225 ]);
226
227 // De-duped
228 ReactDOMServer.renderToString(<Component />);
229 });
230
231 it('should invoke both deprecated and new lifecycles if both are present', () => {
232 const log = [];
233
234 class Component extends React.Component {
235 componentWillMount() {
236 log.push('componentWillMount');
237 }
238 UNSAFE_componentWillMount() {
239 log.push('UNSAFE_componentWillMount');
240 }
241 render() {
242 return null;
243 }
244 }
245
246 ReactDOMServer.renderToString(<Component />);
247 assertConsoleWarnDev([
248 'componentWillMount has been renamed, and is not recommended for use. ' +
249 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
250 '\n' +
251 '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
252 '\n' +
253 'Please update the following components: Component\n' +
254 ' in Component (at **)',
255 ]);
256 expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
257 });
258
259 it('tracks state updates across components', () => {
260 class Outer extends React.Component {
261 UNSAFE_componentWillMount() {
262 this.setState({x: 1});
263 }
264 render() {
265 return <Inner updateParent={this.updateParent}>{this.state.x}</Inner>;
266 }
267 updateParent = () => {
268 this.setState({x: 3});
269 };
270 }
271 class Inner extends React.Component {
272 UNSAFE_componentWillMount() {
273 this.setState({x: 2});
274 this.props.updateParent();
275 }
276 render() {
277 return <div>{this.props.children + '-' + this.state.x}</div>;
278 }
279 }
280 // Shouldn't be 1-3.
281 expect(ReactDOMServer.renderToStaticMarkup(<Outer />)).toBe(
282 '<div>1-2</div>',
283 );
284 assertConsoleErrorDev([
285 'Can only update a mounting component. This ' +
286 'usually means you called setState() outside componentWillMount() on ' +
287 'the server. This is a no-op.\n\n' +
288 'Please check the code for the Outer component.\n' +
289 ' in Outer (at **)',
290 ]);
291 });
292
293 it('should not invoke cWM if static gDSFP is present', () => {
294 class Component extends React.Component {
295 state = {};
296 static getDerivedStateFromProps() {
297 return null;
298 }
299 componentWillMount() {
300 throw Error('unexpected');
301 }
302 render() {
303 return null;
304 }
305 }
306
307 ReactDOMServer.renderToString(<Component />);
308 assertConsoleErrorDev([
309 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n' +
310 '\n' +
311 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
312 ' componentWillMount\n' +
313 '\n' +
314 'The above lifecycles should be removed. Learn more about this warning here:\n' +
315 'https://react.dev/link/unsafe-component-lifecycles\n' +
316 ' in Component (at **)',
317 ]);
318 });
319
320 it('should warn about deprecated lifecycle hooks', () => {
321 class MyComponent extends React.Component {
322 componentWillMount() {}
323 render() {
324 return null;
325 }
326 }
327
328 ReactDOMServer.renderToString(<MyComponent />);
329 assertConsoleWarnDev([
330 'componentWillMount has been renamed, and is not recommended for use. ' +
331 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
332 '\n' +
333 '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
334 '\n' +
335 'Please update the following components: MyComponent\n' +
336 ' in MyComponent (at **)',
337 ]);
338
339 // De-duped
340 ReactDOMServer.renderToString(<MyComponent />);
341 });
342
343 describe('react-lifecycles-compat', () => {
344 const {polyfill} = require('react-lifecycles-compat');
345
346 it('should not warn for components with polyfilled getDerivedStateFromProps', () => {
347 class PolyfilledComponent extends React.Component {
348 state = {};
349 static getDerivedStateFromProps() {
350 return null;
351 }
352 render() {
353 return null;
354 }
355 }
356
357 polyfill(PolyfilledComponent);
358
359 const container = document.createElement('div');
360 ReactDOMServer.renderToString(
361 <React.StrictMode>
362 <PolyfilledComponent />
363 </React.StrictMode>,
364 container,
365 );
366 });
367
368 it('should not warn for components with polyfilled getSnapshotBeforeUpdate', () => {
369 class PolyfilledComponent extends React.Component {
370 getSnapshotBeforeUpdate() {
371 return null;
372 }
373 componentDidUpdate() {}
374 render() {
375 return null;
376 }
377 }
378
379 polyfill(PolyfilledComponent);
380
381 const container = document.createElement('div');
382 ReactDOMServer.renderToString(
383 <React.StrictMode>
384 <PolyfilledComponent />
385 </React.StrictMode>,
386 container,
387 );
388 });
389 });
390 });