main
js 784 lines 19.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 let React;
13 let ReactNoop;
14 let Scheduler;
15 let act;
16 let waitFor;
17 let waitForAll;
18 let waitForPaint;
19
20 describe('StrictEffectsMode defaults', () => {
21 beforeEach(() => {
22 jest.resetModules();
23
24 React = require('react');
25 ReactNoop = require('react-noop-renderer');
26 Scheduler = require('scheduler');
27 act = require('internal-test-utils').act;
28
29 const InternalTestUtils = require('internal-test-utils');
30 waitFor = InternalTestUtils.waitFor;
31 waitForAll = InternalTestUtils.waitForAll;
32 waitForPaint = InternalTestUtils.waitForPaint;
33 });
34
35 // @gate !disableLegacyMode
36 it('should not double invoke effects in legacy mode', async () => {
37 const log = [];
38 function App({text}) {
39 React.useEffect(() => {
40 log.push('useEffect mount');
41 return () => log.push('useEffect unmount');
42 });
43
44 React.useLayoutEffect(() => {
45 log.push('useLayoutEffect mount');
46 return () => log.push('useLayoutEffect unmount');
47 });
48
49 return text;
50 }
51
52 await act(() => {
53 ReactNoop.renderLegacySyncRoot(
54 <React.StrictMode>
55 <App text={'mount'} />
56 </React.StrictMode>,
57 );
58 });
59
60 expect(log).toEqual(['useLayoutEffect mount', 'useEffect mount']);
61 });
62
63 // @gate !disableLegacyMode
64 it('should not double invoke class lifecycles in legacy mode', async () => {
65 const log = [];
66 class App extends React.PureComponent {
67 componentDidMount() {
68 log.push('componentDidMount');
69 }
70
71 componentDidUpdate() {
72 log.push('componentDidUpdate');
73 }
74
75 componentWillUnmount() {
76 log.push('componentWillUnmount');
77 }
78
79 render() {
80 return this.props.text;
81 }
82 }
83
84 await act(() => {
85 ReactNoop.renderLegacySyncRoot(
86 <React.StrictMode>
87 <App text={'mount'} />
88 </React.StrictMode>,
89 );
90 });
91
92 expect(log).toEqual(['componentDidMount']);
93 });
94
95 if (__DEV__) {
96 it('should flush double-invoked effects within the same frame as layout effects if there are no passive effects', async () => {
97 const log = [];
98 function ComponentWithEffects({label}) {
99 React.useLayoutEffect(() => {
100 Scheduler.log(`useLayoutEffect mount "${label}"`);
101 log.push(`useLayoutEffect mount "${label}"`);
102 return () => {
103 Scheduler.log(`useLayoutEffect unmount "${label}"`);
104 log.push(`useLayoutEffect unmount "${label}"`);
105 };
106 });
107
108 return label;
109 }
110
111 await act(async () => {
112 ReactNoop.render(
113 <React.StrictMode>
114 <ComponentWithEffects label={'one'} />
115 </React.StrictMode>,
116 );
117
118 await waitForPaint(['useLayoutEffect mount "one"']);
119 expect(log).toEqual([
120 'useLayoutEffect mount "one"',
121 'useLayoutEffect unmount "one"',
122 'useLayoutEffect mount "one"',
123 ]);
124 });
125
126 log.length = 0;
127 await act(async () => {
128 ReactNoop.render(
129 <React.StrictMode>
130 <ComponentWithEffects label={'one'} />
131 <ComponentWithEffects label={'two'} />
132 </React.StrictMode>,
133 );
134
135 expect(log).toEqual([]);
136 await waitForPaint([
137 // Cleanup and re-run "one" (and "two") since there is no dependencies array.
138 'useLayoutEffect unmount "one"',
139 'useLayoutEffect mount "one"',
140 'useLayoutEffect mount "two"',
141 ]);
142 expect(log).toEqual([
143 // Cleanup and re-run "one" (and "two") since there is no dependencies array.
144 'useLayoutEffect unmount "one"',
145 'useLayoutEffect mount "one"',
146 'useLayoutEffect mount "two"',
147
148 // Since "two" is new, it should be double-invoked.
149 'useLayoutEffect unmount "two"',
150 'useLayoutEffect mount "two"',
151 ]);
152 });
153 });
154
155 // This test also verifies that double-invoked effects flush synchronously
156 // within the same frame as passive effects.
157 it('should double invoke effects only for newly mounted components', async () => {
158 const log = [];
159 function ComponentWithEffects({label}) {
160 React.useEffect(() => {
161 log.push(`useEffect mount "${label}"`);
162 Scheduler.log(`useEffect mount "${label}"`);
163 return () => {
164 log.push(`useEffect unmount "${label}"`);
165 Scheduler.log(`useEffect unmount "${label}"`);
166 };
167 });
168
169 React.useLayoutEffect(() => {
170 log.push(`useLayoutEffect mount "${label}"`);
171 Scheduler.log(`useLayoutEffect mount "${label}"`);
172 return () => {
173 log.push(`useLayoutEffect unmount "${label}"`);
174 Scheduler.log(`useLayoutEffect unmount "${label}"`);
175 };
176 });
177
178 return label;
179 }
180
181 await act(async () => {
182 ReactNoop.render(
183 <React.StrictMode>
184 <ComponentWithEffects label={'one'} />
185 </React.StrictMode>,
186 );
187
188 await waitForAll([
189 'useLayoutEffect mount "one"',
190 'useEffect mount "one"',
191 ]);
192 expect(log).toEqual([
193 'useLayoutEffect mount "one"',
194 'useEffect mount "one"',
195 'useLayoutEffect unmount "one"',
196 'useEffect unmount "one"',
197 'useLayoutEffect mount "one"',
198 'useEffect mount "one"',
199 ]);
200 });
201
202 log.length = 0;
203 await act(async () => {
204 ReactNoop.render(
205 <React.StrictMode>
206 <ComponentWithEffects label={'one'} />
207 <ComponentWithEffects label={'two'} />
208 </React.StrictMode>,
209 );
210
211 await waitFor([
212 // Cleanup and re-run "one" (and "two") since there is no dependencies array.
213 'useLayoutEffect unmount "one"',
214 'useLayoutEffect mount "one"',
215 'useLayoutEffect mount "two"',
216 ]);
217 expect(log).toEqual([
218 // Cleanup and re-run "one" (and "two") since there is no dependencies array.
219 'useLayoutEffect unmount "one"',
220 'useLayoutEffect mount "one"',
221 'useLayoutEffect mount "two"',
222 ]);
223 log.length = 0;
224 await waitForAll([
225 'useEffect unmount "one"',
226 'useEffect mount "one"',
227 'useEffect mount "two"',
228 ]);
229 expect(log).toEqual([
230 'useEffect unmount "one"',
231 'useEffect mount "one"',
232 'useEffect mount "two"',
233
234 // Since "two" is new, it should be double-invoked.
235 'useLayoutEffect unmount "two"',
236 'useEffect unmount "two"',
237 'useLayoutEffect mount "two"',
238 'useEffect mount "two"',
239 ]);
240 });
241 });
242
243 it('double invoking for effects for modern roots', async () => {
244 const log = [];
245 function App({text}) {
246 React.useEffect(() => {
247 log.push('useEffect mount');
248 return () => log.push('useEffect unmount');
249 });
250
251 React.useLayoutEffect(() => {
252 log.push('useLayoutEffect mount');
253 return () => log.push('useLayoutEffect unmount');
254 });
255
256 return text;
257 }
258 await act(() => {
259 ReactNoop.render(
260 <React.StrictMode>
261 <App text={'mount'} />
262 </React.StrictMode>,
263 );
264 });
265
266 expect(log).toEqual([
267 'useLayoutEffect mount',
268 'useEffect mount',
269 'useLayoutEffect unmount',
270 'useEffect unmount',
271 'useLayoutEffect mount',
272 'useEffect mount',
273 ]);
274
275 log.length = 0;
276 await act(() => {
277 ReactNoop.render(
278 <React.StrictMode>
279 <App text={'update'} />
280 </React.StrictMode>,
281 );
282 });
283
284 expect(log).toEqual([
285 'useLayoutEffect unmount',
286 'useLayoutEffect mount',
287 'useEffect unmount',
288 'useEffect mount',
289 ]);
290
291 log.length = 0;
292 await act(() => {
293 ReactNoop.render(null);
294 });
295
296 expect(log).toEqual(['useLayoutEffect unmount', 'useEffect unmount']);
297 });
298
299 it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', async () => {
300 const log = [];
301 function App({text}) {
302 React.useEffect(() => {
303 log.push('useEffect One mount');
304 return () => log.push('useEffect One unmount');
305 });
306
307 React.useEffect(() => {
308 log.push('useEffect Two mount');
309 return () => log.push('useEffect Two unmount');
310 });
311
312 return text;
313 }
314
315 await act(() => {
316 ReactNoop.render(
317 <React.StrictMode>
318 <App text={'mount'} />
319 </React.StrictMode>,
320 );
321 });
322
323 expect(log).toEqual([
324 'useEffect One mount',
325 'useEffect Two mount',
326 'useEffect One unmount',
327 'useEffect Two unmount',
328 'useEffect One mount',
329 'useEffect Two mount',
330 ]);
331
332 log.length = 0;
333 await act(() => {
334 ReactNoop.render(
335 <React.StrictMode>
336 <App text={'update'} />
337 </React.StrictMode>,
338 );
339 });
340
341 expect(log).toEqual([
342 'useEffect One unmount',
343 'useEffect Two unmount',
344 'useEffect One mount',
345 'useEffect Two mount',
346 ]);
347
348 log.length = 0;
349 await act(() => {
350 ReactNoop.render(null);
351 });
352
353 expect(log).toEqual(['useEffect One unmount', 'useEffect Two unmount']);
354 });
355
356 it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', async () => {
357 const log = [];
358 function App({text}) {
359 React.useLayoutEffect(() => {
360 log.push('useLayoutEffect One mount');
361 return () => log.push('useLayoutEffect One unmount');
362 });
363
364 React.useLayoutEffect(() => {
365 log.push('useLayoutEffect Two mount');
366 return () => log.push('useLayoutEffect Two unmount');
367 });
368
369 return text;
370 }
371
372 await act(() => {
373 ReactNoop.render(
374 <React.StrictMode>
375 <App text={'mount'} />
376 </React.StrictMode>,
377 );
378 });
379
380 expect(log).toEqual([
381 'useLayoutEffect One mount',
382 'useLayoutEffect Two mount',
383 'useLayoutEffect One unmount',
384 'useLayoutEffect Two unmount',
385 'useLayoutEffect One mount',
386 'useLayoutEffect Two mount',
387 ]);
388
389 log.length = 0;
390 await act(() => {
391 ReactNoop.render(
392 <React.StrictMode>
393 <App text={'update'} />
394 </React.StrictMode>,
395 );
396 });
397
398 expect(log).toEqual([
399 'useLayoutEffect One unmount',
400 'useLayoutEffect Two unmount',
401 'useLayoutEffect One mount',
402 'useLayoutEffect Two mount',
403 ]);
404
405 log.length = 0;
406 await act(() => {
407 ReactNoop.render(null);
408 });
409
410 expect(log).toEqual([
411 'useLayoutEffect One unmount',
412 'useLayoutEffect Two unmount',
413 ]);
414 });
415
416 it('useEffect and useLayoutEffect is called twice when there is no unmount', async () => {
417 const log = [];
418 function App({text}) {
419 React.useEffect(() => {
420 log.push('useEffect mount');
421 });
422
423 React.useLayoutEffect(() => {
424 log.push('useLayoutEffect mount');
425 });
426
427 return text;
428 }
429
430 await act(() => {
431 ReactNoop.render(
432 <React.StrictMode>
433 <App text={'mount'} />
434 </React.StrictMode>,
435 );
436 });
437
438 expect(log).toEqual([
439 'useLayoutEffect mount',
440 'useEffect mount',
441 'useLayoutEffect mount',
442 'useEffect mount',
443 ]);
444
445 log.length = 0;
446 await act(() => {
447 ReactNoop.render(
448 <React.StrictMode>
449 <App text={'update'} />
450 </React.StrictMode>,
451 );
452 });
453
454 expect(log).toEqual(['useLayoutEffect mount', 'useEffect mount']);
455
456 log.length = 0;
457 await act(() => {
458 ReactNoop.render(null);
459 });
460
461 expect(log).toEqual([]);
462 });
463
464 it('disconnects refs during double invoking', async () => {
465 const onRefMock = jest.fn();
466 function App({text}) {
467 return (
468 <span
469 ref={ref => {
470 onRefMock(ref);
471 }}>
472 text
473 </span>
474 );
475 }
476
477 await act(() => {
478 ReactNoop.render(
479 <React.StrictMode>
480 <App text={'mount'} />
481 </React.StrictMode>,
482 );
483 });
484
485 expect(onRefMock.mock.calls.length).toBe(3);
486 expect(onRefMock.mock.calls[0][0]).not.toBeNull();
487 expect(onRefMock.mock.calls[1][0]).toBe(null);
488 expect(onRefMock.mock.calls[2][0]).not.toBeNull();
489 });
490
491 it('passes the right context to class component lifecycles', async () => {
492 const log = [];
493 class App extends React.PureComponent {
494 test() {}
495
496 componentDidMount() {
497 this.test();
498 log.push('componentDidMount');
499 }
500
501 componentDidUpdate() {
502 this.test();
503 log.push('componentDidUpdate');
504 }
505
506 componentWillUnmount() {
507 this.test();
508 log.push('componentWillUnmount');
509 }
510
511 render() {
512 return null;
513 }
514 }
515
516 await act(() => {
517 ReactNoop.render(
518 <React.StrictMode>
519 <App />
520 </React.StrictMode>,
521 );
522 });
523
524 expect(log).toEqual([
525 'componentDidMount',
526 'componentWillUnmount',
527 'componentDidMount',
528 ]);
529 });
530
531 it('double invoking works for class components', async () => {
532 const log = [];
533 class App extends React.PureComponent {
534 componentDidMount() {
535 log.push('componentDidMount');
536 }
537
538 componentDidUpdate() {
539 log.push('componentDidUpdate');
540 }
541
542 componentWillUnmount() {
543 log.push('componentWillUnmount');
544 }
545
546 render() {
547 return this.props.text;
548 }
549 }
550
551 await act(() => {
552 ReactNoop.render(
553 <React.StrictMode>
554 <App text={'mount'} />
555 </React.StrictMode>,
556 );
557 });
558
559 expect(log).toEqual([
560 'componentDidMount',
561 'componentWillUnmount',
562 'componentDidMount',
563 ]);
564
565 log.length = 0;
566 await act(() => {
567 ReactNoop.render(
568 <React.StrictMode>
569 <App text={'update'} />
570 </React.StrictMode>,
571 );
572 });
573
574 expect(log).toEqual(['componentDidUpdate']);
575
576 log.length = 0;
577 await act(() => {
578 ReactNoop.render(null);
579 });
580
581 expect(log).toEqual(['componentWillUnmount']);
582 });
583
584 it('double flushing passive effects only results in one double invoke', async () => {
585 const log = [];
586 function App({text}) {
587 const [state, setState] = React.useState(0);
588 React.useEffect(() => {
589 if (state !== 1) {
590 setState(1);
591 }
592 log.push('useEffect mount');
593 return () => log.push('useEffect unmount');
594 });
595
596 React.useLayoutEffect(() => {
597 log.push('useLayoutEffect mount');
598 return () => log.push('useLayoutEffect unmount');
599 });
600
601 log.push(text);
602 return text;
603 }
604
605 log.length = 0;
606 await act(() => {
607 ReactNoop.render(
608 <React.StrictMode>
609 <App text={'mount'} />
610 </React.StrictMode>,
611 );
612 });
613
614 expect(log).toEqual([
615 'mount',
616 'mount',
617 'useLayoutEffect mount',
618 'useEffect mount',
619 'useLayoutEffect unmount',
620 'useEffect unmount',
621 'useLayoutEffect mount',
622 'useEffect mount',
623 'mount',
624 'mount',
625 'useLayoutEffect unmount',
626 'useLayoutEffect mount',
627 'useEffect unmount',
628 'useEffect mount',
629 ]);
630 });
631
632 it('newly mounted components after initial mount get double invoked', async () => {
633 let _setShowChild;
634 const log = [];
635 function Child() {
636 React.useEffect(() => {
637 log.push('Child useEffect mount');
638 return () => log.push('Child useEffect unmount');
639 });
640 React.useLayoutEffect(() => {
641 log.push('Child useLayoutEffect mount');
642 return () => log.push('Child useLayoutEffect unmount');
643 });
644
645 return null;
646 }
647
648 function App() {
649 const [showChild, setShowChild] = React.useState(false);
650 _setShowChild = setShowChild;
651 React.useEffect(() => {
652 log.push('App useEffect mount');
653 return () => log.push('App useEffect unmount');
654 });
655 React.useLayoutEffect(() => {
656 log.push('App useLayoutEffect mount');
657 return () => log.push('App useLayoutEffect unmount');
658 });
659
660 return showChild && <Child />;
661 }
662
663 await act(() => {
664 ReactNoop.render(
665 <React.StrictMode>
666 <App />
667 </React.StrictMode>,
668 );
669 });
670
671 expect(log).toEqual([
672 'App useLayoutEffect mount',
673 'App useEffect mount',
674 'App useLayoutEffect unmount',
675 'App useEffect unmount',
676 'App useLayoutEffect mount',
677 'App useEffect mount',
678 ]);
679
680 log.length = 0;
681 await act(() => {
682 _setShowChild(true);
683 });
684
685 expect(log).toEqual([
686 'App useLayoutEffect unmount',
687 'Child useLayoutEffect mount',
688 'App useLayoutEffect mount',
689 'App useEffect unmount',
690 'Child useEffect mount',
691 'App useEffect mount',
692 'Child useLayoutEffect unmount',
693 'Child useEffect unmount',
694 'Child useLayoutEffect mount',
695 'Child useEffect mount',
696 ]);
697 });
698
699 it('classes and functions are double invoked together correctly', async () => {
700 const log = [];
701 class ClassChild extends React.PureComponent {
702 componentDidMount() {
703 log.push('componentDidMount');
704 }
705
706 componentWillUnmount() {
707 log.push('componentWillUnmount');
708 }
709
710 render() {
711 return this.props.text;
712 }
713 }
714
715 function FunctionChild({text}) {
716 React.useEffect(() => {
717 log.push('useEffect mount');
718 return () => log.push('useEffect unmount');
719 });
720 React.useLayoutEffect(() => {
721 log.push('useLayoutEffect mount');
722 return () => log.push('useLayoutEffect unmount');
723 });
724 return text;
725 }
726
727 function App({text}) {
728 return (
729 <React.StrictMode>
730 <ClassChild text={text} />
731 <FunctionChild text={text} />
732 </React.StrictMode>
733 );
734 }
735
736 await act(() => {
737 ReactNoop.render(
738 <React.StrictMode>
739 <App text={'mount'} />
740 </React.StrictMode>,
741 );
742 });
743
744 expect(log).toEqual([
745 'componentDidMount',
746 'useLayoutEffect mount',
747 'useEffect mount',
748 'componentWillUnmount',
749 'useLayoutEffect unmount',
750 'useEffect unmount',
751 'componentDidMount',
752 'useLayoutEffect mount',
753 'useEffect mount',
754 ]);
755
756 log.length = 0;
757 await act(() => {
758 ReactNoop.render(
759 <React.StrictMode>
760 <App text={'mount'} />
761 </React.StrictMode>,
762 );
763 });
764
765 expect(log).toEqual([
766 'useLayoutEffect unmount',
767 'useLayoutEffect mount',
768 'useEffect unmount',
769 'useEffect mount',
770 ]);
771
772 log.length = 0;
773 await act(() => {
774 ReactNoop.render(null);
775 });
776
777 expect(log).toEqual([
778 'componentWillUnmount',
779 'useLayoutEffect unmount',
780 'useEffect unmount',
781 ]);
782 });
783 }
784 });