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
+/* eslint-disable no-for-of-loops/no-for-of-loops */
11
+
12
+'use strict';
13
+
14
+let React;
15
+let ReactDOM;
16
+let Scheduler;
17
+let act;
18
+
19
+describe('ReactFresh', () => {
20
+ let container;
21
+ let familiesByID;
22
+ let familiesByType;
23
+ let newFamilies;
24
+ let updatedFamilies;
25
+ let performHotReload;
26
+ let signaturesByType;
27
+
28
+ beforeEach(() => {
29
+ let scheduleHotUpdate;
30
+ let lastRoot;
31
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
32
+ supportsFiber: true,
33
+ inject: injected => {
34
+ scheduleHotUpdate = injected.scheduleHotUpdate;
35
+ },
36
+ onCommitFiberRoot: (id, root) => {
37
+ lastRoot = root;
38
+ },
39
+ onCommitFiberUnmount: () => {},
40
+ };
41
+
42
+ jest.resetModules();
43
+ React = require('react');
44
+ ReactDOM = require('react-dom');
45
+ Scheduler = require('scheduler');
46
+ act = require('react-dom/test-utils').act;
47
+ container = document.createElement('div');
48
+ document.body.appendChild(container);
49
+
50
+ familiesByID = new Map();
51
+ familiesByType = new WeakMap();
52
+
53
+ if (__DEV__) {
54
+ performHotReload = function(staleFamilies) {
55
+ scheduleHotUpdate({
56
+ root: lastRoot,
57
+ familiesByType,
58
+ updatedFamilies,
59
+ staleFamilies,
60
+ });
61
+ };
62
+ }
63
+ });
64
+
65
+ afterEach(() => {
66
+ document.body.removeChild(container);
67
+ });
68
+
69
+ function prepare(version) {
70
+ newFamilies = new Set();
71
+ updatedFamilies = new Set();
72
+ signaturesByType = new Map();
73
+ const Component = version();
74
+
75
+ // Fill in the signatures.
76
+ for (let family of newFamilies) {
77
+ const latestSignature = signaturesByType.get(family.currentType) || null;
78
+ family.currentSignature = latestSignature;
79
+ }
80
+
81
+ newFamilies = null;
82
+ updatedFamilies = null;
83
+ signaturesByType = null;
84
+
85
+ return Component;
86
+ }
87
+
88
+ function render(version, props) {
89
+ const Component = prepare(version);
90
+ act(() => {
91
+ ReactDOM.render(<Component {...props} />, container);
92
+ });
93
+ return Component;
94
+ }
95
+
96
+ function patch(version) {
97
+ // Will be filled in by __register__ calls in user code.
98
+ newFamilies = new Set();
99
+ updatedFamilies = new Set();
100
+ signaturesByType = new Map();
101
+ const Component = version();
102
+
103
+ // Fill in the signatures.
104
+ for (let family of newFamilies) {
105
+ const latestSignature = signaturesByType.get(family.currentType) || null;
106
+ family.currentSignature = latestSignature;
107
+ }
108
+ // Now that all registration and signatures are collected,
109
+ // find which registrations changed their signatures since last time.
110
+ const staleFamilies = new Set();
111
+ for (let family of updatedFamilies) {
112
+ const latestSignature = signaturesByType.get(family.currentType) || null;
113
+ if (family.currentSignature !== latestSignature) {
114
+ family.currentSignature = latestSignature;
115
+ staleFamilies.add(family);
116
+ }
117
+ }
118
+
119
+ performHotReload(staleFamilies);
120
+ newFamilies = null;
121
+ updatedFamilies = null;
122
+ signaturesByType = null;
123
+ return Component;
124
+ }
125
+
126
+ function __register__(type, id) {
127
+ if (familiesByType.has(type)) {
128
+ return;
129
+ }
130
+ let family = familiesByID.get(id);
131
+ let isNew = false;
132
+ if (family === undefined) {
133
+ isNew = true;
134
+ family = {currentType: type, currentSignature: null};
135
+ familiesByID.set(id, family);
136
+ }
137
+ const prevType = family.currentType;
138
+ if (isNew) {
139
+ // The first time a type is registered, we don't need
140
+ // any special reconciliation logic. So we won't add it to the map.
141
+ // Instead, this will happen the firt time it is edited.
142
+ newFamilies.add(family);
143
+ } else {
144
+ family.currentType = type;
145
+ // Point both previous and next types to this family.
146
+ familiesByType.set(prevType, family);
147
+ familiesByType.set(type, family);
148
+ updatedFamilies.add(family);
149
+ }
150
+
151
+ if (typeof type === 'object' && type !== null) {
152
+ switch (type.$$typeof) {
153
+ case Symbol.for('react.forward_ref'):
154
+ __register__(type.render, id + '$render');
155
+ break;
156
+ case Symbol.for('react.memo'):
157
+ __register__(type.type, id + '$type');
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ function __signature__(type, signature) {
164
+ signaturesByType.set(type, signature);
165
+ }
166
+
167
+ it('can preserve state for compatible types', () => {
168
+ if (__DEV__) {
169
+ let HelloV1 = render(() => {
170
+ function Hello() {
171
+ const [val, setVal] = React.useState(0);
172
+ return (
173
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
174
+ {val}
175
+ </p>
176
+ );
177
+ }
178
+ __register__(Hello, 'Hello');
179
+ return Hello;
180
+ });
181
+
182
+ // Bump the state before patching.
183
+ const el = container.firstChild;
184
+ expect(el.textContent).toBe('0');
185
+ expect(el.style.color).toBe('blue');
186
+ act(() => {
187
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
188
+ });
189
+ expect(el.textContent).toBe('1');
190
+
191
+ // Perform a hot update.
192
+ let HelloV2 = patch(() => {
193
+ function Hello() {
194
+ const [val, setVal] = React.useState(0);
195
+ return (
196
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
197
+ {val}
198
+ </p>
199
+ );
200
+ }
201
+ __register__(Hello, 'Hello');
202
+ return Hello;
203
+ });
204
+
205
+ // Assert the state was preserved but color changed.
206
+ expect(container.firstChild).toBe(el);
207
+ expect(el.textContent).toBe('1');
208
+ expect(el.style.color).toBe('red');
209
+
210
+ // Bump the state again.
211
+ act(() => {
212
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
213
+ });
214
+ expect(container.firstChild).toBe(el);
215
+ expect(el.textContent).toBe('2');
216
+ expect(el.style.color).toBe('red');
217
+
218
+ // Perform top-down renders with both fresh and stale types.
219
+ // Neither should change the state or color.
220
+ // They should always resolve to the latest version.
221
+ render(() => HelloV1);
222
+ render(() => HelloV2);
223
+ render(() => HelloV1);
224
+ expect(container.firstChild).toBe(el);
225
+ expect(el.textContent).toBe('2');
226
+ expect(el.style.color).toBe('red');
227
+
228
+ // Bump the state again.
229
+ act(() => {
230
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
231
+ });
232
+ expect(container.firstChild).toBe(el);
233
+ expect(el.textContent).toBe('3');
234
+ expect(el.style.color).toBe('red');
235
+
236
+ // Finally, a render with incompatible type should reset it.
237
+ render(() => {
238
+ function Hello() {
239
+ const [val, setVal] = React.useState(0);
240
+ return (
241
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
242
+ {val}
243
+ </p>
244
+ );
245
+ }
246
+ // No register call.
247
+ // This is considered a new type.
248
+ return Hello;
249
+ });
250
+ expect(container.firstChild).not.toBe(el);
251
+ const newEl = container.firstChild;
252
+ expect(newEl.textContent).toBe('0');
253
+ expect(newEl.style.color).toBe('blue');
254
+ }
255
+ });
256
+
257
+ it('can preserve state for forwardRef', () => {
258
+ if (__DEV__) {
259
+ let OuterV1 = render(() => {
260
+ function Hello() {
261
+ const [val, setVal] = React.useState(0);
262
+ return (
263
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
264
+ {val}
265
+ </p>
266
+ );
267
+ }
268
+ __register__(Hello, 'Hello');
269
+
270
+ const Outer = React.forwardRef(() => <Hello />);
271
+ __register__(Outer, 'Outer');
272
+ return Outer;
273
+ });
274
+
275
+ // Bump the state before patching.
276
+ const el = container.firstChild;
277
+ expect(el.textContent).toBe('0');
278
+ expect(el.style.color).toBe('blue');
279
+ act(() => {
280
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
281
+ });
282
+ expect(el.textContent).toBe('1');
283
+
284
+ // Perform a hot update.
285
+ let OuterV2 = patch(() => {
286
+ function Hello() {
287
+ const [val, setVal] = React.useState(0);
288
+ return (
289
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
290
+ {val}
291
+ </p>
292
+ );
293
+ }
294
+ __register__(Hello, 'Hello');
295
+
296
+ const Outer = React.forwardRef(() => <Hello />);
297
+ __register__(Outer, 'Outer');
298
+ return Outer;
299
+ });
300
+
301
+ // Assert the state was preserved but color changed.
302
+ expect(container.firstChild).toBe(el);
303
+ expect(el.textContent).toBe('1');
304
+ expect(el.style.color).toBe('red');
305
+
306
+ // Bump the state again.
307
+ act(() => {
308
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
309
+ });
310
+ expect(container.firstChild).toBe(el);
311
+ expect(el.textContent).toBe('2');
312
+ expect(el.style.color).toBe('red');
313
+
314
+ // Perform top-down renders with both fresh and stale types.
315
+ // Neither should change the state or color.
316
+ // They should always resolve to the latest version.
317
+ render(() => OuterV1);
318
+ render(() => OuterV2);
319
+ render(() => OuterV1);
320
+ expect(container.firstChild).toBe(el);
321
+ expect(el.textContent).toBe('2');
322
+ expect(el.style.color).toBe('red');
323
+
324
+ // Finally, a render with incompatible type should reset it.
325
+ render(() => {
326
+ function Hello() {
327
+ const [val, setVal] = React.useState(0);
328
+ return (
329
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
330
+ {val}
331
+ </p>
332
+ );
333
+ }
334
+ __register__(Hello, 'Hello');
335
+
336
+ // Note: no forwardRef wrapper this time.
337
+ return Hello;
338
+ });
339
+
340
+ expect(container.firstChild).not.toBe(el);
341
+ const newEl = container.firstChild;
342
+ expect(newEl.textContent).toBe('0');
343
+ expect(newEl.style.color).toBe('blue');
344
+ }
345
+ });
346
+
347
+ it('should not consider two forwardRefs around the same type to be equivalent', () => {
348
+ if (__DEV__) {
349
+ let ParentV1 = render(
350
+ () => {
351
+ function Hello() {
352
+ const [val, setVal] = React.useState(0);
353
+ return (
354
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
355
+ {val}
356
+ </p>
357
+ );
358
+ }
359
+ __register__(Hello, 'Hello');
360
+
361
+ function renderInner() {
362
+ return <Hello />;
363
+ }
364
+ // Both of these are wrappers around the same inner function.
365
+ // They should be treated as distinct types across reloads.
366
+ let ForwardRefA = React.forwardRef(renderInner);
367
+ __register__(ForwardRefA, 'ForwardRefA');
368
+ let ForwardRefB = React.forwardRef(renderInner);
369
+ __register__(ForwardRefB, 'ForwardRefB');
370
+
371
+ function Parent({cond}) {
372
+ return cond ? <ForwardRefA /> : <ForwardRefB />;
373
+ }
374
+ __register__(Parent, 'Parent');
375
+
376
+ return Parent;
377
+ },
378
+ {cond: true},
379
+ );
380
+
381
+ // Bump the state before switching up types.
382
+ let el = container.firstChild;
383
+ expect(el.textContent).toBe('0');
384
+ expect(el.style.color).toBe('blue');
385
+ act(() => {
386
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
387
+ });
388
+ expect(el.textContent).toBe('1');
389
+
390
+ // Switching up the inner types should reset the state.
391
+ render(() => ParentV1, {cond: false});
392
+ expect(el).not.toBe(container.firstChild);
393
+ el = container.firstChild;
394
+ expect(el.textContent).toBe('0');
395
+ expect(el.style.color).toBe('blue');
396
+
397
+ act(() => {
398
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
399
+ });
400
+ expect(el.textContent).toBe('1');
401
+
402
+ // Switch them up back again.
403
+ render(() => ParentV1, {cond: true});
404
+ expect(el).not.toBe(container.firstChild);
405
+ el = container.firstChild;
406
+ expect(el.textContent).toBe('0');
407
+ expect(el.style.color).toBe('blue');
408
+
409
+ // Now bump up the state to prepare for patching.
410
+ act(() => {
411
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
412
+ });
413
+ expect(el.textContent).toBe('1');
414
+
415
+ // Patch to change the color.
416
+ let ParentV2 = patch(() => {
417
+ function Hello() {
418
+ const [val, setVal] = React.useState(0);
419
+ return (
420
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
421
+ {val}
422
+ </p>
423
+ );
424
+ }
425
+ __register__(Hello, 'Hello');
426
+
427
+ function renderInner() {
428
+ return <Hello />;
429
+ }
430
+ // Both of these are wrappers around the same inner function.
431
+ // They should be treated as distinct types across reloads.
432
+ let ForwardRefA = React.forwardRef(renderInner);
433
+ __register__(ForwardRefA, 'ForwardRefA');
434
+ let ForwardRefB = React.forwardRef(renderInner);
435
+ __register__(ForwardRefB, 'ForwardRefB');
436
+
437
+ function Parent({cond}) {
438
+ return cond ? <ForwardRefA /> : <ForwardRefB />;
439
+ }
440
+ __register__(Parent, 'Parent');
441
+
442
+ return Parent;
443
+ });
444
+
445
+ // The state should be intact; the color should change.
446
+ expect(el).toBe(container.firstChild);
447
+ expect(el.textContent).toBe('1');
448
+ expect(el.style.color).toBe('red');
449
+
450
+ // Switching up the condition should still reset the state.
451
+ render(() => ParentV2, {cond: false});
452
+ expect(el).not.toBe(container.firstChild);
453
+ el = container.firstChild;
454
+ expect(el.textContent).toBe('0');
455
+ expect(el.style.color).toBe('red');
456
+
457
+ // Now bump up the state to prepare for top-level renders.
458
+ act(() => {
459
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
460
+ });
461
+ expect(el).toBe(container.firstChild);
462
+ expect(el.textContent).toBe('1');
463
+ expect(el.style.color).toBe('red');
464
+
465
+ // Finally, verify using top-level render with stale type keeps state.
466
+ render(() => ParentV1);
467
+ render(() => ParentV2);
468
+ render(() => ParentV1);
469
+ expect(container.firstChild).toBe(el);
470
+ expect(el.textContent).toBe('1');
471
+ expect(el.style.color).toBe('red');
472
+ }
473
+ });
474
+
475
+ it('can update forwardRef render function with its wrapper', () => {
476
+ if (__DEV__) {
477
+ render(() => {
478
+ function Hello({color}) {
479
+ const [val, setVal] = React.useState(0);
480
+ return (
481
+ <p style={{color}} onClick={() => setVal(val + 1)}>
482
+ {val}
483
+ </p>
484
+ );
485
+ }
486
+ __register__(Hello, 'Hello');
487
+
488
+ const Outer = React.forwardRef(() => <Hello color="blue" />);
489
+ __register__(Outer, 'Outer');
490
+ return Outer;
491
+ });
492
+
493
+ // Bump the state before patching.
494
+ const el = container.firstChild;
495
+ expect(el.textContent).toBe('0');
496
+ expect(el.style.color).toBe('blue');
497
+ act(() => {
498
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
499
+ });
500
+ expect(el.textContent).toBe('1');
501
+
502
+ // Perform a hot update.
503
+ patch(() => {
504
+ function Hello({color}) {
505
+ const [val, setVal] = React.useState(0);
506
+ return (
507
+ <p style={{color}} onClick={() => setVal(val + 1)}>
508
+ {val}
509
+ </p>
510
+ );
511
+ }
512
+ __register__(Hello, 'Hello');
513
+
514
+ const Outer = React.forwardRef(() => <Hello color="red" />);
515
+ __register__(Outer, 'Outer');
516
+ return Outer;
517
+ });
518
+
519
+ // Assert the state was preserved but color changed.
520
+ expect(container.firstChild).toBe(el);
521
+ expect(el.textContent).toBe('1');
522
+ expect(el.style.color).toBe('red');
523
+ }
524
+ });
525
+
526
+ it('can update forwardRef render function in isolation', () => {
527
+ if (__DEV__) {
528
+ render(() => {
529
+ function Hello({color}) {
530
+ const [val, setVal] = React.useState(0);
531
+ return (
532
+ <p style={{color}} onClick={() => setVal(val + 1)}>
533
+ {val}
534
+ </p>
535
+ );
536
+ }
537
+ __register__(Hello, 'Hello');
538
+
539
+ function renderHello() {
540
+ return <Hello color="blue" />;
541
+ }
542
+ __register__(renderHello, 'renderHello');
543
+
544
+ return React.forwardRef(renderHello);
545
+ });
546
+
547
+ // Bump the state before patching.
548
+ const el = container.firstChild;
549
+ expect(el.textContent).toBe('0');
550
+ expect(el.style.color).toBe('blue');
551
+ act(() => {
552
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
553
+ });
554
+ expect(el.textContent).toBe('1');
555
+
556
+ // Perform a hot update of just the rendering function.
557
+ patch(() => {
558
+ function Hello({color}) {
559
+ const [val, setVal] = React.useState(0);
560
+ return (
561
+ <p style={{color}} onClick={() => setVal(val + 1)}>
562
+ {val}
563
+ </p>
564
+ );
565
+ }
566
+ __register__(Hello, 'Hello');
567
+
568
+ function renderHello() {
569
+ return <Hello color="red" />;
570
+ }
571
+ __register__(renderHello, 'renderHello');
572
+
573
+ // Not updating the wrapper.
574
+ });
575
+
576
+ // Assert the state was preserved but color changed.
577
+ expect(container.firstChild).toBe(el);
578
+ expect(el.textContent).toBe('1');
579
+ expect(el.style.color).toBe('red');
580
+ }
581
+ });
582
+
583
+ it('can preserve state for simple memo', () => {
584
+ if (__DEV__) {
585
+ let OuterV1 = render(() => {
586
+ function Hello() {
587
+ const [val, setVal] = React.useState(0);
588
+ return (
589
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
590
+ {val}
591
+ </p>
592
+ );
593
+ }
594
+ __register__(Hello, 'Hello');
595
+
596
+ const Outer = React.memo(Hello);
597
+ __register__(Outer, 'Outer');
598
+ return Outer;
599
+ });
600
+
601
+ // Bump the state before patching.
602
+ const el = container.firstChild;
603
+ expect(el.textContent).toBe('0');
604
+ expect(el.style.color).toBe('blue');
605
+ act(() => {
606
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
607
+ });
608
+ expect(el.textContent).toBe('1');
609
+
610
+ // Perform a hot update.
611
+ let OuterV2 = patch(() => {
612
+ function Hello() {
613
+ const [val, setVal] = React.useState(0);
614
+ return (
615
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
616
+ {val}
617
+ </p>
618
+ );
619
+ }
620
+ __register__(Hello, 'Hello');
621
+
622
+ const Outer = React.memo(Hello);
623
+ __register__(Outer, 'Outer');
624
+ return Outer;
625
+ });
626
+
627
+ // Assert the state was preserved but color changed.
628
+ expect(container.firstChild).toBe(el);
629
+ expect(el.textContent).toBe('1');
630
+ expect(el.style.color).toBe('red');
631
+
632
+ // Bump the state again.
633
+ act(() => {
634
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
635
+ });
636
+ expect(container.firstChild).toBe(el);
637
+ expect(el.textContent).toBe('2');
638
+ expect(el.style.color).toBe('red');
639
+
640
+ // Perform top-down renders with both fresh and stale types.
641
+ // Neither should change the state or color.
642
+ // They should always resolve to the latest version.
643
+ render(() => OuterV1);
644
+ render(() => OuterV2);
645
+ render(() => OuterV1);
646
+ expect(container.firstChild).toBe(el);
647
+ expect(el.textContent).toBe('2');
648
+ expect(el.style.color).toBe('red');
649
+
650
+ // Finally, a render with incompatible type should reset it.
651
+ render(() => {
652
+ function Hello() {
653
+ const [val, setVal] = React.useState(0);
654
+ return (
655
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
656
+ {val}
657
+ </p>
658
+ );
659
+ }
660
+ __register__(Hello, 'Hello');
661
+
662
+ // Note: no wrapper this time.
663
+ return Hello;
664
+ });
665
+
666
+ expect(container.firstChild).not.toBe(el);
667
+ const newEl = container.firstChild;
668
+ expect(newEl.textContent).toBe('0');
669
+ expect(newEl.style.color).toBe('blue');
670
+ }
671
+ });
672
+
673
+ it('can preserve state for memo with custom comparison', () => {
674
+ if (__DEV__) {
675
+ let OuterV1 = render(() => {
676
+ function Hello() {
677
+ const [val, setVal] = React.useState(0);
678
+ return (
679
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
680
+ {val}
681
+ </p>
682
+ );
683
+ }
684
+
685
+ const Outer = React.memo(Hello, () => true);
686
+ __register__(Outer, 'Outer');
687
+ return Outer;
688
+ });
689
+
690
+ // Bump the state before patching.
691
+ const el = container.firstChild;
692
+ expect(el.textContent).toBe('0');
693
+ expect(el.style.color).toBe('blue');
694
+ act(() => {
695
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
696
+ });
697
+ expect(el.textContent).toBe('1');
698
+
699
+ // Perform a hot update.
700
+ let OuterV2 = patch(() => {
701
+ function Hello() {
702
+ const [val, setVal] = React.useState(0);
703
+ return (
704
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
705
+ {val}
706
+ </p>
707
+ );
708
+ }
709
+
710
+ const Outer = React.memo(Hello, () => true);
711
+ __register__(Outer, 'Outer');
712
+ return Outer;
713
+ });
714
+
715
+ // Assert the state was preserved but color changed.
716
+ expect(container.firstChild).toBe(el);
717
+ expect(el.textContent).toBe('1');
718
+ expect(el.style.color).toBe('red');
719
+
720
+ // Bump the state again.
721
+ act(() => {
722
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
723
+ });
724
+ expect(container.firstChild).toBe(el);
725
+ expect(el.textContent).toBe('2');
726
+ expect(el.style.color).toBe('red');
727
+
728
+ // Perform top-down renders with both fresh and stale types.
729
+ // Neither should change the state or color.
730
+ // They should always resolve to the latest version.
731
+ render(() => OuterV1);
732
+ render(() => OuterV2);
733
+ render(() => OuterV1);
734
+ expect(container.firstChild).toBe(el);
735
+ expect(el.textContent).toBe('2');
736
+ expect(el.style.color).toBe('red');
737
+
738
+ // Finally, a render with incompatible type should reset it.
739
+ render(() => {
740
+ function Hello() {
741
+ const [val, setVal] = React.useState(0);
742
+ return (
743
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
744
+ {val}
745
+ </p>
746
+ );
747
+ }
748
+ __register__(Hello, 'Hello');
749
+
750
+ // Note: no wrapper this time.
751
+ return Hello;
752
+ });
753
+
754
+ expect(container.firstChild).not.toBe(el);
755
+ const newEl = container.firstChild;
756
+ expect(newEl.textContent).toBe('0');
757
+ expect(newEl.style.color).toBe('blue');
758
+ }
759
+ });
760
+
761
+ it('can update simple memo function in isolation', () => {
762
+ if (__DEV__) {
763
+ render(() => {
764
+ function Hello() {
765
+ const [val, setVal] = React.useState(0);
766
+ return (
767
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
768
+ {val}
769
+ </p>
770
+ );
771
+ }
772
+ __register__(Hello, 'Hello');
773
+
774
+ return React.memo(Hello);
775
+ });
776
+
777
+ // Bump the state before patching.
778
+ const el = container.firstChild;
779
+ expect(el.textContent).toBe('0');
780
+ expect(el.style.color).toBe('blue');
781
+ act(() => {
782
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
783
+ });
784
+ expect(el.textContent).toBe('1');
785
+
786
+ // Perform a hot update of just the rendering function.
787
+ patch(() => {
788
+ function Hello() {
789
+ const [val, setVal] = React.useState(0);
790
+ return (
791
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
792
+ {val}
793
+ </p>
794
+ );
795
+ }
796
+ __register__(Hello, 'Hello');
797
+
798
+ // Not updating the wrapper.
799
+ });
800
+
801
+ // Assert the state was preserved but color changed.
802
+ expect(container.firstChild).toBe(el);
803
+ expect(el.textContent).toBe('1');
804
+ expect(el.style.color).toBe('red');
805
+ }
806
+ });
807
+
808
+ it('can preserve state for memo(forwardRef)', () => {
809
+ if (__DEV__) {
810
+ let OuterV1 = render(() => {
811
+ function Hello() {
812
+ const [val, setVal] = React.useState(0);
813
+ return (
814
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
815
+ {val}
816
+ </p>
817
+ );
818
+ }
819
+ __register__(Hello, 'Hello');
820
+
821
+ const Outer = React.memo(React.forwardRef(() => <Hello />));
822
+ __register__(Outer, 'Outer');
823
+ return Outer;
824
+ });
825
+
826
+ // Bump the state before patching.
827
+ const el = container.firstChild;
828
+ expect(el.textContent).toBe('0');
829
+ expect(el.style.color).toBe('blue');
830
+ act(() => {
831
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
832
+ });
833
+ expect(el.textContent).toBe('1');
834
+
835
+ // Perform a hot update.
836
+ let OuterV2 = patch(() => {
837
+ function Hello() {
838
+ const [val, setVal] = React.useState(0);
839
+ return (
840
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
841
+ {val}
842
+ </p>
843
+ );
844
+ }
845
+ __register__(Hello, 'Hello');
846
+
847
+ const Outer = React.memo(React.forwardRef(() => <Hello />));
848
+ __register__(Outer, 'Outer');
849
+ return Outer;
850
+ });
851
+
852
+ // Assert the state was preserved but color changed.
853
+ expect(container.firstChild).toBe(el);
854
+ expect(el.textContent).toBe('1');
855
+ expect(el.style.color).toBe('red');
856
+
857
+ // Bump the state again.
858
+ act(() => {
859
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
860
+ });
861
+ expect(container.firstChild).toBe(el);
862
+ expect(el.textContent).toBe('2');
863
+ expect(el.style.color).toBe('red');
864
+
865
+ // Perform top-down renders with both fresh and stale types.
866
+ // Neither should change the state or color.
867
+ // They should always resolve to the latest version.
868
+ render(() => OuterV1);
869
+ render(() => OuterV2);
870
+ render(() => OuterV1);
871
+ expect(container.firstChild).toBe(el);
872
+ expect(el.textContent).toBe('2');
873
+ expect(el.style.color).toBe('red');
874
+
875
+ // Finally, a render with incompatible type should reset it.
876
+ render(() => {
877
+ function Hello() {
878
+ const [val, setVal] = React.useState(0);
879
+ return (
880
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
881
+ {val}
882
+ </p>
883
+ );
884
+ }
885
+ __register__(Hello, 'Hello');
886
+
887
+ // Note: no wrapper this time.
888
+ return Hello;
889
+ });
890
+
891
+ expect(container.firstChild).not.toBe(el);
892
+ const newEl = container.firstChild;
893
+ expect(newEl.textContent).toBe('0');
894
+ expect(newEl.style.color).toBe('blue');
895
+ }
896
+ });
897
+
898
+ it('does not re-render ancestor components unnecessarily during a hot update', () => {
899
+ if (__DEV__) {
900
+ let appRenders = 0;
901
+
902
+ render(() => {
903
+ function Hello() {
904
+ const [val, setVal] = React.useState(0);
905
+ return (
906
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
907
+ {val}
908
+ </p>
909
+ );
910
+ }
911
+ __register__(Hello, 'Hello');
912
+ function App() {
913
+ appRenders++;
914
+ return <Hello />;
915
+ }
916
+ __register__(App, 'App');
917
+ return App;
918
+ });
919
+
920
+ expect(appRenders).toBe(1);
921
+
922
+ // Bump the state before patching.
923
+ const el = container.firstChild;
924
+ expect(el.textContent).toBe('0');
925
+ expect(el.style.color).toBe('blue');
926
+ act(() => {
927
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
928
+ });
929
+ expect(el.textContent).toBe('1');
930
+
931
+ // No re-renders from the top.
932
+ expect(appRenders).toBe(1);
933
+
934
+ // Perform a hot update for Hello only.
935
+ patch(() => {
936
+ function Hello() {
937
+ const [val, setVal] = React.useState(0);
938
+ return (
939
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
940
+ {val}
941
+ </p>
942
+ );
943
+ }
944
+ __register__(Hello, 'Hello');
945
+ });
946
+
947
+ // Assert the state was preserved but color changed.
948
+ expect(container.firstChild).toBe(el);
949
+ expect(el.textContent).toBe('1');
950
+ expect(el.style.color).toBe('red');
951
+
952
+ // Still no re-renders from the top.
953
+ expect(appRenders).toBe(1);
954
+
955
+ // Bump the state.
956
+ act(() => {
957
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
958
+ });
959
+ expect(el.textContent).toBe('2');
960
+
961
+ // Still no re-renders from the top.
962
+ expect(appRenders).toBe(1);
963
+ }
964
+ });
965
+
966
+ it('can force remount by changing signature', () => {
967
+ if (__DEV__) {
968
+ let HelloV1 = render(() => {
969
+ function Hello() {
970
+ const [val, setVal] = React.useState(0);
971
+ return (
972
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
973
+ {val}
974
+ </p>
975
+ );
976
+ }
977
+ __register__(Hello, 'Hello');
978
+ // When this changes, we'll expect a remount:
979
+ __signature__(Hello, '1');
980
+ return Hello;
981
+ });
982
+
983
+ // Bump the state before patching.
984
+ const el = container.firstChild;
985
+ expect(el.textContent).toBe('0');
986
+ expect(el.style.color).toBe('blue');
987
+ act(() => {
988
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
989
+ });
990
+ expect(el.textContent).toBe('1');
991
+
992
+ // Perform a hot update.
993
+ let HelloV2 = patch(() => {
994
+ function Hello() {
995
+ const [val, setVal] = React.useState(0);
996
+ return (
997
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
998
+ {val}
999
+ </p>
1000
+ );
1001
+ }
1002
+ __register__(Hello, 'Hello');
1003
+ // The signature hasn't changed since the last time:
1004
+ __signature__(Hello, '1');
1005
+ return Hello;
1006
+ });
1007
+
1008
+ // Assert the state was preserved but color changed.
1009
+ expect(container.firstChild).toBe(el);
1010
+ expect(el.textContent).toBe('1');
1011
+ expect(el.style.color).toBe('red');
1012
+
1013
+ // Perform a hot update.
1014
+ let HelloV3 = patch(() => {
1015
+ function Hello() {
1016
+ const [val, setVal] = React.useState(0);
1017
+ return (
1018
+ <p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
1019
+ {val}
1020
+ </p>
1021
+ );
1022
+ }
1023
+ // We're changing the signature now so it will remount:
1024
+ __register__(Hello, 'Hello');
1025
+ __signature__(Hello, '2');
1026
+ return Hello;
1027
+ });
1028
+
1029
+ // Expect a remount.
1030
+ expect(container.firstChild).not.toBe(el);
1031
+ const newEl = container.firstChild;
1032
+ expect(newEl.textContent).toBe('0');
1033
+ expect(newEl.style.color).toBe('yellow');
1034
+
1035
+ // Bump state again.
1036
+ act(() => {
1037
+ newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1038
+ });
1039
+ expect(newEl.textContent).toBe('1');
1040
+ expect(newEl.style.color).toBe('yellow');
1041
+
1042
+ // Perform top-down renders with both fresh and stale types.
1043
+ // Neither should change the state or color.
1044
+ // They should always resolve to the latest version.
1045
+ render(() => HelloV1);
1046
+ render(() => HelloV2);
1047
+ render(() => HelloV3);
1048
+ render(() => HelloV2);
1049
+ render(() => HelloV1);
1050
+ expect(container.firstChild).toBe(newEl);
1051
+ expect(newEl.textContent).toBe('1');
1052
+ expect(newEl.style.color).toBe('yellow');
1053
+
1054
+ // Verify we can patch again while preserving the signature.
1055
+ patch(() => {
1056
+ function Hello() {
1057
+ const [val, setVal] = React.useState(0);
1058
+ return (
1059
+ <p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
1060
+ {val}
1061
+ </p>
1062
+ );
1063
+ }
1064
+ // Same signature as last time.
1065
+ __register__(Hello, 'Hello');
1066
+ __signature__(Hello, '2');
1067
+ return Hello;
1068
+ });
1069
+
1070
+ expect(container.firstChild).toBe(newEl);
1071
+ expect(newEl.textContent).toBe('1');
1072
+ expect(newEl.style.color).toBe('purple');
1073
+
1074
+ // Check removing the signature also causes a remount.
1075
+ patch(() => {
1076
+ function Hello() {
1077
+ const [val, setVal] = React.useState(0);
1078
+ return (
1079
+ <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
1080
+ {val}
1081
+ </p>
1082
+ );
1083
+ }
1084
+ // No signature this time.
1085
+ __register__(Hello, 'Hello');
1086
+ return Hello;
1087
+ });
1088
+
1089
+ // Expect a remount.
1090
+ expect(container.firstChild).not.toBe(newEl);
1091
+ const finalEl = container.firstChild;
1092
+ expect(finalEl.textContent).toBe('0');
1093
+ expect(finalEl.style.color).toBe('orange');
1094
+ }
1095
+ });
1096
+
1097
+ it('keeps a valid tree when forcing remount', () => {
1098
+ if (__DEV__) {
1099
+ let HelloV1 = prepare(() => {
1100
+ function Hello() {
1101
+ return null;
1102
+ }
1103
+ __register__(Hello, 'Hello');
1104
+ __signature__(Hello, '1');
1105
+ return Hello;
1106
+ });
1107
+
1108
+ const Bailout = React.memo(({children}) => {
1109
+ return children;
1110
+ });
1111
+
1112
+ // Each of those renders three instances of HelloV1,
1113
+ // but in different ways.
1114
+ let trees = [
1115
+ <div>
1116
+ <HelloV1 />
1117
+ <div>
1118
+ <HelloV1 />
1119
+ <Bailout>
1120
+ <HelloV1 />
1121
+ </Bailout>
1122
+ </div>
1123
+ </div>,
1124
+ <div>
1125
+ <div>
1126
+ <HelloV1>
1127
+ <HelloV1 />
1128
+ </HelloV1>
1129
+ <HelloV1 />
1130
+ </div>
1131
+ </div>,
1132
+ <div>
1133
+ <span />
1134
+ <HelloV1 />
1135
+ <HelloV1 />
1136
+ <HelloV1 />
1137
+ </div>,
1138
+ <div>
1139
+ <HelloV1 />
1140
+ <span />
1141
+ <HelloV1 />
1142
+ <HelloV1 />
1143
+ </div>,
1144
+ <div>
1145
+ <div>foo</div>
1146
+ <HelloV1 />
1147
+ <div>
1148
+ <HelloV1 />
1149
+ </div>
1150
+ <HelloV1 />
1151
+ <span />
1152
+ </div>,
1153
+ <div>
1154
+ <HelloV1>
1155
+ <span />
1156
+ Hello
1157
+ <span />
1158
+ </HelloV1>,
1159
+ <HelloV1>
1160
+ <React.Fragment>
1161
+ <HelloV1 />
1162
+ </React.Fragment>
1163
+ </HelloV1>,
1164
+ </div>,
1165
+ <HelloV1>
1166
+ <HelloV1>
1167
+ <Bailout>
1168
+ <span />
1169
+ <HelloV1>
1170
+ <span />
1171
+ </HelloV1>
1172
+ <span />
1173
+ </Bailout>
1174
+ </HelloV1>
1175
+ </HelloV1>,
1176
+ <div>
1177
+ <span />
1178
+ <HelloV1 key="0" />
1179
+ <HelloV1 key="1" />
1180
+ <HelloV1 key="2" />
1181
+ <span />
1182
+ </div>,
1183
+ <div>
1184
+ <span />
1185
+ {null}
1186
+ <HelloV1 key="1" />
1187
+ {null}
1188
+ <HelloV1 />
1189
+ <HelloV1 />
1190
+ <span />
1191
+ </div>,
1192
+ <div>
1193
+ <HelloV1 key="2" />
1194
+ <span />
1195
+ <HelloV1 key="0" />
1196
+ <span />
1197
+ <HelloV1 key="1" />
1198
+ </div>,
1199
+ <div>
1200
+ {[[<HelloV1 key="2" />]]}
1201
+ <span>
1202
+ <HelloV1 key="0" />
1203
+ {[null]}
1204
+ <HelloV1 key="1" />
1205
+ </span>
1206
+ </div>,
1207
+ <div>
1208
+ {['foo', <HelloV1 key="hi" />, null, <HelloV1 key="2" />]}
1209
+ <span>
1210
+ {[null]}
1211
+ <HelloV1 key="x" />
1212
+ </span>
1213
+ </div>,
1214
+ <HelloV1>
1215
+ <HelloV1>
1216
+ <span />
1217
+ <Bailout>
1218
+ <HelloV1>hi</HelloV1>
1219
+ <span />
1220
+ </Bailout>
1221
+ </HelloV1>
1222
+ </HelloV1>,
1223
+ ];
1224
+
1225
+ // First, check that each tree handles remounts in isolation.
1226
+ ReactDOM.render(null, container);
1227
+ for (let i = 0; i < trees.length; i++) {
1228
+ runRemountingStressTest(trees[i]);
1229
+ }
1230
+
1231
+ // Then check that each tree is resilient to updates from another tree.
1232
+ for (let i = 0; i < trees.length; i++) {
1233
+ for (let j = 0; j < trees.length; j++) {
1234
+ ReactDOM.render(null, container);
1235
+ // Intentionally don't clean up between the tests:
1236
+ runRemountingStressTest(trees[i]);
1237
+ runRemountingStressTest(trees[j]);
1238
+ runRemountingStressTest(trees[i]);
1239
+ }
1240
+ }
1241
+ }
1242
+ });
1243
+
1244
+ function runRemountingStressTest(tree) {
1245
+ patch(() => {
1246
+ function Hello({children}) {
1247
+ return <section data-color="blue">{children}</section>;
1248
+ }
1249
+ __register__(Hello, 'Hello');
1250
+ __signature__(Hello, '1');
1251
+ return Hello;
1252
+ });
1253
+
1254
+ ReactDOM.render(tree, container);
1255
+ const elements = container.querySelectorAll('section');
1256
+ // Each tree above products exactly three <section> elements:
1257
+ expect(elements.length).toBe(3);
1258
+ elements.forEach(el => {
1259
+ expect(el.dataset.color).toBe('blue');
1260
+ });
1261
+
1262
+ // Patch color without changing the signature.
1263
+ patch(() => {
1264
+ function Hello({children}) {
1265
+ return <section data-color="red">{children}</section>;
1266
+ }
1267
+ __register__(Hello, 'Hello');
1268
+ __signature__(Hello, '1');
1269
+ return Hello;
1270
+ });
1271
+
1272
+ const elementsAfterPatch = container.querySelectorAll('section');
1273
+ expect(elementsAfterPatch.length).toBe(3);
1274
+ elementsAfterPatch.forEach((el, index) => {
1275
+ // The signature hasn't changed so we expect DOM nodes to stay the same.
1276
+ expect(el).toBe(elements[index]);
1277
+ // However, the color should have changed:
1278
+ expect(el.dataset.color).toBe('red');
1279
+ });
1280
+
1281
+ // Patch color *and* change the signature.
1282
+ patch(() => {
1283
+ function Hello({children}) {
1284
+ return <section data-color="orange">{children}</section>;
1285
+ }
1286
+ __register__(Hello, 'Hello');
1287
+ __signature__(Hello, '2'); // Remount
1288
+ return Hello;
1289
+ });
1290
+
1291
+ const elementsAfterRemount = container.querySelectorAll('section');
1292
+ expect(elementsAfterRemount.length).toBe(3);
1293
+ elementsAfterRemount.forEach((el, index) => {
1294
+ // The signature changed so we expect DOM nodes to be different.
1295
+ expect(el).not.toBe(elements[index]);
1296
+ // They should all be using the new color:
1297
+ expect(el.dataset.color).toBe('orange');
1298
+ });
1299
+
1300
+ // Now patch color but *don't* change the signature.
1301
+ patch(() => {
1302
+ function Hello({children}) {
1303
+ return <section data-color="black">{children}</section>;
1304
+ }
1305
+ __register__(Hello, 'Hello');
1306
+ __signature__(Hello, '2'); // Same signature as before
1307
+ return Hello;
1308
+ });
1309
+
1310
+ expect(container.querySelectorAll('section').length).toBe(3);
1311
+ container.querySelectorAll('section').forEach((el, index) => {
1312
+ // The signature didn't change so DOM nodes should stay the same.
1313
+ expect(el).toBe(elementsAfterRemount[index]);
1314
+ // They should all be using the new color:
1315
+ expect(el.dataset.color).toBe('black');
1316
+ });
1317
+
1318
+ // Do another render just in case.
1319
+ ReactDOM.render(tree, container);
1320
+ expect(container.querySelectorAll('section').length).toBe(3);
1321
+ container.querySelectorAll('section').forEach((el, index) => {
1322
+ expect(el).toBe(elementsAfterRemount[index]);
1323
+ expect(el.dataset.color).toBe('black');
1324
+ });
1325
+ }
1326
+
1327
+ it('can remount on signature change within a <root> wrapper', () => {
1328
+ if (__DEV__) {
1329
+ testRemountingWithWrapper(Hello => Hello);
1330
+ }
1331
+ });
1332
+
1333
+ it('can remount on signature change within a simple memo wrapper', () => {
1334
+ if (__DEV__) {
1335
+ testRemountingWithWrapper(Hello => React.memo(Hello));
1336
+ }
1337
+ });
1338
+
1339
+ it('can remount on signature change within forwardRef', () => {
1340
+ if (__DEV__) {
1341
+ testRemountingWithWrapper(Hello => React.forwardRef(Hello));
1342
+ }
1343
+ });
1344
+
1345
+ it('can remount on signature change within forwardRef render function', () => {
1346
+ if (__DEV__) {
1347
+ testRemountingWithWrapper(Hello => React.forwardRef(() => <Hello />));
1348
+ }
1349
+ });
1350
+
1351
+ it('can remount on signature change within nested memo', () => {
1352
+ if (__DEV__) {
1353
+ testRemountingWithWrapper(Hello =>
1354
+ React.memo(React.memo(React.memo(Hello))),
1355
+ );
1356
+ }
1357
+ });
1358
+
1359
+ it('can remount on signature change within a memo wrapper and custom comparison', () => {
1360
+ if (__DEV__) {
1361
+ testRemountingWithWrapper(Hello => React.memo(Hello, () => true));
1362
+ }
1363
+ });
1364
+
1365
+ it('can remount on signature change within a class', () => {
1366
+ if (__DEV__) {
1367
+ testRemountingWithWrapper(Hello => {
1368
+ const child = <Hello />;
1369
+ return class Wrapper extends React.PureComponent {
1370
+ render() {
1371
+ return child;
1372
+ }
1373
+ };
1374
+ });
1375
+ }
1376
+ });
1377
+
1378
+ it('can remount on signature change within a context provider', () => {
1379
+ if (__DEV__) {
1380
+ testRemountingWithWrapper(Hello => {
1381
+ const Context = React.createContext();
1382
+ const child = (
1383
+ <Context.Provider value="constant">
1384
+ <Hello />
1385
+ </Context.Provider>
1386
+ );
1387
+ return function Wrapper() {
1388
+ return child;
1389
+ };
1390
+ });
1391
+ }
1392
+ });
1393
+
1394
+ it('can remount on signature change within a context consumer', () => {
1395
+ if (__DEV__) {
1396
+ testRemountingWithWrapper(Hello => {
1397
+ const Context = React.createContext();
1398
+ const child = <Context.Consumer>{() => <Hello />}</Context.Consumer>;
1399
+ return function Wrapper() {
1400
+ return child;
1401
+ };
1402
+ });
1403
+ }
1404
+ });
1405
+
1406
+ it('can remount on signature change within a suspense node', () => {
1407
+ if (__DEV__) {
1408
+ testRemountingWithWrapper(Hello => {
1409
+ // TODO: we'll probably want to test fallback trees too.
1410
+ const child = (
1411
+ <React.Suspense>
1412
+ <Hello />
1413
+ </React.Suspense>
1414
+ );
1415
+ return function Wrapper() {
1416
+ return child;
1417
+ };
1418
+ });
1419
+ }
1420
+ });
1421
+
1422
+ it('can remount on signature change within a mode node', () => {
1423
+ if (__DEV__) {
1424
+ testRemountingWithWrapper(Hello => {
1425
+ const child = (
1426
+ <React.StrictMode>
1427
+ <Hello />
1428
+ </React.StrictMode>
1429
+ );
1430
+ return function Wrapper() {
1431
+ return child;
1432
+ };
1433
+ });
1434
+ }
1435
+ });
1436
+
1437
+ it('can remount on signature change within a fragment node', () => {
1438
+ if (__DEV__) {
1439
+ testRemountingWithWrapper(Hello => {
1440
+ const child = (
1441
+ <React.Fragment>
1442
+ <Hello />
1443
+ </React.Fragment>
1444
+ );
1445
+ return function Wrapper() {
1446
+ return child;
1447
+ };
1448
+ });
1449
+ }
1450
+ });
1451
+
1452
+ it('can remount on signature change within multiple siblings', () => {
1453
+ if (__DEV__) {
1454
+ testRemountingWithWrapper(Hello => {
1455
+ const child = (
1456
+ <React.Fragment>
1457
+ <React.Fragment>
1458
+ <React.Fragment />
1459
+ </React.Fragment>
1460
+ <Hello />
1461
+ <React.Fragment />
1462
+ </React.Fragment>
1463
+ );
1464
+ return function Wrapper() {
1465
+ return child;
1466
+ };
1467
+ });
1468
+ }
1469
+ });
1470
+
1471
+ it('can remount on signature change within a profiler node', () => {
1472
+ if (__DEV__) {
1473
+ testRemountingWithWrapper(Hello => {
1474
+ const child = <Hello />;
1475
+ return function Wrapper() {
1476
+ return (
1477
+ <React.Profiler onRender={() => {}} id="foo">
1478
+ {child}
1479
+ </React.Profiler>
1480
+ );
1481
+ };
1482
+ });
1483
+ }
1484
+ });
1485
+
1486
+ function testRemountingWithWrapper(wrap) {
1487
+ render(() => {
1488
+ function Hello() {
1489
+ const [val, setVal] = React.useState(0);
1490
+ return (
1491
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1492
+ {val}
1493
+ </p>
1494
+ );
1495
+ }
1496
+ __register__(Hello, 'Hello');
1497
+ // When this changes, we'll expect a remount:
1498
+ __signature__(Hello, '1');
1499
+
1500
+ // Use the passed wrapper.
1501
+ // This will be different in every test.
1502
+ return wrap(Hello);
1503
+ });
1504
+
1505
+ // Bump the state before patching.
1506
+ const el = container.firstChild;
1507
+ expect(el.textContent).toBe('0');
1508
+ expect(el.style.color).toBe('blue');
1509
+ act(() => {
1510
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1511
+ });
1512
+ expect(el.textContent).toBe('1');
1513
+
1514
+ // Perform a hot update that doesn't remount.
1515
+ patch(() => {
1516
+ function Hello() {
1517
+ const [val, setVal] = React.useState(0);
1518
+ return (
1519
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1520
+ {val}
1521
+ </p>
1522
+ );
1523
+ }
1524
+ __register__(Hello, 'Hello');
1525
+ // The signature hasn't changed since the last time:
1526
+ __signature__(Hello, '1');
1527
+ return Hello;
1528
+ });
1529
+
1530
+ // Assert the state was preserved but color changed.
1531
+ expect(container.firstChild).toBe(el);
1532
+ expect(el.textContent).toBe('1');
1533
+ expect(el.style.color).toBe('red');
1534
+
1535
+ // Perform a hot update that remounts.
1536
+ patch(() => {
1537
+ function Hello() {
1538
+ const [val, setVal] = React.useState(0);
1539
+ return (
1540
+ <p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
1541
+ {val}
1542
+ </p>
1543
+ );
1544
+ }
1545
+ // We're changing the signature now so it will remount:
1546
+ __register__(Hello, 'Hello');
1547
+ __signature__(Hello, '2');
1548
+ return Hello;
1549
+ });
1550
+
1551
+ // Expect a remount.
1552
+ expect(container.firstChild).not.toBe(el);
1553
+ const newEl = container.firstChild;
1554
+ expect(newEl.textContent).toBe('0');
1555
+ expect(newEl.style.color).toBe('yellow');
1556
+
1557
+ // Bump state again.
1558
+ act(() => {
1559
+ newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1560
+ });
1561
+ expect(newEl.textContent).toBe('1');
1562
+ expect(newEl.style.color).toBe('yellow');
1563
+
1564
+ // Verify we can patch again while preserving the signature.
1565
+ patch(() => {
1566
+ function Hello() {
1567
+ const [val, setVal] = React.useState(0);
1568
+ return (
1569
+ <p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
1570
+ {val}
1571
+ </p>
1572
+ );
1573
+ }
1574
+ // Same signature as last time.
1575
+ __register__(Hello, 'Hello');
1576
+ __signature__(Hello, '2');
1577
+ return Hello;
1578
+ });
1579
+
1580
+ expect(container.firstChild).toBe(newEl);
1581
+ expect(newEl.textContent).toBe('1');
1582
+ expect(newEl.style.color).toBe('purple');
1583
+
1584
+ // Check removing the signature also causes a remount.
1585
+ patch(() => {
1586
+ function Hello() {
1587
+ const [val, setVal] = React.useState(0);
1588
+ return (
1589
+ <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
1590
+ {val}
1591
+ </p>
1592
+ );
1593
+ }
1594
+ // No signature this time.
1595
+ __register__(Hello, 'Hello');
1596
+ return Hello;
1597
+ });
1598
+
1599
+ // Expect a remount.
1600
+ expect(container.firstChild).not.toBe(newEl);
1601
+ const finalEl = container.firstChild;
1602
+ expect(finalEl.textContent).toBe('0');
1603
+ expect(finalEl.style.color).toBe('orange');
1604
+ }
1605
+
1606
+ it('resets hooks with dependencies on hot reload', () => {
1607
+ if (__DEV__) {
1608
+ let useEffectWithEmptyArrayCalls = 0;
1609
+
1610
+ render(() => {
1611
+ function Hello() {
1612
+ const [val, setVal] = React.useState(0);
1613
+ const tranformed = React.useMemo(() => val * 2, [val]);
1614
+ const handleClick = React.useCallback(() => setVal(v => v + 1), []);
1615
+
1616
+ React.useEffect(() => {
1617
+ useEffectWithEmptyArrayCalls++;
1618
+ }, []);
1619
+
1620
+ return (
1621
+ <p style={{color: 'blue'}} onClick={handleClick}>
1622
+ {tranformed}
1623
+ </p>
1624
+ );
1625
+ }
1626
+ __register__(Hello, 'Hello');
1627
+ return Hello;
1628
+ });
1629
+
1630
+ // Bump the state before patching.
1631
+ const el = container.firstChild;
1632
+ expect(el.textContent).toBe('0');
1633
+ expect(el.style.color).toBe('blue');
1634
+ expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect ran
1635
+ act(() => {
1636
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1637
+ });
1638
+ expect(el.textContent).toBe('2'); // val * 2
1639
+ expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect didn't re-run
1640
+
1641
+ // Perform a hot update.
1642
+ act(() => {
1643
+ patch(() => {
1644
+ function Hello() {
1645
+ const [val, setVal] = React.useState(0);
1646
+ const tranformed = React.useMemo(() => val * 10, [val]);
1647
+ const handleClick = React.useCallback(() => setVal(v => v - 1), []);
1648
+
1649
+ React.useEffect(() => {
1650
+ useEffectWithEmptyArrayCalls++;
1651
+ }, []);
1652
+
1653
+ return (
1654
+ <p style={{color: 'red'}} onClick={handleClick}>
1655
+ {tranformed}
1656
+ </p>
1657
+ );
1658
+ }
1659
+ __register__(Hello, 'Hello');
1660
+ return Hello;
1661
+ });
1662
+ });
1663
+
1664
+ // Assert the state was preserved but memo was evicted.
1665
+ expect(container.firstChild).toBe(el);
1666
+ expect(el.textContent).toBe('10'); // val * 10
1667
+ expect(el.style.color).toBe('red');
1668
+ expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect re-ran
1669
+
1670
+ // This should fire the new callback which decreases the counter.
1671
+ act(() => {
1672
+ el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1673
+ });
1674
+ expect(el.textContent).toBe('0');
1675
+ expect(el.style.color).toBe('red');
1676
+ expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect didn't re-run
1677
+ }
1678
+ });
1679
+
1680
+ it('can hot reload offscreen components', () => {
1681
+ if (__DEV__) {
1682
+ const AppV1 = prepare(() => {
1683
+ function Hello() {
1684
+ React.useLayoutEffect(() => {
1685
+ Scheduler.yieldValue('Hello#layout');
1686
+ });
1687
+ const [val, setVal] = React.useState(0);
1688
+ return (
1689
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1690
+ {val}
1691
+ </p>
1692
+ );
1693
+ }
1694
+ __register__(Hello, 'Hello');
1695
+
1696
+ return function App({offscreen}) {
1697
+ React.useLayoutEffect(() => {
1698
+ Scheduler.yieldValue('App#layout');
1699
+ });
1700
+ return (
1701
+ <div hidden={offscreen}>
1702
+ <Hello />
1703
+ </div>
1704
+ );
1705
+ };
1706
+ });
1707
+
1708
+ const root = ReactDOM.unstable_createRoot(container);
1709
+ root.render(<AppV1 offscreen={true} />);
1710
+ expect(Scheduler).toFlushAndYieldThrough(['App#layout']);
1711
+ const el = container.firstChild;
1712
+ expect(el.hidden).toBe(true);
1713
+ expect(el.firstChild).toBe(null); // Offscreen content not flushed yet.
1714
+
1715
+ // Perform a hot update.
1716
+ patch(() => {
1717
+ function Hello() {
1718
+ React.useLayoutEffect(() => {
1719
+ Scheduler.yieldValue('Hello#layout');
1720
+ });
1721
+ const [val, setVal] = React.useState(0);
1722
+ return (
1723
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1724
+ {val}
1725
+ </p>
1726
+ );
1727
+ }
1728
+ __register__(Hello, 'Hello');
1729
+ });
1730
+
1731
+ // It's still offscreen so we don't see anything.
1732
+ expect(container.firstChild).toBe(el);
1733
+ expect(el.hidden).toBe(true);
1734
+ expect(el.firstChild).toBe(null);
1735
+
1736
+ // Process the offscreen updates.
1737
+ expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
1738
+ expect(container.firstChild).toBe(el);
1739
+ expect(el.firstChild.textContent).toBe('0');
1740
+ expect(el.firstChild.style.color).toBe('red');
1741
+
1742
+ el.firstChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1743
+ expect(el.firstChild.textContent).toBe('0');
1744
+ expect(el.firstChild.style.color).toBe('red');
1745
+ expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
1746
+ expect(el.firstChild.textContent).toBe('1');
1747
+ expect(el.firstChild.style.color).toBe('red');
1748
+
1749
+ // Hot reload while we're offscreen.
1750
+ patch(() => {
1751
+ function Hello() {
1752
+ React.useLayoutEffect(() => {
1753
+ Scheduler.yieldValue('Hello#layout');
1754
+ });
1755
+ const [val, setVal] = React.useState(0);
1756
+ return (
1757
+ <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
1758
+ {val}
1759
+ </p>
1760
+ );
1761
+ }
1762
+ __register__(Hello, 'Hello');
1763
+ });
1764
+
1765
+ // It's still offscreen so we don't see the updates.
1766
+ expect(container.firstChild).toBe(el);
1767
+ expect(el.firstChild.textContent).toBe('1');
1768
+ expect(el.firstChild.style.color).toBe('red');
1769
+
1770
+ // Process the offscreen updates.
1771
+ expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']);
1772
+ expect(container.firstChild).toBe(el);
1773
+ expect(el.firstChild.textContent).toBe('1');
1774
+ expect(el.firstChild.style.color).toBe('orange');
1775
+ }
1776
+ });
1777
+
1778
+ it('remounts failed error boundaries (componentDidCatch)', () => {
1779
+ if (__DEV__) {
1780
+ render(() => {
1781
+ function Hello() {
1782
+ return <h1>Hi</h1>;
1783
+ }
1784
+ __register__(Hello, 'Hello');
1785
+
1786
+ class Boundary extends React.Component {
1787
+ state = {error: null};
1788
+ componentDidCatch(error) {
1789
+ this.setState({error});
1790
+ }
1791
+ render() {
1792
+ if (this.state.error) {
1793
+ return <h1>Oops: {this.state.error.message}</h1>;
1794
+ }
1795
+ return this.props.children;
1796
+ }
1797
+ }
1798
+
1799
+ function App() {
1800
+ return (
1801
+ <React.Fragment>
1802
+ <p>A</p>
1803
+ <Boundary>
1804
+ <Hello />
1805
+ </Boundary>
1806
+ <p>B</p>
1807
+ </React.Fragment>
1808
+ );
1809
+ }
1810
+
1811
+ return App;
1812
+ });
1813
+
1814
+ expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
1815
+ const firstP = container.firstChild;
1816
+ const secondP = firstP.nextSibling.nextSibling;
1817
+
1818
+ // Perform a hot update that fails.
1819
+ patch(() => {
1820
+ function Hello() {
1821
+ throw new Error('No');
1822
+ }
1823
+ __register__(Hello, 'Hello');
1824
+ });
1825
+
1826
+ expect(container.innerHTML).toBe('<p>A</p><h1>Oops: No</h1><p>B</p>');
1827
+ expect(container.firstChild).toBe(firstP);
1828
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
1829
+
1830
+ // Perform a hot update that fixes the error.
1831
+ patch(() => {
1832
+ function Hello() {
1833
+ return <h1>Fixed!</h1>;
1834
+ }
1835
+ __register__(Hello, 'Hello');
1836
+ });
1837
+
1838
+ // This should remount the error boundary (but not anything above it).
1839
+ expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
1840
+ expect(container.firstChild).toBe(firstP);
1841
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
1842
+
1843
+ // Verify next hot reload doesn't remount anything.
1844
+ const helloNode = container.firstChild.nextSibling;
1845
+ patch(() => {
1846
+ function Hello() {
1847
+ return <h1>Nice.</h1>;
1848
+ }
1849
+ __register__(Hello, 'Hello');
1850
+ });
1851
+ expect(container.firstChild.nextSibling).toBe(helloNode);
1852
+ expect(helloNode.textContent).toBe('Nice.');
1853
+ }
1854
+ });
1855
+
1856
+ it('remounts failed error boundaries (getDerivedStateFromError)', () => {
1857
+ if (__DEV__) {
1858
+ render(() => {
1859
+ function Hello() {
1860
+ return <h1>Hi</h1>;
1861
+ }
1862
+ __register__(Hello, 'Hello');
1863
+
1864
+ class Boundary extends React.Component {
1865
+ state = {error: null};
1866
+ static getDerivedStateFromError(error) {
1867
+ return {error};
1868
+ }
1869
+ render() {
1870
+ if (this.state.error) {
1871
+ return <h1>Oops: {this.state.error.message}</h1>;
1872
+ }
1873
+ return this.props.children;
1874
+ }
1875
+ }
1876
+
1877
+ function App() {
1878
+ return (
1879
+ <React.Fragment>
1880
+ <p>A</p>
1881
+ <Boundary>
1882
+ <Hello />
1883
+ </Boundary>
1884
+ <p>B</p>
1885
+ </React.Fragment>
1886
+ );
1887
+ }
1888
+
1889
+ return App;
1890
+ });
1891
+
1892
+ expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
1893
+ const firstP = container.firstChild;
1894
+ const secondP = firstP.nextSibling.nextSibling;
1895
+
1896
+ // Perform a hot update that fails.
1897
+ patch(() => {
1898
+ function Hello() {
1899
+ throw new Error('No');
1900
+ }
1901
+ __register__(Hello, 'Hello');
1902
+ });
1903
+
1904
+ expect(container.innerHTML).toBe('<p>A</p><h1>Oops: No</h1><p>B</p>');
1905
+ expect(container.firstChild).toBe(firstP);
1906
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
1907
+
1908
+ // Perform a hot update that fixes the error.
1909
+ patch(() => {
1910
+ function Hello() {
1911
+ return <h1>Fixed!</h1>;
1912
+ }
1913
+ __register__(Hello, 'Hello');
1914
+ });
1915
+
1916
+ // This should remount the error boundary (but not anything above it).
1917
+ expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
1918
+ expect(container.firstChild).toBe(firstP);
1919
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
1920
+
1921
+ // Verify next hot reload doesn't remount anything.
1922
+ const helloNode = container.firstChild.nextSibling;
1923
+ patch(() => {
1924
+ function Hello() {
1925
+ return <h1>Nice.</h1>;
1926
+ }
1927
+ __register__(Hello, 'Hello');
1928
+ });
1929
+ expect(container.firstChild.nextSibling).toBe(helloNode);
1930
+ expect(helloNode.textContent).toBe('Nice.');
1931
+ }
1932
+ });
1933
+
1934
+ it('remounts error boundaries that failed asynchronously after hot update', () => {
1935
+ if (__DEV__) {
1936
+ render(() => {
1937
+ function Hello() {
1938
+ const [x] = React.useState('');
1939
+ React.useEffect(() => {}, []);
1940
+ x.slice(); // Doesn't throw initially.
1941
+ return <h1>Hi</h1>;
1942
+ }
1943
+ __register__(Hello, 'Hello');
1944
+
1945
+ class Boundary extends React.Component {
1946
+ state = {error: null};
1947
+ static getDerivedStateFromError(error) {
1948
+ return {error};
1949
+ }
1950
+ render() {
1951
+ if (this.state.error) {
1952
+ return <h1>Oops: {this.state.error.message}</h1>;
1953
+ }
1954
+ return this.props.children;
1955
+ }
1956
+ }
1957
+
1958
+ function App() {
1959
+ return (
1960
+ <React.Fragment>
1961
+ <p>A</p>
1962
+ <Boundary>
1963
+ <Hello />
1964
+ </Boundary>
1965
+ <p>B</p>
1966
+ </React.Fragment>
1967
+ );
1968
+ }
1969
+
1970
+ return App;
1971
+ });
1972
+
1973
+ expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
1974
+ const firstP = container.firstChild;
1975
+ const secondP = firstP.nextSibling.nextSibling;
1976
+
1977
+ // Perform a hot update that fails.
1978
+ act(() => {
1979
+ patch(() => {
1980
+ function Hello() {
1981
+ const [x, setX] = React.useState('');
1982
+ React.useEffect(() => {
1983
+ setTimeout(() => {
1984
+ setX(42); // This will crash next render.
1985
+ }, 1);
1986
+ }, []);
1987
+ x.slice();
1988
+ return <h1>Hi</h1>;
1989
+ }
1990
+ __register__(Hello, 'Hello');
1991
+ });
1992
+ });
1993
+
1994
+ expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
1995
+ // Run timeout inside effect:
1996
+ act(() => {
1997
+ jest.runAllTimers();
1998
+ });
1999
+ expect(container.innerHTML).toBe(
2000
+ '<p>A</p><h1>Oops: x.slice is not a function</h1><p>B</p>',
2001
+ );
2002
+ expect(container.firstChild).toBe(firstP);
2003
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
2004
+
2005
+ // Perform a hot update that fixes the error.
2006
+ patch(() => {
2007
+ function Hello() {
2008
+ const [x] = React.useState('');
2009
+ React.useEffect(() => {}, []); // Removes the bad effect code.
2010
+ x.slice(); // Doesn't throw initially.
2011
+ return <h1>Fixed!</h1>;
2012
+ }
2013
+ __register__(Hello, 'Hello');
2014
+ });
2015
+
2016
+ // This should remount the error boundary (but not anything above it).
2017
+ expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
2018
+ expect(container.firstChild).toBe(firstP);
2019
+ expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
2020
+
2021
+ // Verify next hot reload doesn't remount anything.
2022
+ const helloNode = container.firstChild.nextSibling;
2023
+ patch(() => {
2024
+ function Hello() {
2025
+ const [x] = React.useState('');
2026
+ React.useEffect(() => {}, []);
2027
+ x.slice();
2028
+ return <h1>Nice.</h1>;
2029
+ }
2030
+ __register__(Hello, 'Hello');
2031
+ });
2032
+ expect(container.firstChild.nextSibling).toBe(helloNode);
2033
+ expect(helloNode.textContent).toBe('Nice.');
2034
+ }
2035
+ });
2036
+});