main
js 4,676 lines 137 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let React;
13 let ReactDOM;
14 let ReactDOMClient;
15 let ReactFreshRuntime;
16 let Scheduler;
17 let act;
18 let createReactClass;
19 let waitFor;
20 let assertLog;
21
22 describe('ReactFresh', () => {
23 let container;
24 let root;
25
26 beforeEach(() => {
27 if (__DEV__) {
28 jest.resetModules();
29 React = require('react');
30 ReactFreshRuntime = require('react-refresh/runtime');
31 ReactFreshRuntime.injectIntoGlobalHook(global);
32 ReactDOM = require('react-dom');
33 ReactDOMClient = require('react-dom/client');
34 Scheduler = require('scheduler');
35 act = require('internal-test-utils').act;
36
37 const InternalTestUtils = require('internal-test-utils');
38 waitFor = InternalTestUtils.waitFor;
39 assertLog = InternalTestUtils.assertLog;
40
41 createReactClass = require('create-react-class/factory')(
42 React.Component,
43 React.isValidElement,
44 new React.Component().updater,
45 );
46 container = document.createElement('div');
47 root = ReactDOMClient.createRoot(container);
48 document.body.appendChild(container);
49 }
50 });
51
52 afterEach(() => {
53 if (__DEV__) {
54 delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
55 document.body.removeChild(container);
56 }
57 });
58
59 function prepare(version) {
60 const Component = version();
61 return Component;
62 }
63
64 async function render(version, props) {
65 const Component = version();
66 await act(() => {
67 root.render(<Component {...props} />);
68 });
69 return Component;
70 }
71
72 async function patch(version) {
73 const Component = version();
74 await act(() => {
75 ReactFreshRuntime.performReactRefresh();
76 });
77 return Component;
78 }
79
80 function patchSync(version) {
81 const Component = version();
82 ReactFreshRuntime.performReactRefresh();
83 return Component;
84 }
85
86 function $RefreshReg$(type, id) {
87 ReactFreshRuntime.register(type, id);
88 }
89
90 function $RefreshSig$(type, key, forceReset, getCustomHooks) {
91 ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
92 return type;
93 }
94
95 // Note: This is based on a similar component we use in www. We can delete
96 // once the extra div wrapper is no longer necessary.
97 function LegacyHiddenDiv({children, mode}) {
98 return (
99 <div hidden={mode === 'hidden'}>
100 <React.unstable_LegacyHidden
101 mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
102 {children}
103 </React.unstable_LegacyHidden>
104 </div>
105 );
106 }
107
108 it('can preserve state for compatible types', async () => {
109 if (__DEV__) {
110 const HelloV1 = await render(() => {
111 function Hello() {
112 const [val, setVal] = React.useState(0);
113 return (
114 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
115 {val}
116 </p>
117 );
118 }
119 $RefreshReg$(Hello, 'Hello');
120 return Hello;
121 });
122
123 // Bump the state before patching.
124 const el = container.firstChild;
125 expect(el.textContent).toBe('0');
126 expect(el.style.color).toBe('blue');
127 await act(() => {
128 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
129 });
130 expect(el.textContent).toBe('1');
131
132 // Perform a hot update.
133 const HelloV2 = await patch(() => {
134 function Hello() {
135 const [val, setVal] = React.useState(0);
136 return (
137 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
138 {val}
139 </p>
140 );
141 }
142 $RefreshReg$(Hello, 'Hello');
143 return Hello;
144 });
145
146 // Assert the state was preserved but color changed.
147 expect(container.firstChild).toBe(el);
148 expect(el.textContent).toBe('1');
149 expect(el.style.color).toBe('red');
150
151 // Bump the state again.
152 await act(() => {
153 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
154 });
155 expect(container.firstChild).toBe(el);
156 expect(el.textContent).toBe('2');
157 expect(el.style.color).toBe('red');
158
159 // Perform top-down renders with both fresh and stale types.
160 // Neither should change the state or color.
161 // They should always resolve to the latest version.
162 await render(() => HelloV1);
163 await render(() => HelloV2);
164 await render(() => HelloV1);
165 expect(container.firstChild).toBe(el);
166 expect(el.textContent).toBe('2');
167 expect(el.style.color).toBe('red');
168
169 // Bump the state again.
170 await act(() => {
171 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
172 });
173 expect(container.firstChild).toBe(el);
174 expect(el.textContent).toBe('3');
175 expect(el.style.color).toBe('red');
176
177 // Finally, a render with incompatible type should reset it.
178 await render(() => {
179 function Hello() {
180 const [val, setVal] = React.useState(0);
181 return (
182 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
183 {val}
184 </p>
185 );
186 }
187 // No register call.
188 // This is considered a new type.
189 return Hello;
190 });
191 expect(container.firstChild).not.toBe(el);
192 const newEl = container.firstChild;
193 expect(newEl.textContent).toBe('0');
194 expect(newEl.style.color).toBe('blue');
195 }
196 });
197
198 it('can preserve state for forwardRef', async () => {
199 if (__DEV__) {
200 const OuterV1 = await render(() => {
201 function Hello() {
202 const [val, setVal] = React.useState(0);
203 return (
204 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
205 {val}
206 </p>
207 );
208 }
209 $RefreshReg$(Hello, 'Hello');
210
211 const Outer = React.forwardRef(() => <Hello />);
212 $RefreshReg$(Outer, 'Outer');
213 return Outer;
214 });
215
216 // Bump the state before patching.
217 const el = container.firstChild;
218 expect(el.textContent).toBe('0');
219 expect(el.style.color).toBe('blue');
220 await act(() => {
221 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
222 });
223 expect(el.textContent).toBe('1');
224
225 // Perform a hot update.
226 const OuterV2 = await patch(() => {
227 function Hello() {
228 const [val, setVal] = React.useState(0);
229 return (
230 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
231 {val}
232 </p>
233 );
234 }
235 $RefreshReg$(Hello, 'Hello');
236
237 const Outer = React.forwardRef(() => <Hello />);
238 $RefreshReg$(Outer, 'Outer');
239 return Outer;
240 });
241
242 // Assert the state was preserved but color changed.
243 expect(container.firstChild).toBe(el);
244 expect(el.textContent).toBe('1');
245 expect(el.style.color).toBe('red');
246
247 // Bump the state again.
248 await act(() => {
249 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
250 });
251 expect(container.firstChild).toBe(el);
252 expect(el.textContent).toBe('2');
253 expect(el.style.color).toBe('red');
254
255 // Perform top-down renders with both fresh and stale types.
256 // Neither should change the state or color.
257 // They should always resolve to the latest version.
258 await render(() => OuterV1);
259 await render(() => OuterV2);
260 await render(() => OuterV1);
261 expect(container.firstChild).toBe(el);
262 expect(el.textContent).toBe('2');
263 expect(el.style.color).toBe('red');
264
265 // Finally, a render with incompatible type should reset it.
266 await render(() => {
267 function Hello() {
268 const [val, setVal] = React.useState(0);
269 return (
270 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
271 {val}
272 </p>
273 );
274 }
275 $RefreshReg$(Hello, 'Hello');
276
277 // Note: no forwardRef wrapper this time.
278 return Hello;
279 });
280
281 expect(container.firstChild).not.toBe(el);
282 const newEl = container.firstChild;
283 expect(newEl.textContent).toBe('0');
284 expect(newEl.style.color).toBe('blue');
285 }
286 });
287
288 it('should not consider two forwardRefs around the same type to be equivalent', async () => {
289 if (__DEV__) {
290 const ParentV1 = await render(
291 () => {
292 function Hello() {
293 const [val, setVal] = React.useState(0);
294 return (
295 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
296 {val}
297 </p>
298 );
299 }
300 $RefreshReg$(Hello, 'Hello');
301
302 function renderInner() {
303 return <Hello />;
304 }
305 // Both of these are wrappers around the same inner function.
306 // They should be treated as distinct types across reloads.
307 const ForwardRefA = React.forwardRef(renderInner);
308 $RefreshReg$(ForwardRefA, 'ForwardRefA');
309 const ForwardRefB = React.forwardRef(renderInner);
310 $RefreshReg$(ForwardRefB, 'ForwardRefB');
311
312 function Parent({cond}) {
313 return cond ? <ForwardRefA /> : <ForwardRefB />;
314 }
315 $RefreshReg$(Parent, 'Parent');
316
317 return Parent;
318 },
319 {cond: true},
320 );
321
322 // Bump the state before switching up types.
323 let el = container.firstChild;
324 expect(el.textContent).toBe('0');
325 expect(el.style.color).toBe('blue');
326 await act(() => {
327 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
328 });
329 expect(el.textContent).toBe('1');
330
331 // Switching up the inner types should reset the state.
332 await render(() => ParentV1, {cond: false});
333 expect(el).not.toBe(container.firstChild);
334 el = container.firstChild;
335 expect(el.textContent).toBe('0');
336 expect(el.style.color).toBe('blue');
337
338 await act(() => {
339 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
340 });
341 expect(el.textContent).toBe('1');
342
343 // Switch them up back again.
344 await render(() => ParentV1, {cond: true});
345 expect(el).not.toBe(container.firstChild);
346 el = container.firstChild;
347 expect(el.textContent).toBe('0');
348 expect(el.style.color).toBe('blue');
349
350 // Now bump up the state to prepare for patching.
351 await act(() => {
352 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
353 });
354 expect(el.textContent).toBe('1');
355
356 // Patch to change the color.
357 const ParentV2 = await patch(() => {
358 function Hello() {
359 const [val, setVal] = React.useState(0);
360 return (
361 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
362 {val}
363 </p>
364 );
365 }
366 $RefreshReg$(Hello, 'Hello');
367
368 function renderInner() {
369 return <Hello />;
370 }
371 // Both of these are wrappers around the same inner function.
372 // They should be treated as distinct types across reloads.
373 const ForwardRefA = React.forwardRef(renderInner);
374 $RefreshReg$(ForwardRefA, 'ForwardRefA');
375 const ForwardRefB = React.forwardRef(renderInner);
376 $RefreshReg$(ForwardRefB, 'ForwardRefB');
377
378 function Parent({cond}) {
379 return cond ? <ForwardRefA /> : <ForwardRefB />;
380 }
381 $RefreshReg$(Parent, 'Parent');
382
383 return Parent;
384 });
385
386 // The state should be intact; the color should change.
387 expect(el).toBe(container.firstChild);
388 expect(el.textContent).toBe('1');
389 expect(el.style.color).toBe('red');
390
391 // Switching up the condition should still reset the state.
392 await render(() => ParentV2, {cond: false});
393 expect(el).not.toBe(container.firstChild);
394 el = container.firstChild;
395 expect(el.textContent).toBe('0');
396 expect(el.style.color).toBe('red');
397
398 // Now bump up the state to prepare for top-level renders.
399 await act(() => {
400 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
401 });
402 expect(el).toBe(container.firstChild);
403 expect(el.textContent).toBe('1');
404 expect(el.style.color).toBe('red');
405
406 // Finally, verify using top-level render with stale type keeps state.
407 await render(() => ParentV1);
408 await render(() => ParentV2);
409 await render(() => ParentV1);
410 expect(container.firstChild).toBe(el);
411 expect(el.textContent).toBe('1');
412 expect(el.style.color).toBe('red');
413 }
414 });
415
416 it('can update forwardRef render function with its wrapper', async () => {
417 if (__DEV__) {
418 await render(() => {
419 function Hello({color}) {
420 const [val, setVal] = React.useState(0);
421 return (
422 <p style={{color}} onClick={() => setVal(val + 1)}>
423 {val}
424 </p>
425 );
426 }
427 $RefreshReg$(Hello, 'Hello');
428
429 const Outer = React.forwardRef(() => <Hello color="blue" />);
430 $RefreshReg$(Outer, 'Outer');
431 return Outer;
432 });
433
434 // Bump the state before patching.
435 const el = container.firstChild;
436 expect(el.textContent).toBe('0');
437 expect(el.style.color).toBe('blue');
438 await act(() => {
439 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
440 });
441 expect(el.textContent).toBe('1');
442
443 // Perform a hot update.
444 await patch(() => {
445 function Hello({color}) {
446 const [val, setVal] = React.useState(0);
447 return (
448 <p style={{color}} onClick={() => setVal(val + 1)}>
449 {val}
450 </p>
451 );
452 }
453 $RefreshReg$(Hello, 'Hello');
454
455 const Outer = React.forwardRef(() => <Hello color="red" />);
456 $RefreshReg$(Outer, 'Outer');
457 return Outer;
458 });
459
460 // Assert the state was preserved but color changed.
461 expect(container.firstChild).toBe(el);
462 expect(el.textContent).toBe('1');
463 expect(el.style.color).toBe('red');
464 }
465 });
466
467 it('can update forwardRef render function in isolation', async () => {
468 if (__DEV__) {
469 await render(() => {
470 function Hello({color}) {
471 const [val, setVal] = React.useState(0);
472 return (
473 <p style={{color}} onClick={() => setVal(val + 1)}>
474 {val}
475 </p>
476 );
477 }
478 $RefreshReg$(Hello, 'Hello');
479
480 function renderHello() {
481 return <Hello color="blue" />;
482 }
483 $RefreshReg$(renderHello, 'renderHello');
484
485 return React.forwardRef(renderHello);
486 });
487
488 // Bump the state before patching.
489 const el = container.firstChild;
490 expect(el.textContent).toBe('0');
491 expect(el.style.color).toBe('blue');
492 await act(() => {
493 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
494 });
495 expect(el.textContent).toBe('1');
496
497 // Perform a hot update of just the rendering function.
498 await patch(() => {
499 function Hello({color}) {
500 const [val, setVal] = React.useState(0);
501 return (
502 <p style={{color}} onClick={() => setVal(val + 1)}>
503 {val}
504 </p>
505 );
506 }
507 $RefreshReg$(Hello, 'Hello');
508
509 function renderHello() {
510 return <Hello color="red" />;
511 }
512 $RefreshReg$(renderHello, 'renderHello');
513
514 // Not updating the wrapper.
515 });
516
517 // Assert the state was preserved but color changed.
518 expect(container.firstChild).toBe(el);
519 expect(el.textContent).toBe('1');
520 expect(el.style.color).toBe('red');
521 }
522 });
523
524 it('can preserve state for simple memo', async () => {
525 if (__DEV__) {
526 const OuterV1 = await render(() => {
527 function Hello() {
528 const [val, setVal] = React.useState(0);
529 return (
530 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
531 {val}
532 </p>
533 );
534 }
535 $RefreshReg$(Hello, 'Hello');
536
537 const Outer = React.memo(Hello);
538 $RefreshReg$(Outer, 'Outer');
539 return Outer;
540 });
541
542 // Bump the state before patching.
543 const el = container.firstChild;
544 expect(el.textContent).toBe('0');
545 expect(el.style.color).toBe('blue');
546 await act(() => {
547 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
548 });
549 expect(el.textContent).toBe('1');
550
551 // Perform a hot update.
552 const OuterV2 = await patch(() => {
553 function Hello() {
554 const [val, setVal] = React.useState(0);
555 return (
556 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
557 {val}
558 </p>
559 );
560 }
561 $RefreshReg$(Hello, 'Hello');
562
563 const Outer = React.memo(Hello);
564 $RefreshReg$(Outer, 'Outer');
565 return Outer;
566 });
567
568 // Assert the state was preserved but color changed.
569 expect(container.firstChild).toBe(el);
570 expect(el.textContent).toBe('1');
571 expect(el.style.color).toBe('red');
572
573 // Bump the state again.
574 await act(() => {
575 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
576 });
577 expect(container.firstChild).toBe(el);
578 expect(el.textContent).toBe('2');
579 expect(el.style.color).toBe('red');
580
581 // Perform top-down renders with both fresh and stale types.
582 // Neither should change the state or color.
583 // They should always resolve to the latest version.
584 await render(() => OuterV1);
585 await render(() => OuterV2);
586 await render(() => OuterV1);
587 expect(container.firstChild).toBe(el);
588 expect(el.textContent).toBe('2');
589 expect(el.style.color).toBe('red');
590
591 // Finally, a render with incompatible type should reset it.
592 await render(() => {
593 function Hello() {
594 const [val, setVal] = React.useState(0);
595 return (
596 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
597 {val}
598 </p>
599 );
600 }
601 $RefreshReg$(Hello, 'Hello');
602
603 // Note: no wrapper this time.
604 return Hello;
605 });
606
607 expect(container.firstChild).not.toBe(el);
608 const newEl = container.firstChild;
609 expect(newEl.textContent).toBe('0');
610 expect(newEl.style.color).toBe('blue');
611 }
612 });
613
614 it('can preserve state for memo with custom comparison', async () => {
615 if (__DEV__) {
616 const OuterV1 = await render(() => {
617 function Hello() {
618 const [val, setVal] = React.useState(0);
619 return (
620 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
621 {val}
622 </p>
623 );
624 }
625
626 const Outer = React.memo(Hello, () => true);
627 $RefreshReg$(Outer, 'Outer');
628 return Outer;
629 });
630
631 // Bump the state before patching.
632 const el = container.firstChild;
633 expect(el.textContent).toBe('0');
634 expect(el.style.color).toBe('blue');
635 await act(() => {
636 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
637 });
638 expect(el.textContent).toBe('1');
639
640 // Perform a hot update.
641 const OuterV2 = await patch(() => {
642 function Hello() {
643 const [val, setVal] = React.useState(0);
644 return (
645 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
646 {val}
647 </p>
648 );
649 }
650
651 const Outer = React.memo(Hello, () => true);
652 $RefreshReg$(Outer, 'Outer');
653 return Outer;
654 });
655
656 // Assert the state was preserved but color changed.
657 expect(container.firstChild).toBe(el);
658 expect(el.textContent).toBe('1');
659 expect(el.style.color).toBe('red');
660
661 // Bump the state again.
662 await act(() => {
663 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
664 });
665 expect(container.firstChild).toBe(el);
666 expect(el.textContent).toBe('2');
667 expect(el.style.color).toBe('red');
668
669 // Perform top-down renders with both fresh and stale types.
670 // Neither should change the state or color.
671 // They should always resolve to the latest version.
672 await render(() => OuterV1);
673 await render(() => OuterV2);
674 await render(() => OuterV1);
675 expect(container.firstChild).toBe(el);
676 expect(el.textContent).toBe('2');
677 expect(el.style.color).toBe('red');
678
679 // Finally, a render with incompatible type should reset it.
680 await render(() => {
681 function Hello() {
682 const [val, setVal] = React.useState(0);
683 return (
684 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
685 {val}
686 </p>
687 );
688 }
689 $RefreshReg$(Hello, 'Hello');
690
691 // Note: no wrapper this time.
692 return Hello;
693 });
694
695 expect(container.firstChild).not.toBe(el);
696 const newEl = container.firstChild;
697 expect(newEl.textContent).toBe('0');
698 expect(newEl.style.color).toBe('blue');
699 }
700 });
701
702 it('can remount when change function to memo', async () => {
703 if (__DEV__) {
704 await act(async () => {
705 await render(() => {
706 function Test() {
707 return <p>hi test</p>;
708 }
709 $RefreshReg$(Test, 'Test');
710 return Test;
711 });
712 });
713
714 // Check the initial render
715 const el = container.firstChild;
716 expect(el.textContent).toBe('hi test');
717
718 // Patch to change function to memo
719 await act(async () => {
720 await patch(() => {
721 function Test2() {
722 return <p>hi memo</p>;
723 }
724 const Test = React.memo(Test2);
725 $RefreshReg$(Test2, 'Test2');
726 $RefreshReg$(Test, 'Test');
727 return Test;
728 });
729 });
730
731 // Check remount
732 expect(container.firstChild).not.toBe(el);
733 const nextEl = container.firstChild;
734 expect(nextEl.textContent).toBe('hi memo');
735
736 // Patch back to original function
737 await act(async () => {
738 await patch(() => {
739 function Test() {
740 return <p>hi test</p>;
741 }
742 $RefreshReg$(Test, 'Test');
743 return Test;
744 });
745 });
746
747 // Check final remount
748 expect(container.firstChild).not.toBe(nextEl);
749 const newEl = container.firstChild;
750 expect(newEl.textContent).toBe('hi test');
751 }
752 });
753
754 it('can remount when change memo to forwardRef', async () => {
755 if (__DEV__) {
756 await act(async () => {
757 await render(() => {
758 function Test2() {
759 return <p>hi memo</p>;
760 }
761 const Test = React.memo(Test2);
762 $RefreshReg$(Test2, 'Test2');
763 $RefreshReg$(Test, 'Test');
764 return Test;
765 });
766 });
767 // Check the initial render
768 const el = container.firstChild;
769 expect(el.textContent).toBe('hi memo');
770
771 // Patch to change memo to forwardRef
772 await act(async () => {
773 await patch(() => {
774 function Test2() {
775 return <p>hi forwardRef</p>;
776 }
777 const Test = React.forwardRef(Test2);
778 $RefreshReg$(Test2, 'Test2');
779 $RefreshReg$(Test, 'Test');
780 return Test;
781 });
782 });
783 // Check remount
784 expect(container.firstChild).not.toBe(el);
785 const nextEl = container.firstChild;
786 expect(nextEl.textContent).toBe('hi forwardRef');
787
788 // Patch back to memo
789 await act(async () => {
790 await patch(() => {
791 function Test2() {
792 return <p>hi memo</p>;
793 }
794 const Test = React.memo(Test2);
795 $RefreshReg$(Test2, 'Test2');
796 $RefreshReg$(Test, 'Test');
797 return Test;
798 });
799 });
800 // Check final remount
801 expect(container.firstChild).not.toBe(nextEl);
802 const newEl = container.firstChild;
803 expect(newEl.textContent).toBe('hi memo');
804 }
805 });
806
807 it('can remount when change function to forwardRef', async () => {
808 if (__DEV__) {
809 await act(async () => {
810 await render(() => {
811 function Test() {
812 return <p>hi test</p>;
813 }
814 $RefreshReg$(Test, 'Test');
815 return Test;
816 });
817 });
818
819 // Check the initial render
820 const el = container.firstChild;
821 expect(el.textContent).toBe('hi test');
822
823 // Patch to change function to forwardRef
824 await act(async () => {
825 await patch(() => {
826 function Test2() {
827 return <p>hi forwardRef</p>;
828 }
829 const Test = React.forwardRef(Test2);
830 $RefreshReg$(Test2, 'Test2');
831 $RefreshReg$(Test, 'Test');
832 return Test;
833 });
834 });
835
836 // Check remount
837 expect(container.firstChild).not.toBe(el);
838 const nextEl = container.firstChild;
839 expect(nextEl.textContent).toBe('hi forwardRef');
840
841 // Patch back to a new function
842 await act(async () => {
843 await patch(() => {
844 function Test() {
845 return <p>hi test1</p>;
846 }
847 $RefreshReg$(Test, 'Test');
848 return Test;
849 });
850 });
851
852 // Check final remount
853 expect(container.firstChild).not.toBe(nextEl);
854 const newEl = container.firstChild;
855 expect(newEl.textContent).toBe('hi test1');
856 }
857 });
858
859 it('can remount when change memo inner type from function to forwardRef', async () => {
860 if (__DEV__) {
861 await act(async () => {
862 await render(() => {
863 function Test2() {
864 return <p>hi memo</p>;
865 }
866 const Test = React.memo(Test2);
867 $RefreshReg$(Test2, 'Test$React.memo');
868 $RefreshReg$(Test, 'Test');
869 return Test;
870 });
871 });
872
873 // Check the initial render
874 const el = container.firstChild;
875 expect(el.textContent).toBe('hi memo');
876
877 // Patch to wrap the inner function in forwardRef.
878 // The outer type is still a memo, so only the inner family changes.
879 await act(async () => {
880 await patch(() => {
881 function Test2(props, ref) {
882 return <p>hi memo forwardRef</p>;
883 }
884 const Test2Ref = React.forwardRef(Test2);
885 const Test = React.memo(Test2Ref);
886 $RefreshReg$(Test2, 'Test$React.memo$React.forwardRef');
887 $RefreshReg$(Test2Ref, 'Test$React.memo');
888 $RefreshReg$(Test, 'Test');
889 return Test;
890 });
891 });
892
893 // Check remount
894 expect(container.firstChild).not.toBe(el);
895 const nextEl = container.firstChild;
896 expect(nextEl.textContent).toBe('hi memo forwardRef');
897
898 // Patch back to a plain function inside memo
899 await act(async () => {
900 await patch(() => {
901 function Test2() {
902 return <p>hi memo</p>;
903 }
904 const Test = React.memo(Test2);
905 $RefreshReg$(Test2, 'Test$React.memo');
906 $RefreshReg$(Test, 'Test');
907 return Test;
908 });
909 });
910
911 // Check final remount
912 expect(container.firstChild).not.toBe(nextEl);
913 const newEl = container.firstChild;
914 expect(newEl.textContent).toBe('hi memo');
915 }
916 });
917
918 it('can mount an element created before its type changed kinds', async () => {
919 if (__DEV__) {
920 let oldElement;
921 let currentChild = null;
922
923 await act(async () => {
924 await render(() => {
925 function Test() {
926 return <p>hi test</p>;
927 }
928 $RefreshReg$(Test, 'Test');
929 oldElement = <Test />;
930
931 function App() {
932 const [, forceUpdate] = React.useState(0);
933 return (
934 <div onClick={() => forceUpdate(n => n + 1)}>{currentChild}</div>
935 );
936 }
937 $RefreshReg$(App, 'App');
938 return App;
939 });
940 });
941
942 // Change the component kind before it has ever mounted.
943 await act(async () => {
944 await patch(() => {
945 function Test2() {
946 return <p>hi memo</p>;
947 }
948 const Test = React.memo(Test2);
949 $RefreshReg$(Test2, 'Test$React.memo');
950 $RefreshReg$(Test, 'Test');
951
952 function App() {
953 const [, forceUpdate] = React.useState(0);
954 return (
955 <div onClick={() => forceUpdate(n => n + 1)}>{currentChild}</div>
956 );
957 }
958 $RefreshReg$(App, 'App');
959 return App;
960 });
961 });
962
963 // Mount the element created before the edit. The fiber must be
964 // created from the latest type, with the tag matching its kind.
965 currentChild = oldElement;
966 await act(async () => {
967 container.firstChild.click();
968 });
969 expect(container.firstChild.textContent).toBe('hi memo');
970 }
971 });
972
973 it('can remount when adding or removing a memo comparison function', async () => {
974 if (__DEV__) {
975 await act(async () => {
976 await render(() => {
977 function Test2() {
978 return <p>hi memo</p>;
979 }
980 const Test = React.memo(Test2);
981 $RefreshReg$(Test2, 'Test$React.memo');
982 $RefreshReg$(Test, 'Test');
983 return Test;
984 });
985 });
986
987 // Check the initial render
988 const el = container.firstChild;
989 expect(el.textContent).toBe('hi memo');
990
991 // Patch to add a custom comparison function.
992 // The fiber can no longer be a SimpleMemoComponent.
993 await act(async () => {
994 await patch(() => {
995 function Test2() {
996 return <p>hi memo with compare</p>;
997 }
998 const Test = React.memo(Test2, (prevProps, nextProps) => false);
999 $RefreshReg$(Test2, 'Test$React.memo');
1000 $RefreshReg$(Test, 'Test');
1001 return Test;
1002 });
1003 });
1004
1005 // Check remount
1006 expect(container.firstChild).not.toBe(el);
1007 const nextEl = container.firstChild;
1008 expect(nextEl.textContent).toBe('hi memo with compare');
1009
1010 // Patch to remove the comparison function again
1011 await act(async () => {
1012 await patch(() => {
1013 function Test2() {
1014 return <p>hi memo</p>;
1015 }
1016 const Test = React.memo(Test2);
1017 $RefreshReg$(Test2, 'Test$React.memo');
1018 $RefreshReg$(Test, 'Test');
1019 return Test;
1020 });
1021 });
1022
1023 // Check final remount
1024 expect(container.firstChild).not.toBe(nextEl);
1025 const newEl = container.firstChild;
1026 expect(newEl.textContent).toBe('hi memo');
1027 }
1028 });
1029
1030 it('can update a memo comparison function in place', async () => {
1031 if (__DEV__) {
1032 await act(async () => {
1033 await render(() => {
1034 function Inner({label}) {
1035 return <p>{label}</p>;
1036 }
1037 const InnerMemo = React.memo(Inner, (prevProps, nextProps) => true);
1038 $RefreshReg$(Inner, 'Inner$React.memo');
1039 $RefreshReg$(InnerMemo, 'Inner');
1040
1041 function App() {
1042 const [n, setN] = React.useState(1);
1043 return (
1044 <div onClick={() => setN(c => c + 1)}>
1045 <InnerMemo label={'n:' + n} />
1046 </div>
1047 );
1048 }
1049 $RefreshReg$(App, 'App');
1050 return App;
1051 });
1052 });
1053
1054 // Check the initial render
1055 const el = container.firstChild;
1056 expect(el.textContent).toBe('n:1');
1057
1058 // The comparison function blocks the update.
1059 await act(async () => {
1060 el.click();
1061 });
1062 expect(el.textContent).toBe('n:1');
1063
1064 // Patch to change only the comparison function implementation.
1065 await act(async () => {
1066 await patch(() => {
1067 function Inner({label}) {
1068 return <p>{label}</p>;
1069 }
1070 const InnerMemo = React.memo(Inner, (prevProps, nextProps) => false);
1071 $RefreshReg$(Inner, 'Inner$React.memo');
1072 $RefreshReg$(InnerMemo, 'Inner');
1073
1074 function App() {
1075 const [n, setN] = React.useState(1);
1076 return (
1077 <div onClick={() => setN(c => c + 1)}>
1078 <InnerMemo label={'n:' + n} />
1079 </div>
1080 );
1081 }
1082 $RefreshReg$(App, 'App');
1083 return App;
1084 });
1085 });
1086
1087 // No remount, and the previously blocked update shows through
1088 // because the new comparison function is used.
1089 expect(container.firstChild).toBe(el);
1090 expect(el.textContent).toBe('n:2');
1091
1092 // The new comparison function applies to future updates too.
1093 await act(async () => {
1094 el.click();
1095 });
1096 expect(el.textContent).toBe('n:3');
1097 }
1098 });
1099
1100 it('mounts a pre-edit memo element with the latest comparison function', async () => {
1101 if (__DEV__) {
1102 let oldElement;
1103 let newElement;
1104 let currentChild = null;
1105
1106 await act(async () => {
1107 await render(() => {
1108 function Inner({label}) {
1109 return <p>{label}</p>;
1110 }
1111 const InnerMemo = React.memo(Inner);
1112 $RefreshReg$(Inner, 'Inner$React.memo');
1113 $RefreshReg$(InnerMemo, 'Inner');
1114 oldElement = <InnerMemo label="v1" />;
1115
1116 function App() {
1117 const [, forceUpdate] = React.useState(0);
1118 return (
1119 <div onClick={() => forceUpdate(n => n + 1)}>{currentChild}</div>
1120 );
1121 }
1122 $RefreshReg$(App, 'App');
1123 return App;
1124 });
1125 });
1126
1127 // Patch to add a comparison function that blocks all updates,
1128 // before the memo has ever mounted.
1129 await act(async () => {
1130 await patch(() => {
1131 function Inner({label}) {
1132 return <p>{label}</p>;
1133 }
1134 const InnerMemo = React.memo(Inner, (prevProps, nextProps) => true);
1135 $RefreshReg$(Inner, 'Inner$React.memo');
1136 $RefreshReg$(InnerMemo, 'Inner');
1137 newElement = <InnerMemo label="v2" />;
1138
1139 function App() {
1140 const [, forceUpdate] = React.useState(0);
1141 return (
1142 <div onClick={() => forceUpdate(n => n + 1)}>{currentChild}</div>
1143 );
1144 }
1145 $RefreshReg$(App, 'App');
1146 return App;
1147 });
1148 });
1149
1150 // Mount the element created before the edit. It must resolve to the
1151 // latest type rather than mounting in the pre-edit shape.
1152 currentChild = oldElement;
1153 await act(async () => {
1154 container.firstChild.click();
1155 });
1156 const innerEl = container.firstChild.firstChild;
1157 expect(innerEl.textContent).toBe('v1');
1158
1159 // Switch to the element created after the edit. It belongs to the
1160 // same family, so the fiber is reused (no remount)...
1161 currentChild = newElement;
1162 await act(async () => {
1163 container.firstChild.click();
1164 });
1165 expect(container.firstChild.firstChild).toBe(innerEl);
1166 // ...and the comparison function blocks the props update, proving
1167 // the fiber mounted with the comparison function in effect.
1168 expect(innerEl.textContent).toBe('v1');
1169 }
1170 });
1171
1172 it('can remount lazy(memo()) when adding a comparison function', async () => {
1173 if (__DEV__) {
1174 let resolve;
1175 await render(() => {
1176 function Hello() {
1177 return <p>hi memo</p>;
1178 }
1179 const Inner = React.memo(Hello);
1180 $RefreshReg$(Hello, 'Hello');
1181 $RefreshReg$(Inner, 'Inner');
1182
1183 const Outer = React.lazy(
1184 () =>
1185 new Promise(_resolve => {
1186 resolve = () => _resolve({default: Inner});
1187 }),
1188 );
1189 $RefreshReg$(Outer, 'Outer');
1190
1191 function App() {
1192 return (
1193 <React.Suspense fallback={<p>Loading</p>}>
1194 <Outer />
1195 </React.Suspense>
1196 );
1197 }
1198 $RefreshReg$(App, 'App');
1199 return App;
1200 });
1201
1202 expect(container.textContent).toBe('Loading');
1203 await act(() => {
1204 resolve();
1205 });
1206 expect(container.textContent).toBe('hi memo');
1207 const el = container.firstChild;
1208
1209 // Perform a hot update that adds a comparison function. The module
1210 // creating the lazy also re-runs, like when an edit propagates.
1211 await patch(() => {
1212 function Hello() {
1213 return <p>hi memo with compare</p>;
1214 }
1215 const Inner = React.memo(Hello, (prevProps, nextProps) => false);
1216 $RefreshReg$(Hello, 'Hello');
1217 $RefreshReg$(Inner, 'Inner');
1218
1219 const Outer = React.lazy(
1220 () =>
1221 new Promise(_resolve => {
1222 resolve = () => _resolve({default: Inner});
1223 }),
1224 );
1225 $RefreshReg$(Outer, 'Outer');
1226
1227 function App() {
1228 return (
1229 <React.Suspense fallback={<p>Loading</p>}>
1230 <Outer />
1231 </React.Suspense>
1232 );
1233 }
1234 $RefreshReg$(App, 'App');
1235 return App;
1236 });
1237
1238 // The shape change requires a remount. It goes through the latest
1239 // lazy type, which suspends until it resolves. The boundary shows
1240 // the fallback while the previous content stays hidden in the DOM.
1241 expect(container.textContent).toBe('hi memoLoading');
1242 await act(() => {
1243 resolve();
1244 });
1245 expect(container.textContent).toBe('hi memo with compare');
1246 expect(container.firstChild).not.toBe(el);
1247 }
1248 });
1249
1250 it('can remount lazy(memo()) when adding a comparison function without re-creating the lazy', async () => {
1251 if (__DEV__) {
1252 let resolve;
1253 await render(() => {
1254 function Hello() {
1255 return <p>hi memo</p>;
1256 }
1257 const Inner = React.memo(Hello);
1258 $RefreshReg$(Hello, 'Hello');
1259 $RefreshReg$(Inner, 'Inner');
1260
1261 const Outer = React.lazy(
1262 () =>
1263 new Promise(_resolve => {
1264 resolve = () => _resolve({default: Inner});
1265 }),
1266 );
1267 $RefreshReg$(Outer, 'Outer');
1268
1269 function App() {
1270 return (
1271 <React.Suspense fallback={<p>Loading</p>}>
1272 <Outer />
1273 </React.Suspense>
1274 );
1275 }
1276 $RefreshReg$(App, 'App');
1277 return App;
1278 });
1279
1280 expect(container.textContent).toBe('Loading');
1281 await act(() => {
1282 resolve();
1283 });
1284 expect(container.textContent).toBe('hi memo');
1285 const el = container.firstChild;
1286
1287 // Only the lazily loaded module re-runs this time, like when an
1288 // edit is contained to it (the lazy type is not re-created, so the
1289 // remount cannot go through it).
1290 await patch(() => {
1291 function Hello() {
1292 return <p>hi memo with compare</p>;
1293 }
1294 const Inner = React.memo(Hello, (prevProps, nextProps) => false);
1295 $RefreshReg$(Hello, 'Hello');
1296 $RefreshReg$(Inner, 'Inner');
1297 return Inner;
1298 });
1299
1300 // The remount goes through the old lazy, whose payload is already
1301 // resolved, so it doesn't suspend.
1302 expect(container.textContent).toBe('hi memo with compare');
1303 expect(container.firstChild).not.toBe(el);
1304 }
1305 });
1306
1307 it('can remount an unregistered memo wrapper without losing the wrapper', async () => {
1308 if (__DEV__) {
1309 let innerRenders = 0;
1310 await act(async () => {
1311 await render(() => {
1312 function Inner({label}) {
1313 innerRenders++;
1314 return <p>{label}</p>;
1315 }
1316 $RefreshReg$(Inner, 'Inner');
1317 $RefreshSig$(Inner, 'sig1');
1318 // The wrapper is deliberately not registered, like a wrapper
1319 // created inside a third-party HOC.
1320 const InnerMemo = React.memo(Inner);
1321
1322 function App() {
1323 const [, forceUpdate] = React.useState(0);
1324 return (
1325 <div onClick={() => forceUpdate(n => n + 1)}>
1326 <InnerMemo label="hi" />
1327 </div>
1328 );
1329 }
1330 $RefreshReg$(App, 'App');
1331 return App;
1332 });
1333 });
1334
1335 expect(container.textContent).toBe('hi');
1336 expect(innerRenders).toBe(1);
1337
1338 // The memo blocks re-renders with equal props.
1339 await act(async () => {
1340 container.firstChild.click();
1341 });
1342 expect(innerRenders).toBe(1);
1343
1344 // Force a remount by changing the inner function's signature.
1345 // Only the inner function's module re-runs; the wrapper and the
1346 // element referencing it are not re-created.
1347 await act(async () => {
1348 await patch(() => {
1349 function Inner({label}) {
1350 innerRenders++;
1351 return <p>{label}</p>;
1352 }
1353 $RefreshReg$(Inner, 'Inner');
1354 $RefreshSig$(Inner, 'sig2');
1355 return Inner;
1356 });
1357 });
1358 expect(innerRenders).toBe(2);
1359 const innerEl = container.firstChild.firstChild;
1360
1361 // The remounted fiber must still be a memo: equal props stay
1362 // blocked, and the fiber reconciles against the original element
1363 // instead of being replaced again.
1364 await act(async () => {
1365 container.firstChild.click();
1366 });
1367 expect(innerRenders).toBe(2);
1368 expect(container.firstChild.firstChild).toBe(innerEl);
1369 }
1370 });
1371
1372 it('resets state when switching between different component types', async () => {
1373 if (__DEV__) {
1374 await act(async () => {
1375 await render(() => {
1376 function Test() {
1377 const [count, setCount] = React.useState(0);
1378 return (
1379 <div onClick={() => setCount(c => c + 1)}>count: {count}</div>
1380 );
1381 }
1382 $RefreshReg$(Test, 'Test');
1383 return Test;
1384 });
1385 });
1386
1387 expect(container.firstChild.textContent).toBe('count: 0');
1388 await act(async () => {
1389 container.firstChild.click();
1390 });
1391 expect(container.firstChild.textContent).toBe('count: 1');
1392
1393 await act(async () => {
1394 await patch(() => {
1395 function Test2() {
1396 const [count, setCount] = React.useState(0);
1397 return (
1398 <div onClick={() => setCount(c => c + 1)}>count: {count}</div>
1399 );
1400 }
1401 const Test = React.memo(Test2);
1402 $RefreshReg$(Test2, 'Test2');
1403 $RefreshReg$(Test, 'Test');
1404 return Test;
1405 });
1406 });
1407
1408 expect(container.firstChild.textContent).toBe('count: 0');
1409 await act(async () => {
1410 container.firstChild.click();
1411 });
1412 expect(container.firstChild.textContent).toBe('count: 1');
1413
1414 await act(async () => {
1415 await patch(() => {
1416 const Test = React.forwardRef((props, ref) => {
1417 const [count, setCount] = React.useState(0);
1418 const handleClick = () => setCount(c => c + 1);
1419
1420 // Ensure ref is extensible
1421 const divRef = React.useRef(null);
1422 React.useEffect(() => {
1423 if (ref) {
1424 if (typeof ref === 'function') {
1425 ref(divRef.current);
1426 } else if (Object.isExtensible(ref)) {
1427 ref.current = divRef.current;
1428 }
1429 }
1430 }, [ref]);
1431
1432 return (
1433 <div ref={divRef} onClick={handleClick}>
1434 count: {count}
1435 </div>
1436 );
1437 });
1438 $RefreshReg$(Test, 'Test');
1439 return Test;
1440 });
1441 });
1442
1443 expect(container.firstChild.textContent).toBe('count: 0');
1444 await act(async () => {
1445 container.firstChild.click();
1446 });
1447 expect(container.firstChild.textContent).toBe('count: 1');
1448 }
1449 });
1450
1451 it('can update simple memo function in isolation', async () => {
1452 if (__DEV__) {
1453 await render(() => {
1454 function Hello() {
1455 const [val, setVal] = React.useState(0);
1456 return (
1457 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1458 {val}
1459 </p>
1460 );
1461 }
1462 $RefreshReg$(Hello, 'Hello');
1463
1464 return React.memo(Hello);
1465 });
1466
1467 // Bump the state before patching.
1468 const el = container.firstChild;
1469 expect(el.textContent).toBe('0');
1470 expect(el.style.color).toBe('blue');
1471 await act(() => {
1472 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1473 });
1474 expect(el.textContent).toBe('1');
1475
1476 // Perform a hot update of just the rendering function.
1477 await patch(() => {
1478 function Hello() {
1479 const [val, setVal] = React.useState(0);
1480 return (
1481 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1482 {val}
1483 </p>
1484 );
1485 }
1486 $RefreshReg$(Hello, 'Hello');
1487
1488 // Not updating the wrapper.
1489 });
1490
1491 // Assert the state was preserved but color changed.
1492 expect(container.firstChild).toBe(el);
1493 expect(el.textContent).toBe('1');
1494 expect(el.style.color).toBe('red');
1495 }
1496 });
1497
1498 it('can preserve state for memo(forwardRef)', async () => {
1499 if (__DEV__) {
1500 const OuterV1 = await render(() => {
1501 function Hello() {
1502 const [val, setVal] = React.useState(0);
1503 return (
1504 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1505 {val}
1506 </p>
1507 );
1508 }
1509 $RefreshReg$(Hello, 'Hello');
1510
1511 const Outer = React.memo(React.forwardRef(() => <Hello />));
1512 $RefreshReg$(Outer, 'Outer');
1513 return Outer;
1514 });
1515
1516 // Bump the state before patching.
1517 const el = container.firstChild;
1518 expect(el.textContent).toBe('0');
1519 expect(el.style.color).toBe('blue');
1520 await act(() => {
1521 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1522 });
1523 expect(el.textContent).toBe('1');
1524
1525 // Perform a hot update.
1526 const OuterV2 = await patch(() => {
1527 function Hello() {
1528 const [val, setVal] = React.useState(0);
1529 return (
1530 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1531 {val}
1532 </p>
1533 );
1534 }
1535 $RefreshReg$(Hello, 'Hello');
1536
1537 const Outer = React.memo(React.forwardRef(() => <Hello />));
1538 $RefreshReg$(Outer, 'Outer');
1539 return Outer;
1540 });
1541
1542 // Assert the state was preserved but color changed.
1543 expect(container.firstChild).toBe(el);
1544 expect(el.textContent).toBe('1');
1545 expect(el.style.color).toBe('red');
1546
1547 // Bump the state again.
1548 await act(() => {
1549 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1550 });
1551 expect(container.firstChild).toBe(el);
1552 expect(el.textContent).toBe('2');
1553 expect(el.style.color).toBe('red');
1554
1555 // Perform top-down renders with both fresh and stale types.
1556 // Neither should change the state or color.
1557 // They should always resolve to the latest version.
1558 await render(() => OuterV1);
1559 await render(() => OuterV2);
1560 await render(() => OuterV1);
1561 expect(container.firstChild).toBe(el);
1562 expect(el.textContent).toBe('2');
1563 expect(el.style.color).toBe('red');
1564
1565 // Finally, a render with incompatible type should reset it.
1566 await render(() => {
1567 function Hello() {
1568 const [val, setVal] = React.useState(0);
1569 return (
1570 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1571 {val}
1572 </p>
1573 );
1574 }
1575 $RefreshReg$(Hello, 'Hello');
1576
1577 // Note: no wrapper this time.
1578 return Hello;
1579 });
1580
1581 expect(container.firstChild).not.toBe(el);
1582 const newEl = container.firstChild;
1583 expect(newEl.textContent).toBe('0');
1584 expect(newEl.style.color).toBe('blue');
1585 }
1586 });
1587
1588 it('can preserve state for lazy after resolution', async () => {
1589 if (__DEV__) {
1590 let resolve;
1591 const AppV1 = await render(() => {
1592 function Hello() {
1593 const [val, setVal] = React.useState(0);
1594 return (
1595 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1596 {val}
1597 </p>
1598 );
1599 }
1600 $RefreshReg$(Hello, 'Hello');
1601
1602 const Outer = React.lazy(
1603 () =>
1604 new Promise(_resolve => {
1605 resolve = () => _resolve({default: Hello});
1606 }),
1607 );
1608 $RefreshReg$(Outer, 'Outer');
1609
1610 function App() {
1611 return (
1612 <React.Suspense fallback={<p>Loading</p>}>
1613 <Outer />
1614 </React.Suspense>
1615 );
1616 }
1617 $RefreshReg$(App, 'App');
1618
1619 return App;
1620 });
1621
1622 expect(container.textContent).toBe('Loading');
1623 await act(() => {
1624 resolve();
1625 });
1626 expect(container.textContent).toBe('0');
1627
1628 // Bump the state before patching.
1629 const el = container.firstChild;
1630 expect(el.textContent).toBe('0');
1631 expect(el.style.color).toBe('blue');
1632 await act(() => {
1633 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1634 });
1635 expect(el.textContent).toBe('1');
1636
1637 // Perform a hot update.
1638 const AppV2 = await patch(() => {
1639 function Hello() {
1640 const [val, setVal] = React.useState(0);
1641 return (
1642 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1643 {val}
1644 </p>
1645 );
1646 }
1647 $RefreshReg$(Hello, 'Hello');
1648
1649 const Outer = React.lazy(
1650 () =>
1651 new Promise(_resolve => {
1652 resolve = () => _resolve({default: Hello});
1653 }),
1654 );
1655 $RefreshReg$(Outer, 'Outer');
1656
1657 function App() {
1658 return (
1659 <React.Suspense fallback={<p>Loading</p>}>
1660 <Outer />
1661 </React.Suspense>
1662 );
1663 }
1664 $RefreshReg$(App, 'App');
1665
1666 return App;
1667 });
1668
1669 // Assert the state was preserved but color changed.
1670 expect(container.firstChild).toBe(el);
1671 expect(el.textContent).toBe('1');
1672 expect(el.style.color).toBe('red');
1673
1674 // Bump the state again.
1675 await act(() => {
1676 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1677 });
1678 expect(container.firstChild).toBe(el);
1679 expect(el.textContent).toBe('2');
1680 expect(el.style.color).toBe('red');
1681
1682 // Perform top-down renders with both fresh and stale types.
1683 // Neither should change the state or color.
1684 // They should always resolve to the latest version.
1685 await render(() => AppV1);
1686 await render(() => AppV2);
1687 await render(() => AppV1);
1688 expect(container.firstChild).toBe(el);
1689 expect(el.textContent).toBe('2');
1690 expect(el.style.color).toBe('red');
1691
1692 // Finally, a render with incompatible type should reset it.
1693 await render(() => {
1694 function Hello() {
1695 const [val, setVal] = React.useState(0);
1696 return (
1697 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1698 {val}
1699 </p>
1700 );
1701 }
1702 $RefreshReg$(Hello, 'Hello');
1703
1704 // Note: no lazy wrapper this time.
1705
1706 function App() {
1707 return (
1708 <React.Suspense fallback={<p>Loading</p>}>
1709 <Hello />
1710 </React.Suspense>
1711 );
1712 }
1713 $RefreshReg$(App, 'App');
1714
1715 return App;
1716 });
1717
1718 expect(container.firstChild).not.toBe(el);
1719 const newEl = container.firstChild;
1720 expect(newEl.textContent).toBe('0');
1721 expect(newEl.style.color).toBe('blue');
1722 }
1723 });
1724
1725 it('can patch lazy before resolution', async () => {
1726 if (__DEV__) {
1727 let resolve;
1728 await render(() => {
1729 function Hello() {
1730 const [val, setVal] = React.useState(0);
1731 return (
1732 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1733 {val}
1734 </p>
1735 );
1736 }
1737 $RefreshReg$(Hello, 'Hello');
1738
1739 const Outer = React.lazy(
1740 () =>
1741 new Promise(_resolve => {
1742 resolve = () => _resolve({default: Hello});
1743 }),
1744 );
1745 $RefreshReg$(Outer, 'Outer');
1746
1747 function App() {
1748 return (
1749 <React.Suspense fallback={<p>Loading</p>}>
1750 <Outer />
1751 </React.Suspense>
1752 );
1753 }
1754
1755 return App;
1756 });
1757
1758 expect(container.textContent).toBe('Loading');
1759
1760 // Perform a hot update.
1761 await patch(() => {
1762 function Hello() {
1763 const [val, setVal] = React.useState(0);
1764 return (
1765 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1766 {val}
1767 </p>
1768 );
1769 }
1770 $RefreshReg$(Hello, 'Hello');
1771 });
1772
1773 await act(() => {
1774 resolve();
1775 });
1776
1777 // Expect different color on initial mount.
1778 const el = container.firstChild;
1779 expect(el.textContent).toBe('0');
1780 expect(el.style.color).toBe('red');
1781
1782 // Bump state.
1783 await act(() => {
1784 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1785 });
1786 expect(container.firstChild).toBe(el);
1787 expect(el.textContent).toBe('1');
1788 expect(el.style.color).toBe('red');
1789
1790 // Test another reload.
1791 await patch(() => {
1792 function Hello() {
1793 const [val, setVal] = React.useState(0);
1794 return (
1795 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
1796 {val}
1797 </p>
1798 );
1799 }
1800 $RefreshReg$(Hello, 'Hello');
1801 });
1802 expect(container.firstChild).toBe(el);
1803 expect(el.textContent).toBe('1');
1804 expect(el.style.color).toBe('orange');
1805 }
1806 });
1807
1808 it('can patch lazy(forwardRef) before resolution', async () => {
1809 if (__DEV__) {
1810 let resolve;
1811 await render(() => {
1812 function renderHello() {
1813 const [val, setVal] = React.useState(0);
1814 return (
1815 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1816 {val}
1817 </p>
1818 );
1819 }
1820 const Hello = React.forwardRef(renderHello);
1821 $RefreshReg$(Hello, 'Hello');
1822
1823 const Outer = React.lazy(
1824 () =>
1825 new Promise(_resolve => {
1826 resolve = () => _resolve({default: Hello});
1827 }),
1828 );
1829 $RefreshReg$(Outer, 'Outer');
1830
1831 function App() {
1832 return (
1833 <React.Suspense fallback={<p>Loading</p>}>
1834 <Outer />
1835 </React.Suspense>
1836 );
1837 }
1838
1839 return App;
1840 });
1841
1842 expect(container.textContent).toBe('Loading');
1843
1844 // Perform a hot update.
1845 await patch(() => {
1846 function renderHello() {
1847 const [val, setVal] = React.useState(0);
1848 return (
1849 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1850 {val}
1851 </p>
1852 );
1853 }
1854 const Hello = React.forwardRef(renderHello);
1855 $RefreshReg$(Hello, 'Hello');
1856 });
1857
1858 await act(() => {
1859 resolve();
1860 });
1861
1862 // Expect different color on initial mount.
1863 const el = container.firstChild;
1864 expect(el.textContent).toBe('0');
1865 expect(el.style.color).toBe('red');
1866
1867 // Bump state.
1868 await act(() => {
1869 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1870 });
1871 expect(container.firstChild).toBe(el);
1872 expect(el.textContent).toBe('1');
1873 expect(el.style.color).toBe('red');
1874
1875 // Test another reload.
1876 await patch(() => {
1877 function renderHello() {
1878 const [val, setVal] = React.useState(0);
1879 return (
1880 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
1881 {val}
1882 </p>
1883 );
1884 }
1885 const Hello = React.forwardRef(renderHello);
1886 $RefreshReg$(Hello, 'Hello');
1887 });
1888 expect(container.firstChild).toBe(el);
1889 expect(el.textContent).toBe('1');
1890 expect(el.style.color).toBe('orange');
1891 }
1892 });
1893
1894 it('can patch lazy(memo) before resolution', async () => {
1895 if (__DEV__) {
1896 let resolve;
1897 await render(() => {
1898 function renderHello() {
1899 const [val, setVal] = React.useState(0);
1900 return (
1901 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1902 {val}
1903 </p>
1904 );
1905 }
1906 const Hello = React.memo(renderHello);
1907 $RefreshReg$(Hello, 'Hello');
1908
1909 const Outer = React.lazy(
1910 () =>
1911 new Promise(_resolve => {
1912 resolve = () => _resolve({default: Hello});
1913 }),
1914 );
1915 $RefreshReg$(Outer, 'Outer');
1916
1917 function App() {
1918 return (
1919 <React.Suspense fallback={<p>Loading</p>}>
1920 <Outer />
1921 </React.Suspense>
1922 );
1923 }
1924
1925 return App;
1926 });
1927
1928 expect(container.textContent).toBe('Loading');
1929
1930 // Perform a hot update.
1931 await patch(() => {
1932 function renderHello() {
1933 const [val, setVal] = React.useState(0);
1934 return (
1935 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
1936 {val}
1937 </p>
1938 );
1939 }
1940 const Hello = React.memo(renderHello);
1941 $RefreshReg$(Hello, 'Hello');
1942 });
1943
1944 await act(() => {
1945 resolve();
1946 });
1947
1948 // Expect different color on initial mount.
1949 const el = container.firstChild;
1950 expect(el.textContent).toBe('0');
1951 expect(el.style.color).toBe('red');
1952
1953 // Bump state.
1954 await act(() => {
1955 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
1956 });
1957 expect(container.firstChild).toBe(el);
1958 expect(el.textContent).toBe('1');
1959 expect(el.style.color).toBe('red');
1960
1961 // Test another reload.
1962 await patch(() => {
1963 function renderHello() {
1964 const [val, setVal] = React.useState(0);
1965 return (
1966 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
1967 {val}
1968 </p>
1969 );
1970 }
1971 const Hello = React.memo(renderHello);
1972 $RefreshReg$(Hello, 'Hello');
1973 });
1974 expect(container.firstChild).toBe(el);
1975 expect(el.textContent).toBe('1');
1976 expect(el.style.color).toBe('orange');
1977 }
1978 });
1979
1980 it('can patch lazy(memo(forwardRef)) before resolution', async () => {
1981 if (__DEV__) {
1982 let resolve;
1983 await render(() => {
1984 function renderHello() {
1985 const [val, setVal] = React.useState(0);
1986 return (
1987 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
1988 {val}
1989 </p>
1990 );
1991 }
1992 const Hello = React.memo(React.forwardRef(renderHello));
1993 $RefreshReg$(Hello, 'Hello');
1994
1995 const Outer = React.lazy(
1996 () =>
1997 new Promise(_resolve => {
1998 resolve = () => _resolve({default: Hello});
1999 }),
2000 );
2001 $RefreshReg$(Outer, 'Outer');
2002
2003 function App() {
2004 return (
2005 <React.Suspense fallback={<p>Loading</p>}>
2006 <Outer />
2007 </React.Suspense>
2008 );
2009 }
2010
2011 return App;
2012 });
2013
2014 expect(container.textContent).toBe('Loading');
2015
2016 // Perform a hot update.
2017 await patch(() => {
2018 function renderHello() {
2019 const [val, setVal] = React.useState(0);
2020 return (
2021 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2022 {val}
2023 </p>
2024 );
2025 }
2026 const Hello = React.memo(React.forwardRef(renderHello));
2027 $RefreshReg$(Hello, 'Hello');
2028 });
2029
2030 await act(() => {
2031 resolve();
2032 });
2033
2034 // Expect different color on initial mount.
2035 const el = container.firstChild;
2036 expect(el.textContent).toBe('0');
2037 expect(el.style.color).toBe('red');
2038
2039 // Bump state.
2040 await act(() => {
2041 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2042 });
2043 expect(container.firstChild).toBe(el);
2044 expect(el.textContent).toBe('1');
2045 expect(el.style.color).toBe('red');
2046
2047 // Test another reload.
2048 await patch(() => {
2049 function renderHello() {
2050 const [val, setVal] = React.useState(0);
2051 return (
2052 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
2053 {val}
2054 </p>
2055 );
2056 }
2057 const Hello = React.memo(React.forwardRef(renderHello));
2058 $RefreshReg$(Hello, 'Hello');
2059 });
2060 expect(container.firstChild).toBe(el);
2061 expect(el.textContent).toBe('1');
2062 expect(el.style.color).toBe('orange');
2063 }
2064 });
2065
2066 it('only patches the fallback tree while suspended', async () => {
2067 if (__DEV__) {
2068 const AppV1 = await render(
2069 () => {
2070 function Hello({children}) {
2071 const [val, setVal] = React.useState(0);
2072 return (
2073 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2074 {children} {val}
2075 </p>
2076 );
2077 }
2078 $RefreshReg$(Hello, 'Hello');
2079
2080 function Never() {
2081 throw new Promise(resolve => {});
2082 }
2083
2084 function App({shouldSuspend}) {
2085 return (
2086 <React.Suspense fallback={<Hello>Fallback</Hello>}>
2087 <Hello>Content</Hello>
2088 {shouldSuspend && <Never />}
2089 </React.Suspense>
2090 );
2091 }
2092
2093 return App;
2094 },
2095 {shouldSuspend: false},
2096 );
2097
2098 // We start with just the primary tree.
2099 expect(container.childNodes.length).toBe(1);
2100 const primaryChild = container.firstChild;
2101 expect(primaryChild.textContent).toBe('Content 0');
2102 expect(primaryChild.style.color).toBe('blue');
2103 expect(primaryChild.style.display).toBe('');
2104
2105 // Bump primary content state.
2106 await act(() => {
2107 primaryChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2108 });
2109 expect(container.childNodes.length).toBe(1);
2110 expect(container.childNodes[0]).toBe(primaryChild);
2111 expect(primaryChild.textContent).toBe('Content 1');
2112 expect(primaryChild.style.color).toBe('blue');
2113 expect(primaryChild.style.display).toBe('');
2114
2115 // Perform a hot update.
2116 await patch(() => {
2117 function Hello({children}) {
2118 const [val, setVal] = React.useState(0);
2119 return (
2120 <p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
2121 {children} {val}
2122 </p>
2123 );
2124 }
2125 $RefreshReg$(Hello, 'Hello');
2126 });
2127 expect(container.childNodes.length).toBe(1);
2128 expect(container.childNodes[0]).toBe(primaryChild);
2129 expect(primaryChild.textContent).toBe('Content 1');
2130 expect(primaryChild.style.color).toBe('green');
2131 expect(primaryChild.style.display).toBe('');
2132
2133 // Now force the tree to suspend.
2134 await render(() => AppV1, {shouldSuspend: true});
2135
2136 // Expect to see two trees, one of them is hidden.
2137 expect(container.childNodes.length).toBe(2);
2138 expect(container.childNodes[0]).toBe(primaryChild);
2139 const fallbackChild = container.childNodes[1];
2140 expect(primaryChild.textContent).toBe('Content 1');
2141 expect(primaryChild.style.color).toBe('green');
2142 expect(primaryChild.style.display).toBe('none');
2143 expect(fallbackChild.textContent).toBe('Fallback 0');
2144 expect(fallbackChild.style.color).toBe('green');
2145 expect(fallbackChild.style.display).toBe('');
2146
2147 // Bump fallback state.
2148 await act(() => {
2149 fallbackChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2150 });
2151 expect(container.childNodes.length).toBe(2);
2152 expect(container.childNodes[0]).toBe(primaryChild);
2153 expect(container.childNodes[1]).toBe(fallbackChild);
2154 expect(primaryChild.textContent).toBe('Content 1');
2155 expect(primaryChild.style.color).toBe('green');
2156 expect(primaryChild.style.display).toBe('none');
2157 expect(fallbackChild.textContent).toBe('Fallback 1');
2158 expect(fallbackChild.style.color).toBe('green');
2159 expect(fallbackChild.style.display).toBe('');
2160
2161 // Perform a hot update.
2162 await patch(() => {
2163 function Hello({children}) {
2164 const [val, setVal] = React.useState(0);
2165 return (
2166 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2167 {children} {val}
2168 </p>
2169 );
2170 }
2171 $RefreshReg$(Hello, 'Hello');
2172 });
2173
2174 // Only update color in the visible child
2175 expect(container.childNodes.length).toBe(2);
2176 expect(container.childNodes[0]).toBe(primaryChild);
2177 expect(container.childNodes[1]).toBe(fallbackChild);
2178 expect(primaryChild.textContent).toBe('Content 1');
2179 expect(primaryChild.style.color).toBe('green');
2180 expect(primaryChild.style.display).toBe('none');
2181 expect(fallbackChild.textContent).toBe('Fallback 1');
2182 expect(fallbackChild.style.color).toBe('red');
2183 expect(fallbackChild.style.display).toBe('');
2184
2185 // Only primary tree should exist now:
2186 await render(() => AppV1, {shouldSuspend: false});
2187 expect(container.childNodes.length).toBe(1);
2188 expect(container.childNodes[0]).toBe(primaryChild);
2189 expect(primaryChild.textContent).toBe('Content 1');
2190 expect(primaryChild.style.color).toBe('red');
2191 expect(primaryChild.style.display).toBe('');
2192
2193 // Perform a hot update.
2194 await patch(() => {
2195 function Hello({children}) {
2196 const [val, setVal] = React.useState(0);
2197 return (
2198 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
2199 {children} {val}
2200 </p>
2201 );
2202 }
2203 $RefreshReg$(Hello, 'Hello');
2204 });
2205 expect(container.childNodes.length).toBe(1);
2206 expect(container.childNodes[0]).toBe(primaryChild);
2207 expect(primaryChild.textContent).toBe('Content 1');
2208 expect(primaryChild.style.color).toBe('orange');
2209 expect(primaryChild.style.display).toBe('');
2210 }
2211 });
2212
2213 it('does not re-render ancestor components unnecessarily during a hot update', async () => {
2214 if (__DEV__) {
2215 let appRenders = 0;
2216
2217 await render(() => {
2218 function Hello() {
2219 const [val, setVal] = React.useState(0);
2220 return (
2221 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2222 {val}
2223 </p>
2224 );
2225 }
2226 $RefreshReg$(Hello, 'Hello');
2227 function App() {
2228 appRenders++;
2229 return <Hello />;
2230 }
2231 $RefreshReg$(App, 'App');
2232 return App;
2233 });
2234
2235 expect(appRenders).toBe(1);
2236
2237 // Bump the state before patching.
2238 const el = container.firstChild;
2239 expect(el.textContent).toBe('0');
2240 expect(el.style.color).toBe('blue');
2241 await act(() => {
2242 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2243 });
2244 expect(el.textContent).toBe('1');
2245
2246 // No re-renders from the top.
2247 expect(appRenders).toBe(1);
2248
2249 // Perform a hot update for Hello only.
2250 await patch(() => {
2251 function Hello() {
2252 const [val, setVal] = React.useState(0);
2253 return (
2254 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2255 {val}
2256 </p>
2257 );
2258 }
2259 $RefreshReg$(Hello, 'Hello');
2260 });
2261
2262 // Assert the state was preserved but color changed.
2263 expect(container.firstChild).toBe(el);
2264 expect(el.textContent).toBe('1');
2265 expect(el.style.color).toBe('red');
2266
2267 // Still no re-renders from the top.
2268 expect(appRenders).toBe(1);
2269
2270 // Bump the state.
2271 await act(() => {
2272 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2273 });
2274 expect(el.textContent).toBe('2');
2275
2276 // Still no re-renders from the top.
2277 expect(appRenders).toBe(1);
2278 }
2279 });
2280
2281 it('batches re-renders during a hot update', async () => {
2282 if (__DEV__) {
2283 let helloRenders = 0;
2284
2285 await render(() => {
2286 function Hello({children}) {
2287 helloRenders++;
2288 return <div>X{children}X</div>;
2289 }
2290 $RefreshReg$(Hello, 'Hello');
2291
2292 function App() {
2293 return (
2294 <Hello>
2295 <Hello>
2296 <Hello />
2297 </Hello>
2298 <Hello>
2299 <Hello />
2300 </Hello>
2301 </Hello>
2302 );
2303 }
2304 return App;
2305 });
2306 expect(helloRenders).toBe(5);
2307 expect(container.textContent).toBe('XXXXXXXXXX');
2308 helloRenders = 0;
2309
2310 await patch(() => {
2311 function Hello({children}) {
2312 helloRenders++;
2313 return <div>O{children}O</div>;
2314 }
2315 $RefreshReg$(Hello, 'Hello');
2316 });
2317 expect(helloRenders).toBe(5);
2318 expect(container.textContent).toBe('OOOOOOOOOO');
2319 }
2320 });
2321
2322 it('does not leak state between components', async () => {
2323 if (__DEV__) {
2324 const AppV1 = await render(
2325 () => {
2326 function Hello1() {
2327 const [val, setVal] = React.useState(0);
2328 return (
2329 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2330 {val}
2331 </p>
2332 );
2333 }
2334 $RefreshReg$(Hello1, 'Hello1');
2335 function Hello2() {
2336 const [val, setVal] = React.useState(0);
2337 return (
2338 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2339 {val}
2340 </p>
2341 );
2342 }
2343 $RefreshReg$(Hello2, 'Hello2');
2344 function App({cond}) {
2345 return cond ? <Hello1 /> : <Hello2 />;
2346 }
2347 $RefreshReg$(App, 'App');
2348 return App;
2349 },
2350 {cond: false},
2351 );
2352
2353 // Bump the state before patching.
2354 const el = container.firstChild;
2355 expect(el.textContent).toBe('0');
2356 expect(el.style.color).toBe('blue');
2357 await act(() => {
2358 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2359 });
2360 expect(el.textContent).toBe('1');
2361
2362 // Switch the condition, flipping inner content.
2363 // This should reset the state.
2364 await render(() => AppV1, {cond: true});
2365 const el2 = container.firstChild;
2366 expect(el2).not.toBe(el);
2367 expect(el2.textContent).toBe('0');
2368 expect(el2.style.color).toBe('blue');
2369
2370 // Bump it again.
2371 await act(() => {
2372 el2.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2373 });
2374 expect(el2.textContent).toBe('1');
2375
2376 // Perform a hot update for both inner components.
2377 await patch(() => {
2378 function Hello1() {
2379 const [val, setVal] = React.useState(0);
2380 return (
2381 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2382 {val}
2383 </p>
2384 );
2385 }
2386 $RefreshReg$(Hello1, 'Hello1');
2387 function Hello2() {
2388 const [val, setVal] = React.useState(0);
2389 return (
2390 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2391 {val}
2392 </p>
2393 );
2394 }
2395 $RefreshReg$(Hello2, 'Hello2');
2396 });
2397
2398 // Assert the state was preserved but color changed.
2399 expect(container.firstChild).toBe(el2);
2400 expect(el2.textContent).toBe('1');
2401 expect(el2.style.color).toBe('red');
2402
2403 // Flip the condition again.
2404 await render(() => AppV1, {cond: false});
2405 const el3 = container.firstChild;
2406 expect(el3).not.toBe(el2);
2407 expect(el3.textContent).toBe('0');
2408 expect(el3.style.color).toBe('red');
2409 }
2410 });
2411
2412 it('can force remount by changing signature', async () => {
2413 if (__DEV__) {
2414 const HelloV1 = await render(() => {
2415 function Hello() {
2416 const [val, setVal] = React.useState(0);
2417 return (
2418 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2419 {val}
2420 </p>
2421 );
2422 }
2423 $RefreshReg$(Hello, 'Hello');
2424 // When this changes, we'll expect a remount:
2425 $RefreshSig$(Hello, '1');
2426 return Hello;
2427 });
2428
2429 // Bump the state before patching.
2430 const el = container.firstChild;
2431 expect(el.textContent).toBe('0');
2432 expect(el.style.color).toBe('blue');
2433 await act(() => {
2434 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2435 });
2436 expect(el.textContent).toBe('1');
2437
2438 // Perform a hot update.
2439 const HelloV2 = await patch(() => {
2440 function Hello() {
2441 const [val, setVal] = React.useState(0);
2442 return (
2443 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2444 {val}
2445 </p>
2446 );
2447 }
2448 $RefreshReg$(Hello, 'Hello');
2449 // The signature hasn't changed since the last time:
2450 $RefreshSig$(Hello, '1');
2451 return Hello;
2452 });
2453
2454 // Assert the state was preserved but color changed.
2455 expect(container.firstChild).toBe(el);
2456 expect(el.textContent).toBe('1');
2457 expect(el.style.color).toBe('red');
2458
2459 // Perform a hot update.
2460 const HelloV3 = await patch(() => {
2461 function Hello() {
2462 const [val, setVal] = React.useState(0);
2463 return (
2464 <p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
2465 {val}
2466 </p>
2467 );
2468 }
2469 // We're changing the signature now so it will remount:
2470 $RefreshReg$(Hello, 'Hello');
2471 $RefreshSig$(Hello, '2');
2472 return Hello;
2473 });
2474
2475 // Expect a remount.
2476 expect(container.firstChild).not.toBe(el);
2477 const newEl = container.firstChild;
2478 expect(newEl.textContent).toBe('0');
2479 expect(newEl.style.color).toBe('yellow');
2480
2481 // Bump state again.
2482 await act(() => {
2483 newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2484 });
2485 expect(newEl.textContent).toBe('1');
2486 expect(newEl.style.color).toBe('yellow');
2487
2488 // Perform top-down renders with both fresh and stale types.
2489 // Neither should change the state or color.
2490 // They should always resolve to the latest version.
2491 await render(() => HelloV1);
2492 await render(() => HelloV2);
2493 await render(() => HelloV3);
2494 await render(() => HelloV2);
2495 await render(() => HelloV1);
2496 expect(container.firstChild).toBe(newEl);
2497 expect(newEl.textContent).toBe('1');
2498 expect(newEl.style.color).toBe('yellow');
2499
2500 // Verify we can patch again while preserving the signature.
2501 await patch(() => {
2502 function Hello() {
2503 const [val, setVal] = React.useState(0);
2504 return (
2505 <p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
2506 {val}
2507 </p>
2508 );
2509 }
2510 // Same signature as last time.
2511 $RefreshReg$(Hello, 'Hello');
2512 $RefreshSig$(Hello, '2');
2513 return Hello;
2514 });
2515
2516 expect(container.firstChild).toBe(newEl);
2517 expect(newEl.textContent).toBe('1');
2518 expect(newEl.style.color).toBe('purple');
2519
2520 // Check removing the signature also causes a remount.
2521 await patch(() => {
2522 function Hello() {
2523 const [val, setVal] = React.useState(0);
2524 return (
2525 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
2526 {val}
2527 </p>
2528 );
2529 }
2530 // No signature this time.
2531 $RefreshReg$(Hello, 'Hello');
2532 return Hello;
2533 });
2534
2535 // Expect a remount.
2536 expect(container.firstChild).not.toBe(newEl);
2537 const finalEl = container.firstChild;
2538 expect(finalEl.textContent).toBe('0');
2539 expect(finalEl.style.color).toBe('orange');
2540 }
2541 });
2542
2543 it('keeps a valid tree when forcing remount', async () => {
2544 if (__DEV__) {
2545 const HelloV1 = prepare(() => {
2546 function Hello() {
2547 return null;
2548 }
2549 $RefreshReg$(Hello, 'Hello');
2550 $RefreshSig$(Hello, '1');
2551 return Hello;
2552 });
2553
2554 const Bailout = React.memo(({children}) => {
2555 return children;
2556 });
2557
2558 // Each of those renders three instances of HelloV1,
2559 // but in different ways.
2560 const trees = [
2561 <div>
2562 <HelloV1 />
2563 <div>
2564 <HelloV1 />
2565 <Bailout>
2566 <HelloV1 />
2567 </Bailout>
2568 </div>
2569 </div>,
2570 <div>
2571 <div>
2572 <HelloV1>
2573 <HelloV1 />
2574 </HelloV1>
2575 <HelloV1 />
2576 </div>
2577 </div>,
2578 <div>
2579 <span />
2580 <HelloV1 />
2581 <HelloV1 />
2582 <HelloV1 />
2583 </div>,
2584 <div>
2585 <HelloV1 />
2586 <span />
2587 <HelloV1 />
2588 <HelloV1 />
2589 </div>,
2590 <div>
2591 <div>foo</div>
2592 <HelloV1 />
2593 <div>
2594 <HelloV1 />
2595 </div>
2596 <HelloV1 />
2597 <span />
2598 </div>,
2599 <div>
2600 <HelloV1>
2601 <span />
2602 Hello
2603 <span />
2604 </HelloV1>
2605 ,
2606 <HelloV1>
2607 <>
2608 <HelloV1 />
2609 </>
2610 </HelloV1>
2611 ,
2612 </div>,
2613 <HelloV1>
2614 <HelloV1>
2615 <Bailout>
2616 <span />
2617 <HelloV1>
2618 <span />
2619 </HelloV1>
2620 <span />
2621 </Bailout>
2622 </HelloV1>
2623 </HelloV1>,
2624 <div>
2625 <span />
2626 <HelloV1 key="0" />
2627 <HelloV1 key="1" />
2628 <HelloV1 key="2" />
2629 <span />
2630 </div>,
2631 <div>
2632 <span />
2633 {null}
2634 <HelloV1 key="1" />
2635 {null}
2636 <HelloV1 />
2637 <HelloV1 />
2638 <span />
2639 </div>,
2640 <div>
2641 <HelloV1 key="2" />
2642 <span />
2643 <HelloV1 key="0" />
2644 <span />
2645 <HelloV1 key="1" />
2646 </div>,
2647 <div>
2648 {[[<HelloV1 key="2" />]]}
2649 <span>
2650 <HelloV1 key="0" />
2651 {[null]}
2652 <HelloV1 key="1" />
2653 </span>
2654 </div>,
2655 <div>
2656 {['foo', <HelloV1 key="hi" />, null, <HelloV1 key="2" />]}
2657 <span>
2658 {[null]}
2659 <HelloV1 key="x" />
2660 </span>
2661 </div>,
2662 <HelloV1>
2663 <HelloV1>
2664 <span />
2665 <Bailout>
2666 <HelloV1>hi</HelloV1>
2667 <span />
2668 </Bailout>
2669 </HelloV1>
2670 </HelloV1>,
2671 ];
2672
2673 await act(() => {
2674 root.render(null);
2675 });
2676
2677 for (let i = 0; i < trees.length; i++) {
2678 await runRemountingStressTest(trees[i]);
2679 }
2680
2681 // Then check that each tree is resilient to updates from another tree.
2682 for (let i = 0; i < trees.length; i++) {
2683 for (let j = 0; j < trees.length; j++) {
2684 await act(() => {
2685 root.render(null);
2686 });
2687
2688 // Intentionally don't clean up between the tests:
2689 await runRemountingStressTest(trees[i]);
2690 await runRemountingStressTest(trees[j]);
2691 await runRemountingStressTest(trees[i]);
2692 }
2693 }
2694 }
2695 }, 10000);
2696
2697 async function runRemountingStressTest(tree) {
2698 await patch(() => {
2699 function Hello({children}) {
2700 return <section data-color="blue">{children}</section>;
2701 }
2702 $RefreshReg$(Hello, 'Hello');
2703 $RefreshSig$(Hello, '1');
2704 return Hello;
2705 });
2706
2707 await act(() => {
2708 root.render(tree);
2709 });
2710
2711 const elements = container.querySelectorAll('section');
2712 // Each tree above produces exactly three <section> elements:
2713 expect(elements.length).toBe(3);
2714 elements.forEach(el => {
2715 expect(el.dataset.color).toBe('blue');
2716 });
2717
2718 // Patch color without changing the signature.
2719 await patch(() => {
2720 function Hello({children}) {
2721 return <section data-color="red">{children}</section>;
2722 }
2723 $RefreshReg$(Hello, 'Hello');
2724 $RefreshSig$(Hello, '1');
2725 return Hello;
2726 });
2727
2728 const elementsAfterPatch = container.querySelectorAll('section');
2729 expect(elementsAfterPatch.length).toBe(3);
2730 elementsAfterPatch.forEach((el, index) => {
2731 // The signature hasn't changed so we expect DOM nodes to stay the same.
2732 expect(el).toBe(elements[index]);
2733 // However, the color should have changed:
2734 expect(el.dataset.color).toBe('red');
2735 });
2736
2737 // Patch color *and* change the signature.
2738 await patch(() => {
2739 function Hello({children}) {
2740 return <section data-color="orange">{children}</section>;
2741 }
2742 $RefreshReg$(Hello, 'Hello');
2743 $RefreshSig$(Hello, '2'); // Remount
2744 return Hello;
2745 });
2746
2747 const elementsAfterRemount = container.querySelectorAll('section');
2748 expect(elementsAfterRemount.length).toBe(3);
2749 elementsAfterRemount.forEach((el, index) => {
2750 // The signature changed so we expect DOM nodes to be different.
2751 expect(el).not.toBe(elements[index]);
2752 // They should all be using the new color:
2753 expect(el.dataset.color).toBe('orange');
2754 });
2755
2756 // Now patch color but *don't* change the signature.
2757 await patch(() => {
2758 function Hello({children}) {
2759 return <section data-color="black">{children}</section>;
2760 }
2761 $RefreshReg$(Hello, 'Hello');
2762 $RefreshSig$(Hello, '2'); // Same signature as before
2763 return Hello;
2764 });
2765
2766 expect(container.querySelectorAll('section').length).toBe(3);
2767 container.querySelectorAll('section').forEach((el, index) => {
2768 // The signature didn't change so DOM nodes should stay the same.
2769 expect(el).toBe(elementsAfterRemount[index]);
2770 // They should all be using the new color:
2771 expect(el.dataset.color).toBe('black');
2772 });
2773
2774 await act(() => {
2775 root.render(tree);
2776 });
2777
2778 expect(container.querySelectorAll('section').length).toBe(3);
2779 container.querySelectorAll('section').forEach((el, index) => {
2780 expect(el).toBe(elementsAfterRemount[index]);
2781 expect(el.dataset.color).toBe('black');
2782 });
2783 }
2784
2785 it('can remount on signature change within a <root> wrapper', async () => {
2786 if (__DEV__) {
2787 await testRemountingWithWrapper(Hello => Hello);
2788 }
2789 });
2790
2791 it('can remount on signature change within a simple memo wrapper', async () => {
2792 if (__DEV__) {
2793 await testRemountingWithWrapper(Hello => React.memo(Hello));
2794 }
2795 });
2796
2797 it('can remount on signature change within a lazy simple memo wrapper', async () => {
2798 if (__DEV__) {
2799 await testRemountingWithWrapper(Hello =>
2800 React.lazy(() => ({
2801 then(cb) {
2802 cb({default: React.memo(Hello)});
2803 },
2804 })),
2805 );
2806 }
2807 });
2808
2809 it('can remount on signature change within forwardRef', async () => {
2810 if (__DEV__) {
2811 await testRemountingWithWrapper(Hello => React.forwardRef(Hello));
2812 }
2813 });
2814
2815 it('can remount on signature change within forwardRef render function', async () => {
2816 if (__DEV__) {
2817 await testRemountingWithWrapper(Hello =>
2818 React.forwardRef(() => <Hello />),
2819 );
2820 }
2821 });
2822
2823 it('can remount on signature change within nested memo', async () => {
2824 if (__DEV__) {
2825 await testRemountingWithWrapper(Hello =>
2826 React.memo(React.memo(React.memo(Hello))),
2827 );
2828 }
2829 });
2830
2831 it('can remount on signature change within a memo wrapper and custom comparison', async () => {
2832 if (__DEV__) {
2833 await testRemountingWithWrapper(Hello => React.memo(Hello, () => true));
2834 }
2835 });
2836
2837 it('can remount on signature change within a class', async () => {
2838 if (__DEV__) {
2839 await testRemountingWithWrapper(Hello => {
2840 const child = <Hello />;
2841 return class Wrapper extends React.PureComponent {
2842 render() {
2843 return child;
2844 }
2845 };
2846 });
2847 }
2848 });
2849
2850 it('can remount on signature change within a context provider', async () => {
2851 if (__DEV__) {
2852 await testRemountingWithWrapper(Hello => {
2853 const Context = React.createContext();
2854 const child = (
2855 <Context.Provider value="constant">
2856 <Hello />
2857 </Context.Provider>
2858 );
2859 return function Wrapper() {
2860 return child;
2861 };
2862 });
2863 }
2864 });
2865
2866 it('can remount on signature change within a context consumer', async () => {
2867 if (__DEV__) {
2868 await testRemountingWithWrapper(Hello => {
2869 const Context = React.createContext();
2870 const child = <Context.Consumer>{() => <Hello />}</Context.Consumer>;
2871 return function Wrapper() {
2872 return child;
2873 };
2874 });
2875 }
2876 });
2877
2878 it('can remount on signature change within a suspense node', async () => {
2879 if (__DEV__) {
2880 await testRemountingWithWrapper(Hello => {
2881 // TODO: we'll probably want to test fallback trees too.
2882 const child = (
2883 <React.Suspense>
2884 <Hello />
2885 </React.Suspense>
2886 );
2887 return function Wrapper() {
2888 return child;
2889 };
2890 });
2891 }
2892 });
2893
2894 it('can remount on signature change within a mode node', async () => {
2895 if (__DEV__) {
2896 await testRemountingWithWrapper(Hello => {
2897 const child = (
2898 <React.StrictMode>
2899 <Hello />
2900 </React.StrictMode>
2901 );
2902 return function Wrapper() {
2903 return child;
2904 };
2905 });
2906 }
2907 });
2908
2909 it('can remount on signature change within a fragment node', async () => {
2910 if (__DEV__) {
2911 await testRemountingWithWrapper(Hello => {
2912 const child = (
2913 <>
2914 <Hello />
2915 </>
2916 );
2917 return function Wrapper() {
2918 return child;
2919 };
2920 });
2921 }
2922 });
2923
2924 it('can remount on signature change within multiple siblings', async () => {
2925 if (__DEV__) {
2926 await testRemountingWithWrapper(Hello => {
2927 const child = (
2928 <>
2929 <>
2930 <React.Fragment />
2931 </>
2932 <Hello />
2933 <React.Fragment />
2934 </>
2935 );
2936 return function Wrapper() {
2937 return child;
2938 };
2939 });
2940 }
2941 });
2942
2943 it('can remount on signature change within a profiler node', async () => {
2944 if (__DEV__) {
2945 await testRemountingWithWrapper(Hello => {
2946 const child = <Hello />;
2947 return function Wrapper() {
2948 return (
2949 <React.Profiler onRender={() => {}} id="foo">
2950 {child}
2951 </React.Profiler>
2952 );
2953 };
2954 });
2955 }
2956 });
2957
2958 async function testRemountingWithWrapper(wrap) {
2959 await render(() => {
2960 function Hello() {
2961 const [val, setVal] = React.useState(0);
2962 return (
2963 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
2964 {val}
2965 </p>
2966 );
2967 }
2968 $RefreshReg$(Hello, 'Hello');
2969 // When this changes, we'll expect a remount:
2970 $RefreshSig$(Hello, '1');
2971
2972 // Use the passed wrapper.
2973 // This will be different in every test.
2974 return wrap(Hello);
2975 });
2976
2977 // Bump the state before patching.
2978 const el = container.firstChild;
2979 expect(el.textContent).toBe('0');
2980 expect(el.style.color).toBe('blue');
2981 await act(() => {
2982 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
2983 });
2984 expect(el.textContent).toBe('1');
2985
2986 // Perform a hot update that doesn't remount.
2987 await patch(() => {
2988 function Hello() {
2989 const [val, setVal] = React.useState(0);
2990 return (
2991 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
2992 {val}
2993 </p>
2994 );
2995 }
2996 $RefreshReg$(Hello, 'Hello');
2997 // The signature hasn't changed since the last time:
2998 $RefreshSig$(Hello, '1');
2999 return Hello;
3000 });
3001
3002 // Assert the state was preserved but color changed.
3003 expect(container.firstChild).toBe(el);
3004 expect(el.textContent).toBe('1');
3005 expect(el.style.color).toBe('red');
3006
3007 // Perform a hot update that remounts.
3008 await patch(() => {
3009 function Hello() {
3010 const [val, setVal] = React.useState(0);
3011 return (
3012 <p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
3013 {val}
3014 </p>
3015 );
3016 }
3017 // We're changing the signature now so it will remount:
3018 $RefreshReg$(Hello, 'Hello');
3019 $RefreshSig$(Hello, '2');
3020 return Hello;
3021 });
3022
3023 // Expect a remount.
3024 expect(container.firstChild).not.toBe(el);
3025 const newEl = container.firstChild;
3026 expect(newEl.textContent).toBe('0');
3027 expect(newEl.style.color).toBe('yellow');
3028
3029 // Bump state again.
3030 await act(() => {
3031 newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
3032 });
3033 expect(newEl.textContent).toBe('1');
3034 expect(newEl.style.color).toBe('yellow');
3035
3036 // Verify we can patch again while preserving the signature.
3037 await patch(() => {
3038 function Hello() {
3039 const [val, setVal] = React.useState(0);
3040 return (
3041 <p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
3042 {val}
3043 </p>
3044 );
3045 }
3046 // Same signature as last time.
3047 $RefreshReg$(Hello, 'Hello');
3048 $RefreshSig$(Hello, '2');
3049 return Hello;
3050 });
3051
3052 expect(container.firstChild).toBe(newEl);
3053 expect(newEl.textContent).toBe('1');
3054 expect(newEl.style.color).toBe('purple');
3055
3056 // Check removing the signature also causes a remount.
3057 await patch(() => {
3058 function Hello() {
3059 const [val, setVal] = React.useState(0);
3060 return (
3061 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
3062 {val}
3063 </p>
3064 );
3065 }
3066 // No signature this time.
3067 $RefreshReg$(Hello, 'Hello');
3068 return Hello;
3069 });
3070
3071 // Expect a remount.
3072 expect(container.firstChild).not.toBe(newEl);
3073 const finalEl = container.firstChild;
3074 expect(finalEl.textContent).toBe('0');
3075 expect(finalEl.style.color).toBe('orange');
3076 }
3077
3078 it('double invokes effects after a forced remount in StrictMode', async () => {
3079 if (__DEV__) {
3080 const log = [];
3081
3082 const createAppV1 = () => {
3083 function Hello() {
3084 React.useEffect(() => {
3085 log.push('mount v1');
3086 return () => log.push('unmount v1');
3087 }, []);
3088 return <p style={{color: 'blue'}}>Hello</p>;
3089 }
3090 $RefreshReg$(Hello, 'Hello');
3091 $RefreshSig$(Hello, '1');
3092
3093 return Hello;
3094 };
3095
3096 const App = createAppV1();
3097
3098 await act(() => {
3099 root.render(
3100 <React.StrictMode>
3101 <App />
3102 </React.StrictMode>,
3103 );
3104 });
3105
3106 expect(log).toEqual(['mount v1', 'unmount v1', 'mount v1']);
3107 log.length = 0;
3108
3109 await patch(() => {
3110 function Hello() {
3111 React.useEffect(() => {
3112 log.push('mount v2');
3113 return () => log.push('unmount v2');
3114 }, []);
3115 return <p style={{color: 'red'}}>Hello</p>;
3116 }
3117 $RefreshReg$(Hello, 'Hello');
3118 $RefreshSig$(Hello, '2');
3119 return null;
3120 });
3121
3122 expect(container.firstChild.style.color).toBe('red');
3123 expect(log).toEqual(['unmount v1', 'mount v2', 'unmount v2', 'mount v2']);
3124 }
3125 });
3126
3127 it('double invokes an effect added during Fast Refresh remount in StrictMode', async () => {
3128 if (__DEV__) {
3129 const log = [];
3130
3131 const createAppV1 = () => {
3132 function Hello() {
3133 return <p style={{color: 'blue'}}>Hello</p>;
3134 }
3135 $RefreshReg$(Hello, 'Hello');
3136 $RefreshSig$(Hello, '1');
3137 return Hello;
3138 };
3139
3140 const App = createAppV1();
3141
3142 await act(() => {
3143 root.render(
3144 <React.StrictMode>
3145 <App />
3146 </React.StrictMode>,
3147 );
3148 });
3149
3150 expect(log).toEqual([]);
3151
3152 await patch(() => {
3153 function Hello() {
3154 React.useEffect(() => {
3155 log.push('mount v2');
3156 return () => log.push('unmount v2');
3157 }, []);
3158 return <p style={{color: 'red'}}>Hello</p>;
3159 }
3160 $RefreshReg$(Hello, 'Hello');
3161 $RefreshSig$(Hello, '2');
3162 return null;
3163 });
3164
3165 expect(container.firstChild.style.color).toBe('red');
3166 expect(log).toEqual(['mount v2', 'unmount v2', 'mount v2']);
3167 }
3168 });
3169
3170 it('resets hooks with dependencies on hot reload', async () => {
3171 if (__DEV__) {
3172 let useEffectWithEmptyArrayCalls = 0;
3173
3174 await render(() => {
3175 function Hello() {
3176 const [val, setVal] = React.useState(0);
3177 const tranformed = React.useMemo(() => val * 2, [val]);
3178 const handleClick = React.useCallback(() => setVal(v => v + 1), []);
3179
3180 React.useEffect(() => {
3181 useEffectWithEmptyArrayCalls++;
3182 }, []);
3183
3184 return (
3185 <p style={{color: 'blue'}} onClick={handleClick}>
3186 {tranformed}
3187 </p>
3188 );
3189 }
3190 $RefreshReg$(Hello, 'Hello');
3191 return Hello;
3192 });
3193
3194 // Bump the state before patching.
3195 const el = container.firstChild;
3196 expect(el.textContent).toBe('0');
3197 expect(el.style.color).toBe('blue');
3198 expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect ran
3199 await act(() => {
3200 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
3201 });
3202 expect(el.textContent).toBe('2'); // val * 2
3203 expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect didn't re-run
3204
3205 // Perform a hot update.
3206 await patch(() => {
3207 function Hello() {
3208 const [val, setVal] = React.useState(0);
3209 const tranformed = React.useMemo(() => val * 10, [val]);
3210 const handleClick = React.useCallback(() => setVal(v => v - 1), []);
3211
3212 React.useEffect(() => {
3213 useEffectWithEmptyArrayCalls++;
3214 }, []);
3215
3216 return (
3217 <p style={{color: 'red'}} onClick={handleClick}>
3218 {tranformed}
3219 </p>
3220 );
3221 }
3222 $RefreshReg$(Hello, 'Hello');
3223 return Hello;
3224 });
3225
3226 // Assert the state was preserved but memo was evicted.
3227 expect(container.firstChild).toBe(el);
3228 expect(el.textContent).toBe('10'); // val * 10
3229 expect(el.style.color).toBe('red');
3230 expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect re-ran
3231
3232 // This should fire the new callback which decreases the counter.
3233 await act(() => {
3234 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
3235 });
3236 expect(el.textContent).toBe('0');
3237 expect(el.style.color).toBe('red');
3238 expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect didn't re-run
3239 }
3240 });
3241
3242 // This pattern is inspired by useSubscription and similar mechanisms.
3243 it('does not get into infinite loops during render phase updates', async () => {
3244 if (__DEV__) {
3245 await render(() => {
3246 function Hello() {
3247 const source = React.useMemo(() => ({value: 10}), []);
3248 const [state, setState] = React.useState({value: null});
3249 if (state !== source) {
3250 setState(source);
3251 }
3252 return <p style={{color: 'blue'}}>{state.value}</p>;
3253 }
3254 $RefreshReg$(Hello, 'Hello');
3255 return Hello;
3256 });
3257
3258 const el = container.firstChild;
3259 expect(el.textContent).toBe('10');
3260 expect(el.style.color).toBe('blue');
3261
3262 // Perform a hot update.
3263 await patch(() => {
3264 function Hello() {
3265 const source = React.useMemo(() => ({value: 20}), []);
3266 const [state, setState] = React.useState({value: null});
3267 if (state !== source) {
3268 // This should perform a single render-phase update.
3269 setState(source);
3270 }
3271 return <p style={{color: 'red'}}>{state.value}</p>;
3272 }
3273 $RefreshReg$(Hello, 'Hello');
3274 return Hello;
3275 });
3276
3277 expect(container.firstChild).toBe(el);
3278 expect(el.textContent).toBe('20');
3279 expect(el.style.color).toBe('red');
3280 }
3281 });
3282
3283 // @gate enableLegacyHidden && __DEV__
3284 it('can hot reload offscreen components', async () => {
3285 const AppV1 = prepare(() => {
3286 function Hello() {
3287 React.useLayoutEffect(() => {
3288 Scheduler.log('Hello#layout');
3289 });
3290 const [val, setVal] = React.useState(0);
3291 return (
3292 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
3293 {val}
3294 </p>
3295 );
3296 }
3297 $RefreshReg$(Hello, 'Hello');
3298
3299 return function App({offscreen}) {
3300 React.useLayoutEffect(() => {
3301 Scheduler.log('App#layout');
3302 });
3303 return (
3304 <LegacyHiddenDiv mode={offscreen ? 'hidden' : 'visible'}>
3305 <Hello />
3306 </LegacyHiddenDiv>
3307 );
3308 };
3309 });
3310
3311 root.render(<AppV1 offscreen={true} />);
3312 await waitFor(['App#layout']);
3313 const el = container.firstChild;
3314 expect(el.hidden).toBe(true);
3315 expect(el.firstChild).toBe(null); // Offscreen content not flushed yet.
3316
3317 // Perform a hot update.
3318 patchSync(() => {
3319 function Hello() {
3320 React.useLayoutEffect(() => {
3321 Scheduler.log('Hello#layout');
3322 });
3323 const [val, setVal] = React.useState(0);
3324 return (
3325 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
3326 {val}
3327 </p>
3328 );
3329 }
3330 $RefreshReg$(Hello, 'Hello');
3331 });
3332
3333 // It's still offscreen so we don't see anything.
3334 expect(container.firstChild).toBe(el);
3335 expect(el.hidden).toBe(true);
3336 expect(el.firstChild).toBe(null);
3337
3338 // Process the offscreen updates.
3339 await waitFor(['Hello#layout']);
3340 expect(container.firstChild).toBe(el);
3341 expect(el.firstChild.textContent).toBe('0');
3342 expect(el.firstChild.style.color).toBe('red');
3343
3344 await act(() => {
3345 el.firstChild.dispatchEvent(
3346 new MouseEvent('click', {
3347 bubbles: true,
3348 }),
3349 );
3350 });
3351
3352 assertLog(['Hello#layout']);
3353 expect(el.firstChild.textContent).toBe('1');
3354 expect(el.firstChild.style.color).toBe('red');
3355
3356 // Hot reload while we're offscreen.
3357 patchSync(() => {
3358 function Hello() {
3359 React.useLayoutEffect(() => {
3360 Scheduler.log('Hello#layout');
3361 });
3362 const [val, setVal] = React.useState(0);
3363 return (
3364 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
3365 {val}
3366 </p>
3367 );
3368 }
3369 $RefreshReg$(Hello, 'Hello');
3370 });
3371
3372 // It's still offscreen so we don't see the updates.
3373 expect(container.firstChild).toBe(el);
3374 expect(el.firstChild.textContent).toBe('1');
3375 expect(el.firstChild.style.color).toBe('red');
3376
3377 // Process the offscreen updates.
3378 await waitFor(['Hello#layout']);
3379 expect(container.firstChild).toBe(el);
3380 expect(el.firstChild.textContent).toBe('1');
3381 expect(el.firstChild.style.color).toBe('orange');
3382 });
3383
3384 it('remounts failed error boundaries (componentDidCatch)', async () => {
3385 if (__DEV__) {
3386 await render(() => {
3387 function Hello() {
3388 return <h1>Hi</h1>;
3389 }
3390 $RefreshReg$(Hello, 'Hello');
3391
3392 class Boundary extends React.Component {
3393 state = {error: null};
3394 componentDidCatch(error) {
3395 this.setState({error});
3396 }
3397 render() {
3398 if (this.state.error) {
3399 return <h1>Oops: {this.state.error.message}</h1>;
3400 }
3401 return this.props.children;
3402 }
3403 }
3404
3405 function App() {
3406 return (
3407 <>
3408 <p>A</p>
3409 <Boundary>
3410 <Hello />
3411 </Boundary>
3412 <p>B</p>
3413 </>
3414 );
3415 }
3416
3417 return App;
3418 });
3419
3420 expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
3421 const firstP = container.firstChild;
3422 const secondP = firstP.nextSibling.nextSibling;
3423
3424 // Perform a hot update that fails.
3425 await patch(() => {
3426 function Hello() {
3427 throw new Error('No');
3428 }
3429 $RefreshReg$(Hello, 'Hello');
3430 });
3431
3432 expect(container.innerHTML).toBe('<p>A</p><h1>Oops: No</h1><p>B</p>');
3433 expect(container.firstChild).toBe(firstP);
3434 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
3435
3436 // Perform a hot update that fixes the error.
3437 await patch(() => {
3438 function Hello() {
3439 return <h1>Fixed!</h1>;
3440 }
3441 $RefreshReg$(Hello, 'Hello');
3442 });
3443
3444 // This should remount the error boundary (but not anything above it).
3445 expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
3446 expect(container.firstChild).toBe(firstP);
3447 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
3448
3449 // Verify next hot reload doesn't remount anything.
3450 const helloNode = container.firstChild.nextSibling;
3451 await patch(() => {
3452 function Hello() {
3453 return <h1>Nice.</h1>;
3454 }
3455 $RefreshReg$(Hello, 'Hello');
3456 });
3457 expect(container.firstChild.nextSibling).toBe(helloNode);
3458 expect(helloNode.textContent).toBe('Nice.');
3459 }
3460 });
3461
3462 it('remounts failed error boundaries (getDerivedStateFromError)', async () => {
3463 if (__DEV__) {
3464 await render(() => {
3465 function Hello() {
3466 return <h1>Hi</h1>;
3467 }
3468 $RefreshReg$(Hello, 'Hello');
3469
3470 class Boundary extends React.Component {
3471 state = {error: null};
3472 static getDerivedStateFromError(error) {
3473 return {error};
3474 }
3475 render() {
3476 if (this.state.error) {
3477 return <h1>Oops: {this.state.error.message}</h1>;
3478 }
3479 return this.props.children;
3480 }
3481 }
3482
3483 function App() {
3484 return (
3485 <>
3486 <p>A</p>
3487 <Boundary>
3488 <Hello />
3489 </Boundary>
3490 <p>B</p>
3491 </>
3492 );
3493 }
3494
3495 return App;
3496 });
3497
3498 expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
3499 const firstP = container.firstChild;
3500 const secondP = firstP.nextSibling.nextSibling;
3501
3502 // Perform a hot update that fails.
3503 await patch(() => {
3504 function Hello() {
3505 throw new Error('No');
3506 }
3507 $RefreshReg$(Hello, 'Hello');
3508 });
3509
3510 expect(container.innerHTML).toBe('<p>A</p><h1>Oops: No</h1><p>B</p>');
3511 expect(container.firstChild).toBe(firstP);
3512 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
3513
3514 // Perform a hot update that fixes the error.
3515 await patch(() => {
3516 function Hello() {
3517 return <h1>Fixed!</h1>;
3518 }
3519 $RefreshReg$(Hello, 'Hello');
3520 });
3521
3522 // This should remount the error boundary (but not anything above it).
3523 expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
3524 expect(container.firstChild).toBe(firstP);
3525 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
3526
3527 // Verify next hot reload doesn't remount anything.
3528 const helloNode = container.firstChild.nextSibling;
3529 await patch(() => {
3530 function Hello() {
3531 return <h1>Nice.</h1>;
3532 }
3533 $RefreshReg$(Hello, 'Hello');
3534 });
3535 expect(container.firstChild.nextSibling).toBe(helloNode);
3536 expect(helloNode.textContent).toBe('Nice.');
3537 }
3538 });
3539
3540 it('remounts error boundaries that failed asynchronously after hot update', async () => {
3541 if (__DEV__) {
3542 await render(() => {
3543 function Hello() {
3544 const [x] = React.useState('');
3545 React.useEffect(() => {}, []);
3546 x.slice(); // Doesn't throw initially.
3547 return <h1>Hi</h1>;
3548 }
3549 $RefreshReg$(Hello, 'Hello');
3550
3551 class Boundary extends React.Component {
3552 state = {error: null};
3553 static getDerivedStateFromError(error) {
3554 return {error};
3555 }
3556 render() {
3557 if (this.state.error) {
3558 return <h1>Oops: {this.state.error.message}</h1>;
3559 }
3560 return this.props.children;
3561 }
3562 }
3563
3564 function App() {
3565 return (
3566 <>
3567 <p>A</p>
3568 <Boundary>
3569 <Hello />
3570 </Boundary>
3571 <p>B</p>
3572 </>
3573 );
3574 }
3575
3576 return App;
3577 });
3578
3579 expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
3580 const firstP = container.firstChild;
3581 const secondP = firstP.nextSibling.nextSibling;
3582
3583 // Perform a hot update that fails.
3584 let crash;
3585 await patch(() => {
3586 function Hello() {
3587 const [x, setX] = React.useState('');
3588 React.useEffect(() => {
3589 crash = () => {
3590 setX(42); // This will crash next render.
3591 };
3592 }, []);
3593 x.slice();
3594 return <h1>Hi</h1>;
3595 }
3596 $RefreshReg$(Hello, 'Hello');
3597 });
3598
3599 expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
3600 // Run timeout inside effect:
3601 await act(() => {
3602 crash();
3603 });
3604 expect(container.innerHTML).toBe(
3605 '<p>A</p><h1>Oops: x.slice is not a function</h1><p>B</p>',
3606 );
3607 expect(container.firstChild).toBe(firstP);
3608 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
3609
3610 // Perform a hot update that fixes the error.
3611 await patch(() => {
3612 function Hello() {
3613 const [x] = React.useState('');
3614 React.useEffect(() => {}, []); // Removes the bad effect code.
3615 x.slice(); // Doesn't throw initially.
3616 return <h1>Fixed!</h1>;
3617 }
3618 $RefreshReg$(Hello, 'Hello');
3619 });
3620
3621 // This should remount the error boundary (but not anything above it).
3622 expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
3623 expect(container.firstChild).toBe(firstP);
3624 expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
3625
3626 // Verify next hot reload doesn't remount anything.
3627 const helloNode = container.firstChild.nextSibling;
3628 await patch(() => {
3629 function Hello() {
3630 const [x] = React.useState('');
3631 React.useEffect(() => {}, []);
3632 x.slice();
3633 return <h1>Nice.</h1>;
3634 }
3635 $RefreshReg$(Hello, 'Hello');
3636 });
3637
3638 expect(container.firstChild.nextSibling).toBe(helloNode);
3639 expect(helloNode.textContent).toBe('Nice.');
3640 }
3641 });
3642
3643 it('remounts a failed root on mount', async () => {
3644 if (__DEV__) {
3645 await expect(
3646 render(() => {
3647 function Hello() {
3648 throw new Error('No');
3649 }
3650 $RefreshReg$(Hello, 'Hello');
3651
3652 return Hello;
3653 }),
3654 ).rejects.toThrow('No');
3655 expect(container.innerHTML).toBe('');
3656
3657 // A bad retry
3658 await expect(async () => {
3659 await patch(() => {
3660 function Hello() {
3661 throw new Error('Not yet');
3662 }
3663 $RefreshReg$(Hello, 'Hello');
3664 });
3665 }).rejects.toThrow('Not yet');
3666 expect(container.innerHTML).toBe('');
3667
3668 // Perform a hot update that fixes the error.
3669 await patch(() => {
3670 function Hello() {
3671 return <h1>Fixed!</h1>;
3672 }
3673 $RefreshReg$(Hello, 'Hello');
3674 });
3675 // This should mount the root.
3676 expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
3677
3678 // Ensure we can keep failing and recovering later.
3679 await expect(async () => {
3680 await patch(() => {
3681 function Hello() {
3682 throw new Error('No 2');
3683 }
3684 $RefreshReg$(Hello, 'Hello');
3685 });
3686 }).rejects.toThrow('No 2');
3687 expect(container.innerHTML).toBe('');
3688 await expect(async () => {
3689 await patch(() => {
3690 function Hello() {
3691 throw new Error('Not yet 2');
3692 }
3693 $RefreshReg$(Hello, 'Hello');
3694 });
3695 }).rejects.toThrow('Not yet 2');
3696 expect(container.innerHTML).toBe('');
3697 await patch(() => {
3698 function Hello() {
3699 return <h1>Fixed 2!</h1>;
3700 }
3701 $RefreshReg$(Hello, 'Hello');
3702 });
3703 expect(container.innerHTML).toBe('<h1>Fixed 2!</h1>');
3704
3705 // Updates after intentional unmount are ignored.
3706 await act(() => {
3707 root.unmount();
3708 });
3709 await patch(() => {
3710 function Hello() {
3711 throw new Error('Ignored');
3712 }
3713 $RefreshReg$(Hello, 'Hello');
3714 });
3715 expect(container.innerHTML).toBe('');
3716 await patch(() => {
3717 function Hello() {
3718 return <h1>Ignored</h1>;
3719 }
3720 $RefreshReg$(Hello, 'Hello');
3721 });
3722 expect(container.innerHTML).toBe('');
3723 }
3724 });
3725
3726 it('does not retry an intentionally unmounted failed root', async () => {
3727 if (__DEV__) {
3728 await expect(
3729 render(() => {
3730 function Hello() {
3731 throw new Error('No');
3732 }
3733 $RefreshReg$(Hello, 'Hello');
3734
3735 return Hello;
3736 }),
3737 ).rejects.toThrow('No');
3738 expect(container.innerHTML).toBe('');
3739
3740 // Intentional unmount.
3741 await act(() => {
3742 root.unmount();
3743 });
3744
3745 // Perform a hot update that fixes the error.
3746 await patch(() => {
3747 function Hello() {
3748 return <h1>Fixed!</h1>;
3749 }
3750 $RefreshReg$(Hello, 'Hello');
3751 });
3752 // This should stay unmounted.
3753 expect(container.innerHTML).toBe('');
3754 }
3755 });
3756
3757 it('remounts a failed root on update', async () => {
3758 if (__DEV__) {
3759 await render(() => {
3760 function Hello() {
3761 return <h1>Hi</h1>;
3762 }
3763 $RefreshReg$(Hello, 'Hello');
3764
3765 return Hello;
3766 });
3767 expect(container.innerHTML).toBe('<h1>Hi</h1>');
3768
3769 // Perform a hot update that fails.
3770 // This removes the root.
3771 await expect(async () => {
3772 await patch(() => {
3773 function Hello() {
3774 throw new Error('No');
3775 }
3776 $RefreshReg$(Hello, 'Hello');
3777 });
3778 }).rejects.toThrow('No');
3779 expect(container.innerHTML).toBe('');
3780
3781 // A bad retry
3782 await expect(async () => {
3783 await patch(() => {
3784 function Hello() {
3785 throw new Error('Not yet');
3786 }
3787 $RefreshReg$(Hello, 'Hello');
3788 });
3789 }).rejects.toThrow('Not yet');
3790 expect(container.innerHTML).toBe('');
3791
3792 // Perform a hot update that fixes the error.
3793 await patch(() => {
3794 function Hello() {
3795 return <h1>Fixed!</h1>;
3796 }
3797 $RefreshReg$(Hello, 'Hello');
3798 });
3799 // This should remount the root.
3800 expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
3801
3802 // Verify next hot reload doesn't remount anything.
3803 const helloNode = container.firstChild;
3804 await patch(() => {
3805 function Hello() {
3806 return <h1>Nice.</h1>;
3807 }
3808 $RefreshReg$(Hello, 'Hello');
3809 });
3810 expect(container.firstChild).toBe(helloNode);
3811 expect(helloNode.textContent).toBe('Nice.');
3812
3813 // Break again.
3814 await expect(async () => {
3815 await patch(() => {
3816 function Hello() {
3817 throw new Error('Oops');
3818 }
3819 $RefreshReg$(Hello, 'Hello');
3820 });
3821 }).rejects.toThrow('Oops');
3822 expect(container.innerHTML).toBe('');
3823
3824 // Perform a hot update that fixes the error.
3825 await patch(() => {
3826 function Hello() {
3827 return <h1>At last.</h1>;
3828 }
3829 $RefreshReg$(Hello, 'Hello');
3830 });
3831 // This should remount the root.
3832 expect(container.innerHTML).toBe('<h1>At last.</h1>');
3833
3834 // Check we don't attempt to reverse an intentional unmount.
3835 await act(() => {
3836 root.unmount();
3837 });
3838 expect(container.innerHTML).toBe('');
3839 await patch(() => {
3840 function Hello() {
3841 return <h1>Never mind me!</h1>;
3842 }
3843 $RefreshReg$(Hello, 'Hello');
3844 });
3845 expect(container.innerHTML).toBe('');
3846
3847 // Mount a new container.
3848 root = ReactDOMClient.createRoot(container);
3849 await render(() => {
3850 function Hello() {
3851 return <h1>Hi</h1>;
3852 }
3853 $RefreshReg$(Hello, 'Hello');
3854
3855 return Hello;
3856 });
3857 expect(container.innerHTML).toBe('<h1>Hi</h1>');
3858
3859 // Break again.
3860 await expect(async () => {
3861 await patch(() => {
3862 function Hello() {
3863 throw new Error('Oops');
3864 }
3865 $RefreshReg$(Hello, 'Hello');
3866 });
3867 }).rejects.toThrow('Oops');
3868 expect(container.innerHTML).toBe('');
3869
3870 // Check we don't attempt to reverse an intentional unmount, even after an error.
3871 await act(() => {
3872 root.unmount();
3873 });
3874 expect(container.innerHTML).toBe('');
3875 await patch(() => {
3876 function Hello() {
3877 return <h1>Never mind me!</h1>;
3878 }
3879 $RefreshReg$(Hello, 'Hello');
3880 });
3881 expect(container.innerHTML).toBe('');
3882 }
3883 });
3884
3885 it('regression test: does not get into an infinite loop', async () => {
3886 if (__DEV__) {
3887 const containerA = document.createElement('div');
3888 const containerB = document.createElement('div');
3889 const rootA = ReactDOMClient.createRoot(containerA);
3890 const rootB = ReactDOMClient.createRoot(containerB);
3891
3892 // Initially, nothing interesting.
3893 const RootAV1 = () => {
3894 return 'A1';
3895 };
3896 $RefreshReg$(RootAV1, 'RootA');
3897 const RootBV1 = () => {
3898 return 'B1';
3899 };
3900 $RefreshReg$(RootBV1, 'RootB');
3901
3902 await act(() => {
3903 rootA.render(<RootAV1 />);
3904 rootB.render(<RootBV1 />);
3905 });
3906 expect(containerA.innerHTML).toBe('A1');
3907 expect(containerB.innerHTML).toBe('B1');
3908
3909 // Then make the first root fail.
3910 const RootAV2 = () => {
3911 throw new Error('A2!');
3912 };
3913 $RefreshReg$(RootAV2, 'RootA');
3914 await expect(
3915 act(() => {
3916 ReactFreshRuntime.performReactRefresh();
3917 }),
3918 ).rejects.toThrow('A2!');
3919 expect(containerA.innerHTML).toBe('');
3920 expect(containerB.innerHTML).toBe('B1');
3921
3922 // Then patch the first root, but make it fail in the commit phase.
3923 // This used to trigger an infinite loop due to a list of failed roots
3924 // being mutated while it was being iterated on.
3925 const RootAV3 = () => {
3926 React.useLayoutEffect(() => {
3927 throw new Error('A3!');
3928 }, []);
3929 return 'A3';
3930 };
3931 $RefreshReg$(RootAV3, 'RootA');
3932 await expect(
3933 act(() => {
3934 ReactFreshRuntime.performReactRefresh();
3935 }),
3936 ).rejects.toThrow('A3!');
3937 expect(containerA.innerHTML).toBe('');
3938 expect(containerB.innerHTML).toBe('B1');
3939
3940 const RootAV4 = () => {
3941 return 'A4';
3942 };
3943 $RefreshReg$(RootAV4, 'RootA');
3944 await act(() => {
3945 ReactFreshRuntime.performReactRefresh();
3946 });
3947 expect(containerA.innerHTML).toBe('A4');
3948 expect(containerB.innerHTML).toBe('B1');
3949 }
3950 });
3951
3952 it('remounts classes on every edit', async () => {
3953 if (__DEV__) {
3954 const HelloV1 = await render(() => {
3955 class Hello extends React.Component {
3956 state = {count: 0};
3957 handleClick = () => {
3958 this.setState(prev => ({
3959 count: prev.count + 1,
3960 }));
3961 };
3962 render() {
3963 return (
3964 <p style={{color: 'blue'}} onClick={this.handleClick}>
3965 {this.state.count}
3966 </p>
3967 );
3968 }
3969 }
3970 // For classes, we wouldn't do this call via Babel plugin.
3971 // Instead, we'd do it at module boundaries.
3972 // Normally classes would get a different type and remount anyway,
3973 // but at module boundaries we may want to prevent propagation.
3974 // However we still want to force a remount and use latest version.
3975 $RefreshReg$(Hello, 'Hello');
3976 return Hello;
3977 });
3978
3979 // Bump the state before patching.
3980 const el = container.firstChild;
3981 expect(el.textContent).toBe('0');
3982 expect(el.style.color).toBe('blue');
3983 await act(() => {
3984 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
3985 });
3986 expect(el.textContent).toBe('1');
3987
3988 // Perform a hot update.
3989 const HelloV2 = await patch(() => {
3990 class Hello extends React.Component {
3991 state = {count: 0};
3992 handleClick = () => {
3993 this.setState(prev => ({
3994 count: prev.count + 1,
3995 }));
3996 };
3997 render() {
3998 return (
3999 <p style={{color: 'red'}} onClick={this.handleClick}>
4000 {this.state.count}
4001 </p>
4002 );
4003 }
4004 }
4005 $RefreshReg$(Hello, 'Hello');
4006 return Hello;
4007 });
4008
4009 // It should have remounted the class.
4010 expect(container.firstChild).not.toBe(el);
4011 const newEl = container.firstChild;
4012 expect(newEl.textContent).toBe('0');
4013 expect(newEl.style.color).toBe('red');
4014 await act(() => {
4015 newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
4016 });
4017 expect(newEl.textContent).toBe('1');
4018
4019 // Now top-level renders of both types resolve to latest.
4020 await render(() => HelloV1);
4021 await render(() => HelloV2);
4022 expect(container.firstChild).toBe(newEl);
4023 expect(newEl.style.color).toBe('red');
4024 expect(newEl.textContent).toBe('1');
4025
4026 const HelloV3 = await patch(() => {
4027 class Hello extends React.Component {
4028 state = {count: 0};
4029 handleClick = () => {
4030 this.setState(prev => ({
4031 count: prev.count + 1,
4032 }));
4033 };
4034 render() {
4035 return (
4036 <p style={{color: 'orange'}} onClick={this.handleClick}>
4037 {this.state.count}
4038 </p>
4039 );
4040 }
4041 }
4042 $RefreshReg$(Hello, 'Hello');
4043 return Hello;
4044 });
4045
4046 // It should have remounted the class again.
4047 expect(container.firstChild).not.toBe(el);
4048 const finalEl = container.firstChild;
4049 expect(finalEl.textContent).toBe('0');
4050 expect(finalEl.style.color).toBe('orange');
4051 await act(() => {
4052 finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
4053 });
4054 expect(finalEl.textContent).toBe('1');
4055
4056 await render(() => HelloV3);
4057 await render(() => HelloV2);
4058 await render(() => HelloV1);
4059 expect(container.firstChild).toBe(finalEl);
4060 expect(finalEl.style.color).toBe('orange');
4061 expect(finalEl.textContent).toBe('1');
4062 }
4063 });
4064
4065 it('updates refs when remounting', async () => {
4066 if (__DEV__) {
4067 const testRef = React.createRef();
4068 await render(
4069 () => {
4070 class Hello extends React.Component {
4071 getColor() {
4072 return 'green';
4073 }
4074 render() {
4075 return <p />;
4076 }
4077 }
4078 $RefreshReg$(Hello, 'Hello');
4079 return Hello;
4080 },
4081 {ref: testRef},
4082 );
4083 expect(testRef.current.getColor()).toBe('green');
4084
4085 await patch(() => {
4086 class Hello extends React.Component {
4087 getColor() {
4088 return 'orange';
4089 }
4090 render() {
4091 return <p />;
4092 }
4093 }
4094 $RefreshReg$(Hello, 'Hello');
4095 });
4096 expect(testRef.current.getColor()).toBe('orange');
4097
4098 await patch(() => {
4099 const Hello = React.forwardRef((props, ref) => {
4100 React.useImperativeHandle(ref, () => ({
4101 getColor() {
4102 return 'pink';
4103 },
4104 }));
4105 return <p />;
4106 });
4107 $RefreshReg$(Hello, 'Hello');
4108 });
4109 expect(testRef.current.getColor()).toBe('pink');
4110
4111 await patch(() => {
4112 const Hello = React.forwardRef((props, ref) => {
4113 React.useImperativeHandle(ref, () => ({
4114 getColor() {
4115 return 'yellow';
4116 },
4117 }));
4118 return <p />;
4119 });
4120 $RefreshReg$(Hello, 'Hello');
4121 });
4122 expect(testRef.current.getColor()).toBe('yellow');
4123
4124 await patch(() => {
4125 const Hello = React.forwardRef((props, ref) => {
4126 React.useImperativeHandle(ref, () => ({
4127 getColor() {
4128 return 'yellow';
4129 },
4130 }));
4131 return <p />;
4132 });
4133 $RefreshReg$(Hello, 'Hello');
4134 });
4135 expect(testRef.current.getColor()).toBe('yellow');
4136 }
4137 });
4138
4139 it('remounts on conversion from class to function and back', async () => {
4140 if (__DEV__) {
4141 const HelloV1 = await render(() => {
4142 function Hello() {
4143 const [val, setVal] = React.useState(0);
4144 return (
4145 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
4146 {val}
4147 </p>
4148 );
4149 }
4150 $RefreshReg$(Hello, 'Hello');
4151 return Hello;
4152 });
4153
4154 // Bump the state before patching.
4155 const el = container.firstChild;
4156 expect(el.textContent).toBe('0');
4157 expect(el.style.color).toBe('blue');
4158 await act(() => {
4159 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
4160 });
4161 expect(el.textContent).toBe('1');
4162
4163 // Perform a hot update that turns it into a class.
4164 const HelloV2 = await patch(() => {
4165 class Hello extends React.Component {
4166 state = {count: 0};
4167 handleClick = () => {
4168 this.setState(prev => ({
4169 count: prev.count + 1,
4170 }));
4171 };
4172 render() {
4173 return (
4174 <p style={{color: 'red'}} onClick={this.handleClick}>
4175 {this.state.count}
4176 </p>
4177 );
4178 }
4179 }
4180 $RefreshReg$(Hello, 'Hello');
4181 return Hello;
4182 });
4183
4184 // It should have remounted.
4185 expect(container.firstChild).not.toBe(el);
4186 const newEl = container.firstChild;
4187 expect(newEl.textContent).toBe('0');
4188 expect(newEl.style.color).toBe('red');
4189 await act(() => {
4190 newEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
4191 });
4192 expect(newEl.textContent).toBe('1');
4193
4194 // Now top-level renders of both types resolve to latest.
4195 await render(() => HelloV1);
4196 await render(() => HelloV2);
4197 expect(container.firstChild).toBe(newEl);
4198 expect(newEl.style.color).toBe('red');
4199 expect(newEl.textContent).toBe('1');
4200
4201 // Now convert it back to a function.
4202 const HelloV3 = await patch(() => {
4203 function Hello() {
4204 const [val, setVal] = React.useState(0);
4205 return (
4206 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
4207 {val}
4208 </p>
4209 );
4210 }
4211 $RefreshReg$(Hello, 'Hello');
4212 return Hello;
4213 });
4214
4215 // It should have remounted again.
4216 expect(container.firstChild).not.toBe(el);
4217 const finalEl = container.firstChild;
4218 expect(finalEl.textContent).toBe('0');
4219 expect(finalEl.style.color).toBe('orange');
4220 await act(() => {
4221 finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true}));
4222 });
4223 expect(finalEl.textContent).toBe('1');
4224
4225 await render(() => HelloV3);
4226 await render(() => HelloV2);
4227 await render(() => HelloV1);
4228 expect(container.firstChild).toBe(finalEl);
4229 expect(finalEl.style.color).toBe('orange');
4230 expect(finalEl.textContent).toBe('1');
4231
4232 // Now that it's a function, verify edits keep state.
4233 await patch(() => {
4234 function Hello() {
4235 const [val, setVal] = React.useState(0);
4236 return (
4237 <p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
4238 {val}
4239 </p>
4240 );
4241 }
4242 $RefreshReg$(Hello, 'Hello');
4243 return Hello;
4244 });
4245 expect(container.firstChild).toBe(finalEl);
4246 expect(finalEl.style.color).toBe('purple');
4247 expect(finalEl.textContent).toBe('1');
4248 }
4249 });
4250
4251 it('can update multiple roots independently', async () => {
4252 if (__DEV__) {
4253 // Declare the first version.
4254 const HelloV1 = () => {
4255 const [val, setVal] = React.useState(0);
4256 return (
4257 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
4258 {val}
4259 </p>
4260 );
4261 };
4262 $RefreshReg$(HelloV1, 'Hello');
4263
4264 // Perform a hot update before any roots exist.
4265 const HelloV2 = () => {
4266 const [val, setVal] = React.useState(0);
4267 return (
4268 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
4269 {val}
4270 </p>
4271 );
4272 };
4273 $RefreshReg$(HelloV2, 'Hello');
4274 await act(() => {
4275 ReactFreshRuntime.performReactRefresh();
4276 });
4277
4278 // Mount three roots.
4279 const cont1 = document.createElement('div');
4280 const cont2 = document.createElement('div');
4281 const cont3 = document.createElement('div');
4282 document.body.appendChild(cont1);
4283 document.body.appendChild(cont2);
4284 document.body.appendChild(cont3);
4285 const root1 = ReactDOMClient.createRoot(cont1);
4286 const root2 = ReactDOMClient.createRoot(cont2);
4287 const root3 = ReactDOMClient.createRoot(cont3);
4288 try {
4289 await act(() => {
4290 root1.render(<HelloV1 id={1} />);
4291 });
4292 await act(() => {
4293 root2.render(<HelloV2 id={2} />);
4294 });
4295 await act(() => {
4296 root3.render(<HelloV1 id={3} />);
4297 });
4298
4299 // Expect we see the V2 color.
4300 expect(cont1.firstChild.style.color).toBe('red');
4301 expect(cont2.firstChild.style.color).toBe('red');
4302 expect(cont3.firstChild.style.color).toBe('red');
4303 expect(cont1.firstChild.textContent).toBe('0');
4304 expect(cont2.firstChild.textContent).toBe('0');
4305 expect(cont3.firstChild.textContent).toBe('0');
4306
4307 // Bump the state for each of them.
4308 await act(() => {
4309 cont1.firstChild.dispatchEvent(
4310 new MouseEvent('click', {bubbles: true}),
4311 );
4312 cont2.firstChild.dispatchEvent(
4313 new MouseEvent('click', {bubbles: true}),
4314 );
4315 cont3.firstChild.dispatchEvent(
4316 new MouseEvent('click', {bubbles: true}),
4317 );
4318 });
4319 expect(cont1.firstChild.style.color).toBe('red');
4320 expect(cont2.firstChild.style.color).toBe('red');
4321 expect(cont3.firstChild.style.color).toBe('red');
4322 expect(cont1.firstChild.textContent).toBe('1');
4323 expect(cont2.firstChild.textContent).toBe('1');
4324 expect(cont3.firstChild.textContent).toBe('1');
4325
4326 // Perform another hot update.
4327 const HelloV3 = () => {
4328 const [val, setVal] = React.useState(0);
4329 return (
4330 <p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
4331 {val}
4332 </p>
4333 );
4334 };
4335 $RefreshReg$(HelloV3, 'Hello');
4336 await act(() => {
4337 ReactFreshRuntime.performReactRefresh();
4338 });
4339
4340 // It should affect all roots.
4341 expect(cont1.firstChild.style.color).toBe('green');
4342 expect(cont2.firstChild.style.color).toBe('green');
4343 expect(cont3.firstChild.style.color).toBe('green');
4344 expect(cont1.firstChild.textContent).toBe('1');
4345 expect(cont2.firstChild.textContent).toBe('1');
4346 expect(cont3.firstChild.textContent).toBe('1');
4347
4348 // Unmount the second root.
4349 await act(() => {
4350 root2.unmount();
4351 });
4352 // Make the first root throw and unmount on hot update.
4353 const HelloV4 = ({id}) => {
4354 if (id === 1) {
4355 throw new Error('Oops.');
4356 }
4357 const [val, setVal] = React.useState(0);
4358 return (
4359 <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
4360 {val}
4361 </p>
4362 );
4363 };
4364 $RefreshReg$(HelloV4, 'Hello');
4365 await expect(
4366 act(() => {
4367 ReactFreshRuntime.performReactRefresh();
4368 }),
4369 ).rejects.toThrow('Oops.');
4370
4371 // Still, we expect the last root to be updated.
4372 expect(cont1.innerHTML).toBe('');
4373 expect(cont2.innerHTML).toBe('');
4374 expect(cont3.firstChild.style.color).toBe('orange');
4375 expect(cont3.firstChild.textContent).toBe('1');
4376 } finally {
4377 document.body.removeChild(cont1);
4378 document.body.removeChild(cont2);
4379 document.body.removeChild(cont3);
4380 }
4381 }
4382 });
4383
4384 // Module runtimes can use this to decide whether
4385 // to propagate an update up to the modules that imported it,
4386 // or to stop at the current module because it's a component.
4387 // This can't and doesn't need to be 100% precise.
4388 it('can detect likely component types', () => {
4389 function useTheme() {}
4390 function Widget() {}
4391
4392 if (__DEV__) {
4393 expect(ReactFreshRuntime.isLikelyComponentType(false)).toBe(false);
4394 expect(ReactFreshRuntime.isLikelyComponentType(null)).toBe(false);
4395 expect(ReactFreshRuntime.isLikelyComponentType('foo')).toBe(false);
4396
4397 // We need to hit a balance here.
4398 // If we lean towards assuming everything is a component,
4399 // editing modules that export plain functions won't trigger
4400 // a proper reload because we will bottle up the update.
4401 // So we're being somewhat conservative.
4402 expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false);
4403 expect(ReactFreshRuntime.isLikelyComponentType(function () {})).toBe(
4404 false,
4405 );
4406 expect(
4407 ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}),
4408 ).toBe(false);
4409 const loadUser = () => {};
4410 expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false);
4411 const useStore = () => {};
4412 expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false);
4413 expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false);
4414 const rogueProxy = new Proxy(
4415 {},
4416 {
4417 get(target, property) {
4418 throw new Error();
4419 },
4420 },
4421 );
4422 expect(ReactFreshRuntime.isLikelyComponentType(rogueProxy)).toBe(false);
4423
4424 // These seem like function components.
4425 const Button = () => {};
4426 expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);
4427 expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);
4428 const ProxyButton = new Proxy(Button, {
4429 get(target, property) {
4430 return target[property];
4431 },
4432 });
4433 expect(ReactFreshRuntime.isLikelyComponentType(ProxyButton)).toBe(true);
4434 const anon = (() => () => {})();
4435 anon.displayName = 'Foo';
4436 expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true);
4437
4438 // These seem like class components.
4439 class Btn extends React.Component {}
4440 class PureBtn extends React.PureComponent {}
4441 const ProxyBtn = new Proxy(Btn, {
4442 get(target, property) {
4443 return target[property];
4444 },
4445 });
4446 expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);
4447 expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);
4448 expect(ReactFreshRuntime.isLikelyComponentType(ProxyBtn)).toBe(true);
4449 expect(
4450 ReactFreshRuntime.isLikelyComponentType(
4451 createReactClass({render() {}}),
4452 ),
4453 ).toBe(true);
4454
4455 // These don't.
4456 class Figure {
4457 move() {}
4458 }
4459 expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);
4460 class Point extends Figure {}
4461 expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);
4462
4463 // Run the same tests without Babel.
4464 // This tests real arrow functions and classes, as implemented in Node.
4465
4466 // eslint-disable-next-line no-new-func
4467 new Function(
4468 'global',
4469 'React',
4470 'ReactFreshRuntime',
4471 'expect',
4472 'createReactClass',
4473 `
4474 expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false);
4475 expect(ReactFreshRuntime.isLikelyComponentType(function() {})).toBe(false);
4476 expect(
4477 ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}),
4478 ).toBe(false);
4479 const loadUser = () => {};
4480 expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false);
4481 const useStore = () => {};
4482 expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false);
4483 function useTheme() {}
4484 expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false);
4485
4486 // These seem like function components.
4487 let Button = () => {};
4488 expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);
4489 function Widget() {}
4490 expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);
4491 let anon = (() => () => {})();
4492 anon.displayName = 'Foo';
4493 expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true);
4494
4495 // These seem like class components.
4496 class Btn extends React.Component {}
4497 class PureBtn extends React.PureComponent {}
4498 expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);
4499 expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);
4500 expect(
4501 ReactFreshRuntime.isLikelyComponentType(createReactClass({render() {}})),
4502 ).toBe(true);
4503
4504 // These don't.
4505 class Figure {
4506 move() {}
4507 }
4508 expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);
4509 class Point extends Figure {}
4510 expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);
4511 `,
4512 )(global, React, ReactFreshRuntime, expect, createReactClass);
4513 }
4514 });
4515
4516 it('reports updated and remounted families to the caller', () => {
4517 if (__DEV__) {
4518 const HelloV1 = () => {
4519 const [val, setVal] = React.useState(0);
4520 return (
4521 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
4522 {val}
4523 </p>
4524 );
4525 };
4526 $RefreshReg$(HelloV1, 'Hello');
4527
4528 const HelloV2 = () => {
4529 const [val, setVal] = React.useState(0);
4530 return (
4531 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
4532 {val}
4533 </p>
4534 );
4535 };
4536 $RefreshReg$(HelloV2, 'Hello');
4537
4538 const update = ReactFreshRuntime.performReactRefresh();
4539 expect(update.updatedFamilies.size).toBe(1);
4540 expect(update.staleFamilies.size).toBe(0);
4541 const family = update.updatedFamilies.values().next().value;
4542 expect(family.current.name).toBe('HelloV2');
4543 // For example, we can use this to print a log of what was updated.
4544 }
4545 });
4546
4547 function initFauxDevToolsHook() {
4548 const onCommitFiberRoot = jest.fn();
4549 const onCommitFiberUnmount = jest.fn();
4550
4551 let idCounter = 0;
4552 const renderers = new Map();
4553
4554 // This is a minimal shim for the global hook installed by DevTools.
4555 // The real one is in packages/react-devtools-shared/src/hook.js.
4556 global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
4557 renderers,
4558 supportsFiber: true,
4559 inject(renderer) {
4560 const id = ++idCounter;
4561 renderers.set(id, renderer);
4562 return id;
4563 },
4564 onCommitFiberRoot,
4565 onCommitFiberUnmount,
4566 };
4567 }
4568
4569 // This simulates the scenario in https://github.com/facebook/react/issues/17626
4570 it('can inject the runtime after the renderer executes', async () => {
4571 if (__DEV__) {
4572 initFauxDevToolsHook();
4573
4574 // Load these first, as if they're coming from a CDN.
4575 jest.resetModules();
4576 React = require('react');
4577 ReactDOM = require('react-dom');
4578 ReactDOMClient = require('react-dom/client');
4579 Scheduler = require('scheduler');
4580 act = require('internal-test-utils').act;
4581
4582 // Important! Inject into the global hook *after* ReactDOM runs:
4583 ReactFreshRuntime = require('react-refresh/runtime');
4584 ReactFreshRuntime.injectIntoGlobalHook(global);
4585
4586 root = ReactDOMClient.createRoot(container);
4587
4588 // We're verifying that we're able to track roots mounted after this point.
4589 // The rest of this test is taken from the simplest first test case.
4590
4591 await render(() => {
4592 function Hello() {
4593 const [val, setVal] = React.useState(0);
4594 return (
4595 <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
4596 {val}
4597 </p>
4598 );
4599 }
4600 $RefreshReg$(Hello, 'Hello');
4601 return Hello;
4602 });
4603
4604 // Bump the state before patching.
4605 const el = container.firstChild;
4606 expect(el.textContent).toBe('0');
4607 expect(el.style.color).toBe('blue');
4608 await act(() => {
4609 el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
4610 });
4611 expect(el.textContent).toBe('1');
4612
4613 // Perform a hot update.
4614 await patch(() => {
4615 function Hello() {
4616 const [val, setVal] = React.useState(0);
4617 return (
4618 <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
4619 {val}
4620 </p>
4621 );
4622 }
4623 $RefreshReg$(Hello, 'Hello');
4624 return Hello;
4625 });
4626
4627 // Assert the state was preserved but color changed.
4628 expect(container.firstChild).toBe(el);
4629 expect(el.textContent).toBe('1');
4630 expect(el.style.color).toBe('red');
4631 }
4632 });
4633
4634 // This simulates the scenario in https://github.com/facebook/react/issues/20100
4635 it('does not block DevTools when an unsupported legacy renderer is injected', () => {
4636 if (__DEV__) {
4637 initFauxDevToolsHook();
4638
4639 const onCommitFiberRoot =
4640 global.__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot;
4641
4642 // Redirect all React/ReactDOM requires to v16.8.0
4643 // This version predates Fast Refresh support.
4644 jest.mock('scheduler', () => jest.requireActual('scheduler-0-13'));
4645 jest.mock('scheduler/tracing', () =>
4646 jest.requireActual('scheduler-0-13/tracing'),
4647 );
4648 jest.mock('react', () => jest.requireActual('react-16-8'));
4649 jest.mock('react-dom', () => jest.requireActual('react-dom-16-8'));
4650
4651 // Load React and company.
4652 jest.resetModules();
4653 React = require('react');
4654 ReactDOM = require('react-dom');
4655 Scheduler = require('scheduler');
4656
4657 // Important! Inject into the global hook *after* ReactDOM runs:
4658 ReactFreshRuntime = require('react-refresh/runtime');
4659 ReactFreshRuntime.injectIntoGlobalHook(global);
4660
4661 // NOTE: Intentionally using createElement in this test instead of JSX
4662 // because old versions of React are incompatible with the JSX transform
4663 // used by our test suite.
4664 const Hello = () => {
4665 const [state] = React.useState('Hi!');
4666 // Intentionally
4667 return React.createElement('div', null, state);
4668 };
4669 $RefreshReg$(Hello, 'Hello');
4670 const Component = Hello;
4671 ReactDOM.render(React.createElement(Component), container);
4672
4673 expect(onCommitFiberRoot).toHaveBeenCalled();
4674 }
4675 });
4676 });