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