main
js 611 lines 17.2 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let React;
13 let ReactDOM;
14 let ReactDOMClient;
15 let act;
16 let Scheduler;
17 let assertLog;
18 let TestComponent;
19 let testComponentInstance;
20 let assertConsoleErrorDev;
21
22 describe('ReactCompositeComponent-state', () => {
23 beforeEach(() => {
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMClient = require('react-dom/client');
27 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
28 Scheduler = require('scheduler');
29
30 const InternalTestUtils = require('internal-test-utils');
31 assertLog = InternalTestUtils.assertLog;
32
33 function LogAfterCommit({children, color}) {
34 React.useEffect(() => {
35 Scheduler.log(`commit ${color}`);
36 });
37 return children;
38 }
39
40 TestComponent = class extends React.Component {
41 constructor(props) {
42 super(props);
43 this.peekAtState('getInitialState', undefined, props);
44 this.state = {color: 'red'};
45 testComponentInstance = this;
46 }
47
48 peekAtState = (from, state = this.state, props = this.props) => {
49 Scheduler.log(`${from} ${state && state.color}`);
50 };
51
52 peekAtCallback = from => {
53 return () => this.peekAtState(from);
54 };
55
56 setFavoriteColor(nextColor) {
57 this.setState(
58 {color: nextColor},
59 this.peekAtCallback('setFavoriteColor'),
60 );
61 }
62
63 render() {
64 this.peekAtState('render');
65 return (
66 <LogAfterCommit color={this.state.color}>
67 <div>{this.state.color}</div>
68 </LogAfterCommit>
69 );
70 }
71
72 UNSAFE_componentWillMount() {
73 this.peekAtState('componentWillMount-start');
74 this.setState(function (state) {
75 this.peekAtState('before-setState-sunrise', state);
76 });
77 this.setState(
78 {color: 'sunrise'},
79 this.peekAtCallback('setState-sunrise'),
80 );
81 this.setState(function (state) {
82 this.peekAtState('after-setState-sunrise', state);
83 });
84 this.peekAtState('componentWillMount-after-sunrise');
85 this.setState(
86 {color: 'orange'},
87 this.peekAtCallback('setState-orange'),
88 );
89 this.setState(function (state) {
90 this.peekAtState('after-setState-orange', state);
91 });
92 this.peekAtState('componentWillMount-end');
93 }
94
95 componentDidMount() {
96 this.peekAtState('componentDidMount-start');
97 this.setState(
98 {color: 'yellow'},
99 this.peekAtCallback('setState-yellow'),
100 );
101 this.peekAtState('componentDidMount-end');
102 }
103
104 UNSAFE_componentWillReceiveProps(newProps) {
105 this.peekAtState('componentWillReceiveProps-start');
106 if (newProps.nextColor) {
107 this.setState(function (state) {
108 this.peekAtState('before-setState-receiveProps', state);
109 return {color: newProps.nextColor};
110 });
111 // No longer a public API, but we can test that it works internally by
112 // reaching into the updater.
113 this.updater.enqueueReplaceState(this, {color: undefined});
114 this.setState(function (state) {
115 this.peekAtState('before-setState-again-receiveProps', state);
116 return {color: newProps.nextColor};
117 }, this.peekAtCallback('setState-receiveProps'));
118 this.setState(function (state) {
119 this.peekAtState('after-setState-receiveProps', state);
120 });
121 }
122 this.peekAtState('componentWillReceiveProps-end');
123 }
124
125 shouldComponentUpdate(nextProps, nextState) {
126 this.peekAtState('shouldComponentUpdate-currentState');
127 this.peekAtState('shouldComponentUpdate-nextState', nextState);
128 return true;
129 }
130
131 UNSAFE_componentWillUpdate(nextProps, nextState) {
132 this.peekAtState('componentWillUpdate-currentState');
133 this.peekAtState('componentWillUpdate-nextState', nextState);
134 }
135
136 componentDidUpdate(prevProps, prevState) {
137 this.peekAtState('componentDidUpdate-currentState');
138 this.peekAtState('componentDidUpdate-prevState', prevState);
139 }
140
141 componentWillUnmount() {
142 this.peekAtState('componentWillUnmount');
143 }
144 };
145 });
146
147 it('should support setting state', async () => {
148 const container = document.createElement('div');
149 document.body.appendChild(container);
150 const root = ReactDOMClient.createRoot(container);
151
152 await act(() => {
153 root.render(<TestComponent />);
154 });
155
156 assertLog([
157 // there is no state when getInitialState() is called
158 'getInitialState undefined',
159 'componentWillMount-start red',
160 // setState()'s only enqueue pending states.
161 'componentWillMount-after-sunrise red',
162 'componentWillMount-end red',
163 // pending state queue is processed
164 'before-setState-sunrise red',
165 'after-setState-sunrise sunrise',
166 'after-setState-orange orange',
167 // pending state has been applied
168 'render orange',
169 'componentDidMount-start orange',
170 // setState-sunrise and setState-orange should be called here,
171 // after the bug in #1740
172 // componentDidMount() called setState({color:'yellow'}), which is async.
173 // The update doesn't happen until the next flush.
174 'componentDidMount-end orange',
175 'setState-sunrise orange',
176 'setState-orange orange',
177 'commit orange',
178 'shouldComponentUpdate-currentState orange',
179 'shouldComponentUpdate-nextState yellow',
180 'componentWillUpdate-currentState orange',
181 'componentWillUpdate-nextState yellow',
182 'render yellow',
183 'componentDidUpdate-currentState yellow',
184 'componentDidUpdate-prevState orange',
185 'setState-yellow yellow',
186 'commit yellow',
187 ]);
188
189 await act(() => {
190 root.render(<TestComponent nextColor="green" />);
191 });
192
193 assertLog([
194 'componentWillReceiveProps-start yellow',
195 // setState({color:'green'}) only enqueues a pending state.
196 'componentWillReceiveProps-end yellow',
197 // pending state queue is processed
198 // We keep updates in the queue to support
199 // replaceState(prevState => newState).
200 'before-setState-receiveProps yellow',
201 'before-setState-again-receiveProps undefined',
202 'after-setState-receiveProps green',
203 'shouldComponentUpdate-currentState yellow',
204 'shouldComponentUpdate-nextState green',
205 'componentWillUpdate-currentState yellow',
206 'componentWillUpdate-nextState green',
207 'render green',
208 'componentDidUpdate-currentState green',
209 'componentDidUpdate-prevState yellow',
210 'setState-receiveProps green',
211 'commit green',
212 ]);
213
214 await act(() => {
215 testComponentInstance.setFavoriteColor('blue');
216 });
217
218 assertLog([
219 // setFavoriteColor('blue')
220 'shouldComponentUpdate-currentState green',
221 'shouldComponentUpdate-nextState blue',
222 'componentWillUpdate-currentState green',
223 'componentWillUpdate-nextState blue',
224 'render blue',
225 'componentDidUpdate-currentState blue',
226 'componentDidUpdate-prevState green',
227 'setFavoriteColor blue',
228 'commit blue',
229 ]);
230 await act(() => {
231 testComponentInstance.forceUpdate(
232 testComponentInstance.peekAtCallback('forceUpdate'),
233 );
234 });
235 assertLog([
236 // forceUpdate()
237 'componentWillUpdate-currentState blue',
238 'componentWillUpdate-nextState blue',
239 'render blue',
240 'componentDidUpdate-currentState blue',
241 'componentDidUpdate-prevState blue',
242 'forceUpdate blue',
243 'commit blue',
244 ]);
245
246 root.unmount();
247
248 assertLog([
249 // unmount()
250 // state is available within `componentWillUnmount()`
251 'componentWillUnmount blue',
252 ]);
253 });
254
255 it('should call componentDidUpdate of children first', async () => {
256 const container = document.createElement('div');
257
258 let child = null;
259 let parent = null;
260
261 class Child extends React.Component {
262 state = {bar: false};
263 componentDidMount() {
264 child = this;
265 }
266 componentDidUpdate() {
267 Scheduler.log('child did update');
268 }
269 render() {
270 return <div />;
271 }
272 }
273
274 let shouldUpdate = true;
275
276 class Intermediate extends React.Component {
277 shouldComponentUpdate() {
278 return shouldUpdate;
279 }
280 render() {
281 return <Child />;
282 }
283 }
284
285 class Parent extends React.Component {
286 state = {foo: false};
287 componentDidMount() {
288 parent = this;
289 }
290 componentDidUpdate() {
291 Scheduler.log('parent did update');
292 }
293 render() {
294 return <Intermediate />;
295 }
296 }
297
298 const root = ReactDOMClient.createRoot(container);
299 await act(() => {
300 root.render(<Parent />);
301 });
302
303 await act(() => {
304 parent.setState({foo: true});
305 child.setState({bar: true});
306 });
307
308 // When we render changes top-down in a batch, children's componentDidUpdate
309 // happens before the parent.
310 assertLog(['child did update', 'parent did update']);
311
312 shouldUpdate = false;
313
314 await act(() => {
315 parent.setState({foo: false});
316 child.setState({bar: false});
317 });
318
319 // We expect the same thing to happen if we bail out in the middle.
320 assertLog(['child did update', 'parent did update']);
321 });
322
323 it('should batch unmounts', async () => {
324 let outer;
325 class Inner extends React.Component {
326 render() {
327 return <div />;
328 }
329
330 componentWillUnmount() {
331 // This should get silently ignored (maybe with a warning), but it
332 // shouldn't break React.
333 outer.setState({showInner: false});
334 }
335 }
336
337 class Outer extends React.Component {
338 state = {showInner: true};
339 componentDidMount() {
340 outer = this;
341 }
342
343 render() {
344 return <div>{this.state.showInner && <Inner />}</div>;
345 }
346 }
347
348 const container = document.createElement('div');
349 const root = ReactDOMClient.createRoot(container);
350 await act(() => {
351 root.render(<Outer />);
352 });
353
354 expect(() => {
355 root.unmount();
356 }).not.toThrow();
357 });
358
359 it('should update state when called from child cWRP', async () => {
360 class Parent extends React.Component {
361 state = {value: 'one'};
362 render() {
363 Scheduler.log('parent render ' + this.state.value);
364 return <Child parent={this} value={this.state.value} />;
365 }
366 }
367 let updated = false;
368 class Child extends React.Component {
369 UNSAFE_componentWillReceiveProps() {
370 if (updated) {
371 return;
372 }
373 Scheduler.log('child componentWillReceiveProps ' + this.props.value);
374 this.props.parent.setState({value: 'two'});
375 Scheduler.log(
376 'child componentWillReceiveProps done ' + this.props.value,
377 );
378 updated = true;
379 }
380 render() {
381 Scheduler.log('child render ' + this.props.value);
382 return <div>{this.props.value}</div>;
383 }
384 }
385 const container = document.createElement('div');
386 const root = ReactDOMClient.createRoot(container);
387 await act(() => {
388 root.render(<Parent />);
389 });
390
391 assertLog(['parent render one', 'child render one']);
392 await act(() => {
393 root.render(<Parent />);
394 });
395
396 assertLog([
397 'parent render one',
398 'child componentWillReceiveProps one',
399 'child componentWillReceiveProps done one',
400 'child render one',
401 'parent render two',
402 'child render two',
403 ]);
404 });
405
406 it('should merge state when sCU returns false', async () => {
407 let test;
408 class Test extends React.Component {
409 state = {a: 0};
410 componentDidMount() {
411 test = this;
412 }
413
414 render() {
415 return null;
416 }
417 shouldComponentUpdate(nextProps, nextState) {
418 Scheduler.log(
419 'scu from ' +
420 Object.keys(this.state) +
421 ' to ' +
422 Object.keys(nextState),
423 );
424 return false;
425 }
426 }
427
428 const container = document.createElement('div');
429 const root = ReactDOMClient.createRoot(container);
430 await act(() => {
431 root.render(<Test />);
432 });
433 await act(() => {
434 test.setState({b: 0});
435 });
436
437 assertLog(['scu from a to a,b']);
438 await act(() => {
439 test.setState({c: 0});
440 });
441 assertLog(['scu from a,b to a,b,c']);
442 });
443
444 it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', async () => {
445 class Test extends React.Component {
446 state = {step: 1, extra: true};
447 UNSAFE_componentWillReceiveProps() {
448 this.setState({step: 2}, () => {
449 // Tests that earlier setState callbacks are not dropped
450 Scheduler.log(
451 `callback -- step: ${this.state.step}, extra: ${!!this.state
452 .extra}`,
453 );
454 });
455 // Treat like replaceState
456 this.state = {step: 3};
457 }
458 render() {
459 Scheduler.log(
460 `render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
461 );
462 return null;
463 }
464 }
465
466 // Mount
467 const container = document.createElement('div');
468 const root = ReactDOMClient.createRoot(container);
469 await act(() => {
470 root.render(<Test />);
471 });
472 // Update
473 ReactDOM.flushSync(() => {
474 root.render(<Test />);
475 });
476 assertConsoleErrorDev([
477 'Test.componentWillReceiveProps(): Assigning directly to ' +
478 "this.state is deprecated (except inside a component's constructor). " +
479 'Use setState instead.\n' +
480 ' in Test (at **)',
481 ]);
482
483 assertLog([
484 'render -- step: 1, extra: true',
485 'render -- step: 3, extra: false',
486 'callback -- step: 3, extra: false',
487 ]);
488
489 // Check deduplication; (no additional warnings are expected)
490 expect(() => {
491 ReactDOM.flushSync(() => {
492 root.render(<Test />);
493 });
494 }).not.toThrow();
495 });
496
497 it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
498 class Test extends React.Component {
499 state = {step: 1, extra: true};
500 UNSAFE_componentWillMount() {
501 this.setState({step: 2}, () => {
502 // Tests that earlier setState callbacks are not dropped
503 Scheduler.log(
504 `callback -- step: ${this.state.step}, extra: ${!!this.state
505 .extra}`,
506 );
507 });
508 // Treat like replaceState
509 this.state = {step: 3};
510 }
511 render() {
512 Scheduler.log(
513 `render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
514 );
515 return null;
516 }
517 }
518
519 // Mount
520 const container = document.createElement('div');
521 const root = ReactDOMClient.createRoot(container);
522 ReactDOM.flushSync(() => {
523 root.render(<Test />);
524 });
525 assertConsoleErrorDev([
526 'Test.componentWillMount(): Assigning directly to ' +
527 "this.state is deprecated (except inside a component's constructor). " +
528 'Use setState instead.\n' +
529 ' in Test (at **)',
530 ]);
531
532 assertLog([
533 'render -- step: 3, extra: false',
534 'callback -- step: 3, extra: false',
535
536 // A second time for the retry.
537 'render -- step: 3, extra: false',
538 'callback -- step: 3, extra: false',
539 ]);
540 });
541
542 it('should not support setState in componentWillUnmount', async () => {
543 let subscription;
544 class A extends React.Component {
545 componentWillUnmount() {
546 subscription();
547 }
548 render() {
549 return 'A';
550 }
551 }
552
553 class B extends React.Component {
554 state = {siblingUnmounted: false};
555 UNSAFE_componentWillMount() {
556 subscription = () => this.setState({siblingUnmounted: true});
557 }
558 render() {
559 return 'B' + (this.state.siblingUnmounted ? ' No Sibling' : '');
560 }
561 }
562
563 const el = document.createElement('div');
564 const root = ReactDOMClient.createRoot(el);
565 await act(() => {
566 root.render(<A />);
567 });
568 expect(el.textContent).toBe('A');
569
570 ReactDOM.flushSync(() => {
571 root.render(<B />);
572 });
573 assertConsoleErrorDev([
574 "Can't perform a React state update on a component that hasn't mounted yet. " +
575 'This indicates that you have a side-effect in your render function that ' +
576 'asynchronously tries to update the component. ' +
577 'Move this work to useEffect instead.\n' +
578 ' in B (at **)',
579 ]);
580 });
581
582 // @gate !disableLegacyMode
583 it('Legacy mode should support setState in componentWillUnmount (#18851)', () => {
584 let subscription;
585 class A extends React.Component {
586 componentWillUnmount() {
587 subscription();
588 }
589 render() {
590 return 'A';
591 }
592 }
593
594 class B extends React.Component {
595 state = {siblingUnmounted: false};
596 UNSAFE_componentWillMount() {
597 subscription = () => this.setState({siblingUnmounted: true});
598 }
599 render() {
600 return 'B' + (this.state.siblingUnmounted ? ' No Sibling' : '');
601 }
602 }
603
604 const el = document.createElement('div');
605 ReactDOM.render(<A />, el);
606 expect(el.textContent).toBe('A');
607
608 ReactDOM.render(<B />, el);
609 expect(el.textContent).toBe('B No Sibling');
610 });
611 });