main
js 2,280 lines 65 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 ReactDOMClient;
14 let ReactFreshRuntime;
15 let Scheduler;
16 let act;
17 let assertLog;
18
19 const babel = require('@babel/core');
20 const freshPlugin = require('react-refresh/babel');
21 const ts = require('typescript');
22
23 describe('ReactFreshIntegration', () => {
24 let container;
25 let root;
26 let exportsObj;
27
28 beforeEach(() => {
29 if (__DEV__) {
30 jest.resetModules();
31 React = require('react');
32 ReactFreshRuntime = require('react-refresh/runtime');
33 ReactFreshRuntime.injectIntoGlobalHook(global);
34 ReactDOMClient = require('react-dom/client');
35 Scheduler = require('scheduler/unstable_mock');
36 ({act, assertLog} = require('internal-test-utils'));
37 container = document.createElement('div');
38 document.body.appendChild(container);
39 root = ReactDOMClient.createRoot(container);
40 exportsObj = undefined;
41 }
42 });
43
44 afterEach(() => {
45 if (__DEV__) {
46 root.unmount();
47 // Ensure we don't leak memory by holding onto dead roots.
48 expect(ReactFreshRuntime._getMountedRootCount()).toBe(0);
49 document.body.removeChild(container);
50 }
51 });
52
53 function executeJavaScript(source, compileDestructuring) {
54 const compiled = babel.transform(source, {
55 babelrc: false,
56 presets: ['@babel/react'],
57 plugins: [
58 [freshPlugin, {skipEnvCheck: true}],
59 '@babel/plugin-transform-modules-commonjs',
60 compileDestructuring && '@babel/plugin-transform-destructuring',
61 ].filter(Boolean),
62 }).code;
63 return executeCompiled(compiled);
64 }
65
66 function executeTypescript(source) {
67 const typescriptSource = babel.transform(source, {
68 babelrc: false,
69 configFile: false,
70 presets: ['@babel/react'],
71 plugins: [
72 [freshPlugin, {skipEnvCheck: true}],
73 ['@babel/plugin-syntax-typescript', {isTSX: true}],
74 ],
75 }).code;
76 const compiled = ts.transpileModule(typescriptSource, {
77 module: ts.ModuleKind.CommonJS,
78 }).outputText;
79 return executeCompiled(compiled);
80 }
81
82 function executeCompiled(compiled) {
83 exportsObj = {};
84 // eslint-disable-next-line no-new-func
85 new Function(
86 'global',
87 'require',
88 'React',
89 'Scheduler',
90 'exports',
91 '$RefreshReg$',
92 '$RefreshSig$',
93 compiled,
94 )(
95 global,
96 require,
97 React,
98 Scheduler,
99 exportsObj,
100 $RefreshReg$,
101 $RefreshSig$,
102 );
103 // Module systems will register exports as a fallback.
104 // This is useful for cases when e.g. a class is exported,
105 // and we don't want to propagate the update beyond this module.
106 $RefreshReg$(exportsObj.default, 'exports.default');
107 return exportsObj.default;
108 }
109
110 function $RefreshReg$(type, id) {
111 ReactFreshRuntime.register(type, id);
112 }
113
114 function $RefreshSig$() {
115 return ReactFreshRuntime.createSignatureFunctionForTransform();
116 }
117
118 describe.each([
119 [
120 'JavaScript syntax with destructuring enabled',
121 source => executeJavaScript(source, true),
122 testJavaScript,
123 ],
124 [
125 'JavaScript syntax with destructuring disabled',
126 source => executeJavaScript(source, false),
127 testJavaScript,
128 ],
129 ['TypeScript syntax', executeTypescript, testTypeScript],
130 ])('%s', (language, execute, runTest) => {
131 async function render(source) {
132 const Component = execute(source);
133 await act(() => {
134 root.render(<Component />);
135 });
136 // Module initialization shouldn't be counted as a hot update.
137 expect(ReactFreshRuntime.performReactRefresh()).toBe(null);
138 }
139
140 async function patch(source) {
141 const prevExports = exportsObj;
142 execute(source);
143 const nextExports = exportsObj;
144
145 // Check if exported families have changed.
146 // (In a real module system we'd do this for *all* exports.)
147 // For example, this can happen if you convert a class to a function.
148 // Or if you wrap something in a HOC.
149 const didExportsChange =
150 ReactFreshRuntime.getFamilyByType(prevExports.default) !==
151 ReactFreshRuntime.getFamilyByType(nextExports.default);
152 if (didExportsChange) {
153 // In a real module system, we would propagate such updates upwards,
154 // and re-execute modules that imported this one. (Just like if we edited them.)
155 // This makes adding/removing/renaming exports re-render references to them.
156 // Here, we'll just force a re-render using the newer type to emulate this.
157 const NextComponent = nextExports.default;
158 await act(() => {
159 root.render(<NextComponent />);
160 });
161 }
162 await act(() => {
163 const result = ReactFreshRuntime.performReactRefresh();
164 if (!didExportsChange) {
165 // Normally we expect that some components got updated in our tests.
166 expect(result).not.toBe(null);
167 } else {
168 // However, we have tests where we convert functions to classes,
169 // and in those cases it's expected nothing would get updated.
170 // (Instead, the export change branch above would take care of it.)
171 }
172 });
173 expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
174 }
175
176 runTest(render, patch);
177 });
178
179 function testJavaScript(render, patch) {
180 it('reloads function declarations', async () => {
181 if (__DEV__) {
182 await render(`
183 function Parent() {
184 return <Child prop="A" />;
185 };
186
187 function Child({prop}) {
188 return <h1>{prop}1</h1>;
189 };
190
191 export default Parent;
192 `);
193 const el = container.firstChild;
194 expect(el.textContent).toBe('A1');
195 await patch(`
196 function Parent() {
197 return <Child prop="B" />;
198 };
199
200 function Child({prop}) {
201 return <h1>{prop}2</h1>;
202 };
203
204 export default Parent;
205 `);
206 expect(container.firstChild).toBe(el);
207 expect(el.textContent).toBe('B2');
208 }
209 });
210
211 it('reloads arrow functions', async () => {
212 if (__DEV__) {
213 await render(`
214 const Parent = () => {
215 return <Child prop="A" />;
216 };
217
218 const Child = ({prop}) => {
219 return <h1>{prop}1</h1>;
220 };
221
222 export default Parent;
223 `);
224 const el = container.firstChild;
225 expect(el.textContent).toBe('A1');
226 await patch(`
227 const Parent = () => {
228 return <Child prop="B" />;
229 };
230
231 const Child = ({prop}) => {
232 return <h1>{prop}2</h1>;
233 };
234
235 export default Parent;
236 `);
237 expect(container.firstChild).toBe(el);
238 expect(el.textContent).toBe('B2');
239 }
240 });
241
242 it('reloads a combination of memo and forwardRef', async () => {
243 if (__DEV__) {
244 await render(`
245 const {memo} = React;
246
247 const Parent = memo(React.forwardRef(function (props, ref) {
248 return <Child prop="A" ref={ref} />;
249 }));
250
251 const Child = React.memo(({prop}) => {
252 return <h1>{prop}1</h1>;
253 });
254
255 export default React.memo(Parent);
256 `);
257 const el = container.firstChild;
258 expect(el.textContent).toBe('A1');
259 await patch(`
260 const {memo} = React;
261
262 const Parent = memo(React.forwardRef(function (props, ref) {
263 return <Child prop="B" ref={ref} />;
264 }));
265
266 const Child = React.memo(({prop}) => {
267 return <h1>{prop}2</h1>;
268 });
269
270 export default React.memo(Parent);
271 `);
272 expect(container.firstChild).toBe(el);
273 expect(el.textContent).toBe('B2');
274 }
275 });
276
277 it('reloads default export with named memo', async () => {
278 if (__DEV__) {
279 await render(`
280 const {memo} = React;
281
282 const Child = React.memo(({prop}) => {
283 return <h1>{prop}1</h1>;
284 });
285
286 export default memo(React.forwardRef(function Parent(props, ref) {
287 return <Child prop="A" ref={ref} />;
288 }));
289 `);
290 const el = container.firstChild;
291 expect(el.textContent).toBe('A1');
292 await patch(`
293 const {memo} = React;
294
295 const Child = React.memo(({prop}) => {
296 return <h1>{prop}2</h1>;
297 });
298
299 export default memo(React.forwardRef(function Parent(props, ref) {
300 return <Child prop="B" ref={ref} />;
301 }));
302 `);
303 expect(container.firstChild).toBe(el);
304 expect(el.textContent).toBe('B2');
305 }
306 });
307
308 // @gate __DEV__
309 it('ignores ref for class component in hidden subtree', async () => {
310 const code = `
311 import {Activity} from 'react';
312
313 // Avoid creating a new class on Fast Refresh.
314 global.A = global.A ?? class A extends React.Component {
315 render() {
316 return <div />;
317 }
318 }
319 const A = global.A;
320
321 function hiddenRef() {
322 throw new Error('Unexpected hiddenRef() invocation.');
323 }
324
325 export default function App() {
326 return (
327 <Activity mode="hidden">
328 <A ref={hiddenRef} />
329 </Activity>
330 );
331 };
332 `;
333
334 await render(code);
335 await patch(code);
336 });
337
338 // @gate __DEV__
339 it('ignores ref for hoistable resource in hidden subtree', async () => {
340 const code = `
341 import {Activity} from 'react';
342
343 function hiddenRef() {
344 throw new Error('Unexpected hiddenRef() invocation.');
345 }
346
347 export default function App() {
348 return (
349 <Activity mode="hidden">
350 <link rel="preload" href="foo" ref={hiddenRef} />
351 </Activity>
352 );
353 };
354 `;
355
356 await render(code);
357 await patch(code);
358 });
359
360 // @gate __DEV__
361 it('ignores ref for host component in hidden subtree', async () => {
362 const code = `
363 import {Activity} from 'react';
364
365 function hiddenRef() {
366 throw new Error('Unexpected hiddenRef() invocation.');
367 }
368
369 export default function App() {
370 return (
371 <Activity mode="hidden">
372 <div ref={hiddenRef} />
373 </Activity>
374 );
375 };
376 `;
377
378 await render(code);
379 await patch(code);
380 });
381
382 // @gate __DEV__
383 it('ignores ref for Activity in hidden subtree', async () => {
384 const code = `
385 import {Activity} from 'react';
386
387 function hiddenRef(value) {
388 throw new Error('Unexpected hiddenRef() invocation.');
389 }
390
391 export default function App() {
392 return (
393 <Activity mode="hidden">
394 <Activity mode="visible" ref={hiddenRef}>
395 <div />
396 </Activity>
397 </Activity>
398 );
399 };
400 `;
401
402 await render(code);
403 await patch(code);
404 });
405
406 // @gate __DEV__
407 it('ignores ref for Scope in hidden subtree', async () => {
408 const code = `
409 import {
410 Activity,
411 unstable_Scope as Scope,
412 } from 'react';
413
414 function hiddenRef(value) {
415 throw new Error('Unexpected hiddenRef() invocation.');
416 }
417
418 export default function App() {
419 return (
420 <Activity mode="hidden">
421 <Scope ref={hiddenRef}>
422 <div />
423 </Scope>
424 </Activity>
425 );
426 };
427 `;
428
429 await render(code);
430 await patch(code);
431 });
432
433 // @gate __DEV__
434 it('ignores ref for functional component in hidden subtree', async () => {
435 const code = `
436 import {Activity} from 'react';
437
438 // Avoid creating a new component on Fast Refresh.
439 global.A = global.A ?? function A() {
440 return <div />;
441 }
442 const A = global.A;
443
444 function hiddenRef() {
445 throw new Error('Unexpected hiddenRef() invocation.');
446 }
447
448 export default function App() {
449 return (
450 <Activity mode="hidden">
451 <A ref={hiddenRef} />
452 </Activity>
453 );
454 };
455 `;
456
457 await render(code);
458 await patch(code);
459 });
460
461 // @gate __DEV__
462 it('ignores ref for ref forwarding component in hidden subtree', async () => {
463 const code = `
464 import {
465 forwardRef,
466 Activity,
467 } from 'react';
468
469 // Avoid creating a new component on Fast Refresh.
470 global.A = global.A ?? forwardRef(function A(props, ref) {
471 return <div ref={ref} />;
472 });
473 const A = global.A;
474
475 function hiddenRef() {
476 throw new Error('Unexpected hiddenRef() invocation.');
477 }
478
479 export default function App() {
480 return (
481 <Activity mode="hidden">
482 <A ref={hiddenRef} />
483 </Activity>
484 );
485 };
486 `;
487
488 await render(code);
489 await patch(code);
490 });
491
492 // @gate __DEV__
493 it('ignores ref for simple memo component in hidden subtree', async () => {
494 const code = `
495 import {
496 memo,
497 Activity,
498 } from 'react';
499
500 // Avoid creating a new component on Fast Refresh.
501 global.A = global.A ?? memo(function A() {
502 return <div />;
503 });
504 const A = global.A;
505
506 function hiddenRef() {
507 throw new Error('Unexpected hiddenRef() invocation.');
508 }
509
510 export default function App() {
511 return (
512 <Activity mode="hidden">
513 <A ref={hiddenRef} />
514 </Activity>
515 );
516 };
517 `;
518
519 await render(code);
520 await patch(code);
521 });
522
523 // @gate __DEV__
524 it('ignores ref for memo component in hidden subtree', async () => {
525 // A custom compare function means this won't use SimpleMemoComponent.
526 const code = `
527 import {
528 memo,
529 Activity,
530 } from 'react';
531
532 // Avoid creating a new component on Fast Refresh.
533 global.A = global.A ?? memo(
534 function A() {
535 return <div />;
536 },
537 () => false,
538 );
539 const A = global.A;
540
541 function hiddenRef() {
542 throw new Error('Unexpected hiddenRef() invocation.');
543 }
544
545 export default function App() {
546 return (
547 <Activity mode="hidden">
548 <A ref={hiddenRef} />
549 </Activity>
550 );
551 };
552 `;
553
554 await render(code);
555 await patch(code);
556 });
557
558 it('reloads HOCs if they return functions', async () => {
559 if (__DEV__) {
560 await render(`
561 function hoc(letter) {
562 return function() {
563 return <h1>{letter}1</h1>;
564 }
565 }
566
567 export default function Parent() {
568 return <Child />;
569 }
570
571 const Child = hoc('A');
572 `);
573 const el = container.firstChild;
574 expect(el.textContent).toBe('A1');
575 await patch(`
576 function hoc(letter) {
577 return function() {
578 return <h1>{letter}2</h1>;
579 }
580 }
581
582 export default function Parent() {
583 return React.createElement(Child);
584 }
585
586 const Child = hoc('B');
587 `);
588 expect(container.firstChild).toBe(el);
589 expect(el.textContent).toBe('B2');
590 }
591 });
592
593 it('resets state when renaming a state variable', async () => {
594 if (__DEV__) {
595 await render(`
596 const {useState} = React;
597 const S = 1;
598
599 export default function App() {
600 const [foo, setFoo] = useState(S);
601 return <h1>A{foo}</h1>;
602 }
603 `);
604 const el = container.firstChild;
605 expect(el.textContent).toBe('A1');
606
607 await patch(`
608 const {useState} = React;
609 const S = 2;
610
611 export default function App() {
612 const [foo, setFoo] = useState(S);
613 return <h1>B{foo}</h1>;
614 }
615 `);
616 // Same state variable name, so state is preserved.
617 expect(container.firstChild).toBe(el);
618 expect(el.textContent).toBe('B1');
619
620 await patch(`
621 const {useState} = React;
622 const S = 3;
623
624 export default function App() {
625 const [bar, setBar] = useState(S);
626 return <h1>C{bar}</h1>;
627 }
628 `);
629 // Different state variable name, so state is reset.
630 expect(container.firstChild).not.toBe(el);
631 const newEl = container.firstChild;
632 expect(newEl.textContent).toBe('C3');
633 }
634 });
635
636 it('resets state when renaming a state variable in a HOC', async () => {
637 if (__DEV__) {
638 await render(`
639 const {useState} = React;
640 const S = 1;
641
642 function hoc(Wrapped) {
643 return function Generated() {
644 const [foo, setFoo] = useState(S);
645 return <Wrapped value={foo} />;
646 };
647 }
648
649 export default hoc(({ value }) => {
650 return <h1>A{value}</h1>;
651 });
652 `);
653 const el = container.firstChild;
654 expect(el.textContent).toBe('A1');
655
656 await patch(`
657 const {useState} = React;
658 const S = 2;
659
660 function hoc(Wrapped) {
661 return function Generated() {
662 const [foo, setFoo] = useState(S);
663 return <Wrapped value={foo} />;
664 };
665 }
666
667 export default hoc(({ value }) => {
668 return <h1>B{value}</h1>;
669 });
670 `);
671 // Same state variable name, so state is preserved.
672 expect(container.firstChild).toBe(el);
673 expect(el.textContent).toBe('B1');
674
675 await patch(`
676 const {useState} = React;
677 const S = 3;
678
679 function hoc(Wrapped) {
680 return function Generated() {
681 const [bar, setBar] = useState(S);
682 return <Wrapped value={bar} />;
683 };
684 }
685
686 export default hoc(({ value }) => {
687 return <h1>C{value}</h1>;
688 });
689 `);
690 // Different state variable name, so state is reset.
691 expect(container.firstChild).not.toBe(el);
692 const newEl = container.firstChild;
693 expect(newEl.textContent).toBe('C3');
694 }
695 });
696
697 it('resets state when renaming a state variable in a HOC with indirection', async () => {
698 if (__DEV__) {
699 await render(`
700 const {useState} = React;
701 const S = 1;
702
703 function hoc(Wrapped) {
704 return function Generated() {
705 const [foo, setFoo] = useState(S);
706 return <Wrapped value={foo} />;
707 };
708 }
709
710 function Indirection({ value }) {
711 return <h1>A{value}</h1>;
712 }
713
714 export default hoc(Indirection);
715 `);
716 const el = container.firstChild;
717 expect(el.textContent).toBe('A1');
718
719 await patch(`
720 const {useState} = React;
721 const S = 2;
722
723 function hoc(Wrapped) {
724 return function Generated() {
725 const [foo, setFoo] = useState(S);
726 return <Wrapped value={foo} />;
727 };
728 }
729
730 function Indirection({ value }) {
731 return <h1>B{value}</h1>;
732 }
733
734 export default hoc(Indirection);
735 `);
736 // Same state variable name, so state is preserved.
737 expect(container.firstChild).toBe(el);
738 expect(el.textContent).toBe('B1');
739
740 await patch(`
741 const {useState} = React;
742 const S = 3;
743
744 function hoc(Wrapped) {
745 return function Generated() {
746 const [bar, setBar] = useState(S);
747 return <Wrapped value={bar} />;
748 };
749 }
750
751 function Indirection({ value }) {
752 return <h1>C{value}</h1>;
753 }
754
755 export default hoc(Indirection);
756 `);
757 // Different state variable name, so state is reset.
758 expect(container.firstChild).not.toBe(el);
759 const newEl = container.firstChild;
760 expect(newEl.textContent).toBe('C3');
761 }
762 });
763
764 it('resets state when renaming a state variable inside a HOC with direct call', async () => {
765 if (__DEV__) {
766 await render(`
767 const {useState} = React;
768 const S = 1;
769
770 function hocWithDirectCall(Wrapped) {
771 return function Generated() {
772 return Wrapped();
773 };
774 }
775
776 export default hocWithDirectCall(() => {
777 const [foo, setFoo] = useState(S);
778 return <h1>A{foo}</h1>;
779 });
780 `);
781 const el = container.firstChild;
782 expect(el.textContent).toBe('A1');
783
784 await patch(`
785 const {useState} = React;
786 const S = 2;
787
788 function hocWithDirectCall(Wrapped) {
789 return function Generated() {
790 return Wrapped();
791 };
792 }
793
794 export default hocWithDirectCall(() => {
795 const [foo, setFoo] = useState(S);
796 return <h1>B{foo}</h1>;
797 });
798 `);
799 // Same state variable name, so state is preserved.
800 expect(container.firstChild).toBe(el);
801 expect(el.textContent).toBe('B1');
802
803 await patch(`
804 const {useState} = React;
805 const S = 3;
806
807 function hocWithDirectCall(Wrapped) {
808 return function Generated() {
809 return Wrapped();
810 };
811 }
812
813 export default hocWithDirectCall(() => {
814 const [bar, setBar] = useState(S);
815 return <h1>C{bar}</h1>;
816 });
817 `);
818 // Different state variable name, so state is reset.
819 expect(container.firstChild).not.toBe(el);
820 const newEl = container.firstChild;
821 expect(newEl.textContent).toBe('C3');
822 }
823 });
824
825 it('does not crash when changing Hook order inside a HOC with direct call', async () => {
826 if (__DEV__) {
827 await render(`
828 const {useEffect} = React;
829
830 function hocWithDirectCall(Wrapped) {
831 return function Generated() {
832 return Wrapped();
833 };
834 }
835
836 export default hocWithDirectCall(() => {
837 useEffect(() => {}, []);
838 return <h1>A</h1>;
839 });
840 `);
841 const el = container.firstChild;
842 expect(el.textContent).toBe('A');
843
844 await patch(`
845 const {useEffect} = React;
846
847 function hocWithDirectCall(Wrapped) {
848 return function Generated() {
849 return Wrapped();
850 };
851 }
852
853 export default hocWithDirectCall(() => {
854 useEffect(() => {}, []);
855 useEffect(() => {}, []);
856 return <h1>B</h1>;
857 });
858 `);
859 // Hook order changed, so we remount.
860 expect(container.firstChild).not.toBe(el);
861 const newEl = container.firstChild;
862 expect(newEl.textContent).toBe('B');
863 }
864 });
865
866 it('does not crash when changing Hook order inside a memo-ed HOC with direct call', async () => {
867 if (__DEV__) {
868 await render(`
869 const {useEffect, memo} = React;
870
871 function hocWithDirectCall(Wrapped) {
872 return memo(function Generated() {
873 return Wrapped();
874 });
875 }
876
877 export default hocWithDirectCall(() => {
878 useEffect(() => {}, []);
879 return <h1>A</h1>;
880 });
881 `);
882 const el = container.firstChild;
883 expect(el.textContent).toBe('A');
884
885 await patch(`
886 const {useEffect, memo} = React;
887
888 function hocWithDirectCall(Wrapped) {
889 return memo(function Generated() {
890 return Wrapped();
891 });
892 }
893
894 export default hocWithDirectCall(() => {
895 useEffect(() => {}, []);
896 useEffect(() => {}, []);
897 return <h1>B</h1>;
898 });
899 `);
900 // Hook order changed, so we remount.
901 expect(container.firstChild).not.toBe(el);
902 const newEl = container.firstChild;
903 expect(newEl.textContent).toBe('B');
904 }
905 });
906
907 it('does not crash when changing Hook order inside a memo+forwardRef-ed HOC with direct call', async () => {
908 if (__DEV__) {
909 await render(`
910 const {useEffect, memo, forwardRef} = React;
911
912 function hocWithDirectCall(Wrapped) {
913 return memo(forwardRef(function Generated() {
914 return Wrapped();
915 }));
916 }
917
918 export default hocWithDirectCall(() => {
919 useEffect(() => {}, []);
920 return <h1>A</h1>;
921 });
922 `);
923 const el = container.firstChild;
924 expect(el.textContent).toBe('A');
925
926 await patch(`
927 const {useEffect, memo, forwardRef} = React;
928
929 function hocWithDirectCall(Wrapped) {
930 return memo(forwardRef(function Generated() {
931 return Wrapped();
932 }));
933 }
934
935 export default hocWithDirectCall(() => {
936 useEffect(() => {}, []);
937 useEffect(() => {}, []);
938 return <h1>B</h1>;
939 });
940 `);
941 // Hook order changed, so we remount.
942 expect(container.firstChild).not.toBe(el);
943 const newEl = container.firstChild;
944 expect(newEl.textContent).toBe('B');
945 }
946 });
947
948 it('does not crash when changing Hook order inside a HOC returning an object', async () => {
949 if (__DEV__) {
950 await render(`
951 const {useEffect} = React;
952
953 function hocWithDirectCall(Wrapped) {
954 return {Wrapped: Wrapped};
955 }
956
957 export default hocWithDirectCall(() => {
958 useEffect(() => {}, []);
959 return <h1>A</h1>;
960 }).Wrapped;
961 `);
962 const el = container.firstChild;
963 expect(el.textContent).toBe('A');
964
965 await patch(`
966 const {useEffect} = React;
967
968 function hocWithDirectCall(Wrapped) {
969 return {Wrapped: Wrapped};
970 }
971
972 export default hocWithDirectCall(() => {
973 useEffect(() => {}, []);
974 useEffect(() => {}, []);
975 return <h1>B</h1>;
976 }).Wrapped;
977 `);
978 // Hook order changed, so we remount.
979 expect(container.firstChild).not.toBe(el);
980 const newEl = container.firstChild;
981 expect(newEl.textContent).toBe('B');
982 }
983 });
984
985 it('resets effects while preserving state', async () => {
986 if (__DEV__) {
987 await render(`
988 const {useState} = React;
989
990 export default function App() {
991 const [value, setValue] = useState(0);
992 return <h1>A{value}</h1>;
993 }
994 `);
995 let el = container.firstChild;
996 expect(el.textContent).toBe('A0');
997
998 // Add an effect.
999 await patch(`
1000 const {useState} = React;
1001
1002 export default function App() {
1003 const [value, setValue] = useState(0);
1004 React.useEffect(() => {
1005 Scheduler.log('B mount');
1006 setValue(1)
1007 return () => {
1008 Scheduler.log('B unmount');
1009 };
1010 }, []);
1011 return <h1>B{value}</h1>;
1012 }
1013 `);
1014
1015 // We added an effect, thereby changing Hook order.
1016 // This causes a remount.
1017 expect(container.firstChild).not.toBe(el);
1018 el = container.firstChild;
1019 expect(el.textContent).toBe('B1');
1020 assertLog(['B mount']);
1021
1022 await patch(`
1023 const {useState} = React;
1024
1025 export default function App() {
1026 const [value, setValue] = useState(0);
1027 React.useEffect(() => {
1028 Scheduler.log('C mount');
1029 return () => {
1030 Scheduler.log('C unmount');
1031 };
1032 }, []);
1033 return <h1>C{value}</h1>;
1034 }
1035 `);
1036 // Same Hooks are called, so state is preserved.
1037 expect(container.firstChild).toBe(el);
1038 expect(el.textContent).toBe('C1');
1039
1040 // Effects are always reset, so effect B was unmounted and C was mounted.
1041 assertLog(['B unmount', 'C mount']);
1042
1043 await patch(`
1044 const {useState} = React;
1045
1046 export default function App() {
1047 const [value, setValue] = useState(0);
1048 return <h1>D{value}</h1>;
1049 }
1050 `);
1051 // Removing the effect changes the signature
1052 // and causes the component to remount.
1053 expect(container.firstChild).not.toBe(el);
1054 el = container.firstChild;
1055 expect(el.textContent).toBe('D0');
1056 assertLog(['C unmount']);
1057 }
1058 });
1059
1060 it('does not get confused when custom hooks are reordered', async () => {
1061 if (__DEV__) {
1062 await render(`
1063 function useFancyState(initialState) {
1064 return React.useState(initialState);
1065 }
1066
1067 const App = () => {
1068 const [x, setX] = useFancyState('X');
1069 const [y, setY] = useFancyState('Y');
1070 return <h1>A{x}{y}</h1>;
1071 };
1072
1073 export default App;
1074 `);
1075 let el = container.firstChild;
1076 expect(el.textContent).toBe('AXY');
1077
1078 await patch(`
1079 function useFancyState(initialState) {
1080 return React.useState(initialState);
1081 }
1082
1083 const App = () => {
1084 const [x, setX] = useFancyState('X');
1085 const [y, setY] = useFancyState('Y');
1086 return <h1>B{x}{y}</h1>;
1087 };
1088
1089 export default App;
1090 `);
1091 // Same state variables, so no remount.
1092 expect(container.firstChild).toBe(el);
1093 expect(el.textContent).toBe('BXY');
1094
1095 await patch(`
1096 function useFancyState(initialState) {
1097 return React.useState(initialState);
1098 }
1099
1100 const App = () => {
1101 const [y, setY] = useFancyState('Y');
1102 const [x, setX] = useFancyState('X');
1103 return <h1>B{x}{y}</h1>;
1104 };
1105
1106 export default App;
1107 `);
1108 // Hooks were re-ordered. This causes a remount.
1109 // Therefore, Hook calls don't accidentally share state.
1110 expect(container.firstChild).not.toBe(el);
1111 el = container.firstChild;
1112 expect(el.textContent).toBe('BXY');
1113 }
1114 });
1115
1116 it('does not get confused when component is called early', async () => {
1117 if (__DEV__) {
1118 await render(`
1119 // This isn't really a valid pattern but it's close enough
1120 // to simulate what happens when you call ReactDOM.render
1121 // in the same file. We want to ensure this doesn't confuse
1122 // the runtime.
1123 App();
1124
1125 function App() {
1126 const [x, setX] = useFancyState('X');
1127 const [y, setY] = useFancyState('Y');
1128 return <h1>A{x}{y}</h1>;
1129 };
1130
1131 function useFancyState(initialState) {
1132 // No real Hook calls to avoid triggering invalid call invariant.
1133 // We only want to verify that we can still call this function early.
1134 return initialState;
1135 }
1136
1137 export default App;
1138 `);
1139 let el = container.firstChild;
1140 expect(el.textContent).toBe('AXY');
1141
1142 await patch(`
1143 // This isn't really a valid pattern but it's close enough
1144 // to simulate what happens when you call ReactDOM.render
1145 // in the same file. We want to ensure this doesn't confuse
1146 // the runtime.
1147 App();
1148
1149 function App() {
1150 const [x, setX] = useFancyState('X');
1151 const [y, setY] = useFancyState('Y');
1152 return <h1>B{x}{y}</h1>;
1153 };
1154
1155 function useFancyState(initialState) {
1156 // No real Hook calls to avoid triggering invalid call invariant.
1157 // We only want to verify that we can still call this function early.
1158 return initialState;
1159 }
1160
1161 export default App;
1162 `);
1163 // Same state variables, so no remount.
1164 expect(container.firstChild).toBe(el);
1165 expect(el.textContent).toBe('BXY');
1166
1167 await patch(`
1168 // This isn't really a valid pattern but it's close enough
1169 // to simulate what happens when you call ReactDOM.render
1170 // in the same file. We want to ensure this doesn't confuse
1171 // the runtime.
1172 App();
1173
1174 function App() {
1175 const [y, setY] = useFancyState('Y');
1176 const [x, setX] = useFancyState('X');
1177 return <h1>B{x}{y}</h1>;
1178 };
1179
1180 function useFancyState(initialState) {
1181 // No real Hook calls to avoid triggering invalid call invariant.
1182 // We only want to verify that we can still call this function early.
1183 return initialState;
1184 }
1185
1186 export default App;
1187 `);
1188 // Hooks were re-ordered. This causes a remount.
1189 // Therefore, Hook calls don't accidentally share state.
1190 expect(container.firstChild).not.toBe(el);
1191 el = container.firstChild;
1192 expect(el.textContent).toBe('BXY');
1193 }
1194 });
1195
1196 it('does not get confused by Hooks defined inline', async () => {
1197 // This is not a recommended pattern but at least it shouldn't break.
1198 if (__DEV__) {
1199 await render(`
1200 const App = () => {
1201 const useFancyState = (initialState) => {
1202 const result = React.useState(initialState);
1203 return result;
1204 };
1205 const [x, setX] = useFancyState('X1');
1206 const [y, setY] = useFancyState('Y1');
1207 return <h1>A{x}{y}</h1>;
1208 };
1209
1210 export default App;
1211 `);
1212 let el = container.firstChild;
1213 expect(el.textContent).toBe('AX1Y1');
1214
1215 await patch(`
1216 const App = () => {
1217 const useFancyState = (initialState) => {
1218 const result = React.useState(initialState);
1219 return result;
1220 };
1221 const [x, setX] = useFancyState('X2');
1222 const [y, setY] = useFancyState('Y2');
1223 return <h1>B{x}{y}</h1>;
1224 };
1225
1226 export default App;
1227 `);
1228 // Remount even though nothing changed because
1229 // the custom Hook is inside -- and so we don't
1230 // really know whether its signature has changed.
1231 // We could potentially make it work, but for now
1232 // let's assert we don't crash with confusing errors.
1233 expect(container.firstChild).not.toBe(el);
1234 el = container.firstChild;
1235 expect(el.textContent).toBe('BX2Y2');
1236 }
1237 });
1238
1239 it('remounts component if custom hook it uses changes order', async () => {
1240 if (__DEV__) {
1241 await render(`
1242 const App = () => {
1243 const [x, setX] = useFancyState('X');
1244 const [y, setY] = useFancyState('Y');
1245 return <h1>A{x}{y}</h1>;
1246 };
1247
1248 const useFancyState = (initialState) => {
1249 const result = useIndirection(initialState);
1250 return result;
1251 };
1252
1253 function useIndirection(initialState) {
1254 return React.useState(initialState);
1255 }
1256
1257 export default App;
1258 `);
1259 let el = container.firstChild;
1260 expect(el.textContent).toBe('AXY');
1261
1262 await patch(`
1263 const App = () => {
1264 const [x, setX] = useFancyState('X');
1265 const [y, setY] = useFancyState('Y');
1266 return <h1>B{x}{y}</h1>;
1267 };
1268
1269 const useFancyState = (initialState) => {
1270 const result = useIndirection();
1271 return result;
1272 };
1273
1274 function useIndirection(initialState) {
1275 return React.useState(initialState);
1276 }
1277
1278 export default App;
1279 `);
1280 // We didn't change anything except the header text.
1281 // So we don't expect a remount.
1282 expect(container.firstChild).toBe(el);
1283 expect(el.textContent).toBe('BXY');
1284
1285 await patch(`
1286 const App = () => {
1287 const [x, setX] = useFancyState('X');
1288 const [y, setY] = useFancyState('Y');
1289 return <h1>C{x}{y}</h1>;
1290 };
1291
1292 const useFancyState = (initialState) => {
1293 const result = useIndirection(initialState);
1294 return result;
1295 };
1296
1297 function useIndirection(initialState) {
1298 React.useEffect(() => {});
1299 return React.useState(initialState);
1300 }
1301
1302 export default App;
1303 `);
1304 // The useIndirection Hook added an affect,
1305 // so we had to remount the component.
1306 expect(container.firstChild).not.toBe(el);
1307 el = container.firstChild;
1308 expect(el.textContent).toBe('CXY');
1309
1310 await patch(`
1311 const App = () => {
1312 const [x, setX] = useFancyState('X');
1313 const [y, setY] = useFancyState('Y');
1314 return <h1>D{x}{y}</h1>;
1315 };
1316
1317 const useFancyState = (initialState) => {
1318 const result = useIndirection();
1319 return result;
1320 };
1321
1322 function useIndirection(initialState) {
1323 React.useEffect(() => {});
1324 return React.useState(initialState);
1325 }
1326
1327 export default App;
1328 `);
1329 // We didn't change anything except the header text.
1330 // So we don't expect a remount.
1331 expect(container.firstChild).toBe(el);
1332 expect(el.textContent).toBe('DXY');
1333 }
1334 });
1335
1336 it('does not lose the inferred arrow names', async () => {
1337 if (__DEV__) {
1338 await render(`
1339 const Parent = () => {
1340 return <Child/>;
1341 };
1342
1343 const Child = () => {
1344 useMyThing();
1345 return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
1346 };
1347
1348 const useMyThing = () => {
1349 React.useState();
1350 };
1351
1352 export default Parent;
1353 `);
1354 expect(container.textContent).toBe('Parent Child useMyThing');
1355 }
1356 });
1357
1358 it('does not lose the inferred function names', async () => {
1359 if (__DEV__) {
1360 await render(`
1361 var Parent = function() {
1362 return <Child/>;
1363 };
1364
1365 var Child = function() {
1366 useMyThing();
1367 return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
1368 };
1369
1370 var useMyThing = function() {
1371 React.useState();
1372 };
1373
1374 export default Parent;
1375 `);
1376 expect(container.textContent).toBe('Parent Child useMyThing');
1377 }
1378 });
1379
1380 it('resets state on every edit with @refresh reset annotation', async () => {
1381 if (__DEV__) {
1382 await render(`
1383 const {useState} = React;
1384 const S = 1;
1385
1386 export default function App() {
1387 const [foo, setFoo] = useState(S);
1388 return <h1>A{foo}</h1>;
1389 }
1390 `);
1391 let el = container.firstChild;
1392 expect(el.textContent).toBe('A1');
1393
1394 await patch(`
1395 const {useState} = React;
1396 const S = 2;
1397
1398 export default function App() {
1399 const [foo, setFoo] = useState(S);
1400 return <h1>B{foo}</h1>;
1401 }
1402 `);
1403 // Same state variable name, so state is preserved.
1404 expect(container.firstChild).toBe(el);
1405 expect(el.textContent).toBe('B1');
1406
1407 await patch(`
1408 const {useState} = React;
1409 const S = 3;
1410
1411 /* @refresh reset */
1412
1413 export default function App() {
1414 const [foo, setFoo] = useState(S);
1415 return <h1>C{foo}</h1>;
1416 }
1417 `);
1418 // Found remount annotation, so state is reset.
1419 expect(container.firstChild).not.toBe(el);
1420 el = container.firstChild;
1421 expect(el.textContent).toBe('C3');
1422
1423 await patch(`
1424 const {useState} = React;
1425 const S = 4;
1426
1427 export default function App() {
1428
1429 // @refresh reset
1430
1431 const [foo, setFoo] = useState(S);
1432 return <h1>D{foo}</h1>;
1433 }
1434 `);
1435 // Found remount annotation, so state is reset.
1436 expect(container.firstChild).not.toBe(el);
1437 el = container.firstChild;
1438 expect(el.textContent).toBe('D4');
1439
1440 await patch(`
1441 const {useState} = React;
1442 const S = 5;
1443
1444 export default function App() {
1445 const [foo, setFoo] = useState(S);
1446 return <h1>E{foo}</h1>;
1447 }
1448 `);
1449 // There is no remount annotation anymore,
1450 // so preserve the previous state.
1451 expect(container.firstChild).toBe(el);
1452 expect(el.textContent).toBe('E4');
1453
1454 await patch(`
1455 const {useState} = React;
1456 const S = 6;
1457
1458 export default function App() {
1459 const [foo, setFoo] = useState(S);
1460 return <h1>F{foo}</h1>;
1461 }
1462 `);
1463 // Continue editing.
1464 expect(container.firstChild).toBe(el);
1465 expect(el.textContent).toBe('F4');
1466
1467 await patch(`
1468 const {useState} = React;
1469 const S = 7;
1470
1471 export default function App() {
1472
1473 /* @refresh reset */
1474
1475 const [foo, setFoo] = useState(S);
1476 return <h1>G{foo}</h1>;
1477 }
1478 `);
1479 // Force remount one last time.
1480 expect(container.firstChild).not.toBe(el);
1481 el = container.firstChild;
1482 expect(el.textContent).toBe('G7');
1483 }
1484 });
1485
1486 // This is best effort for simple cases.
1487 // We won't attempt to resolve identifiers.
1488 it('resets state when useState initial state is edited', async () => {
1489 if (__DEV__) {
1490 await render(`
1491 const {useState} = React;
1492
1493 export default function App() {
1494 const [foo, setFoo] = useState(1);
1495 return <h1>A{foo}</h1>;
1496 }
1497 `);
1498 let el = container.firstChild;
1499 expect(el.textContent).toBe('A1');
1500
1501 await patch(`
1502 const {useState} = React;
1503
1504 export default function App() {
1505 const [foo, setFoo] = useState(1);
1506 return <h1>B{foo}</h1>;
1507 }
1508 `);
1509 // Same initial state, so it's preserved.
1510 expect(container.firstChild).toBe(el);
1511 expect(el.textContent).toBe('B1');
1512
1513 await patch(`
1514 const {useState} = React;
1515
1516 export default function App() {
1517 const [foo, setFoo] = useState(2);
1518 return <h1>C{foo}</h1>;
1519 }
1520 `);
1521 // Different initial state, so state is reset.
1522 expect(container.firstChild).not.toBe(el);
1523 el = container.firstChild;
1524 expect(el.textContent).toBe('C2');
1525 }
1526 });
1527
1528 // This is best effort for simple cases.
1529 // We won't attempt to resolve identifiers.
1530 it('resets state when useReducer initial state is edited', async () => {
1531 if (__DEV__) {
1532 await render(`
1533 const {useReducer} = React;
1534
1535 export default function App() {
1536 const [foo, setFoo] = useReducer(x => x, 1);
1537 return <h1>A{foo}</h1>;
1538 }
1539 `);
1540 let el = container.firstChild;
1541 expect(el.textContent).toBe('A1');
1542
1543 await patch(`
1544 const {useReducer} = React;
1545
1546 export default function App() {
1547 const [foo, setFoo] = useReducer(x => x, 1);
1548 return <h1>B{foo}</h1>;
1549 }
1550 `);
1551 // Same initial state, so it's preserved.
1552 expect(container.firstChild).toBe(el);
1553 expect(el.textContent).toBe('B1');
1554
1555 await patch(`
1556 const {useReducer} = React;
1557
1558 export default function App() {
1559 const [foo, setFoo] = useReducer(x => x, 2);
1560 return <h1>C{foo}</h1>;
1561 }
1562 `);
1563 // Different initial state, so state is reset.
1564 expect(container.firstChild).not.toBe(el);
1565 el = container.firstChild;
1566 expect(el.textContent).toBe('C2');
1567 }
1568 });
1569
1570 it('remounts when switching export from function to class', async () => {
1571 if (__DEV__) {
1572 await render(`
1573 export default function App() {
1574 return <h1>A1</h1>;
1575 }
1576 `);
1577 let el = container.firstChild;
1578 expect(el.textContent).toBe('A1');
1579 await patch(`
1580 export default function App() {
1581 return <h1>A2</h1>;
1582 }
1583 `);
1584 // Keep state.
1585 expect(container.firstChild).toBe(el);
1586 expect(el.textContent).toBe('A2');
1587
1588 await patch(`
1589 export default class App extends React.Component {
1590 render() {
1591 return <h1>B1</h1>
1592 }
1593 }
1594 `);
1595 // Reset (function -> class).
1596 expect(container.firstChild).not.toBe(el);
1597 el = container.firstChild;
1598 expect(el.textContent).toBe('B1');
1599 await patch(`
1600 export default class App extends React.Component {
1601 render() {
1602 return <h1>B2</h1>
1603 }
1604 }
1605 `);
1606 // Reset (classes always do).
1607 expect(container.firstChild).not.toBe(el);
1608 el = container.firstChild;
1609 expect(el.textContent).toBe('B2');
1610
1611 await patch(`
1612 export default function App() {
1613 return <h1>C1</h1>;
1614 }
1615 `);
1616 // Reset (class -> function).
1617 expect(container.firstChild).not.toBe(el);
1618 el = container.firstChild;
1619 expect(el.textContent).toBe('C1');
1620 await patch(`
1621 export default function App() {
1622 return <h1>C2</h1>;
1623 }
1624 `);
1625 expect(container.firstChild).toBe(el);
1626 expect(el.textContent).toBe('C2');
1627
1628 await patch(`
1629 export default function App() {
1630 return <h1>D1</h1>;
1631 }
1632 `);
1633 el = container.firstChild;
1634 expect(el.textContent).toBe('D1');
1635 await patch(`
1636 export default function App() {
1637 return <h1>D2</h1>;
1638 }
1639 `);
1640 // Keep state.
1641 expect(container.firstChild).toBe(el);
1642 expect(el.textContent).toBe('D2');
1643 }
1644 });
1645
1646 it('remounts when switching export from class to function', async () => {
1647 if (__DEV__) {
1648 await render(`
1649 export default class App extends React.Component {
1650 render() {
1651 return <h1>A1</h1>
1652 }
1653 }
1654 `);
1655 let el = container.firstChild;
1656 expect(el.textContent).toBe('A1');
1657 await patch(`
1658 export default class App extends React.Component {
1659 render() {
1660 return <h1>A2</h1>
1661 }
1662 }
1663 `);
1664 // Reset (classes always do).
1665 expect(container.firstChild).not.toBe(el);
1666 el = container.firstChild;
1667 expect(el.textContent).toBe('A2');
1668
1669 await patch(`
1670 export default function App() {
1671 return <h1>B1</h1>;
1672 }
1673 `);
1674 // Reset (class -> function).
1675 expect(container.firstChild).not.toBe(el);
1676 el = container.firstChild;
1677 expect(el.textContent).toBe('B1');
1678 await patch(`
1679 export default function App() {
1680 return <h1>B2</h1>;
1681 }
1682 `);
1683 // Keep state.
1684 expect(container.firstChild).toBe(el);
1685 expect(el.textContent).toBe('B2');
1686
1687 await patch(`
1688 export default class App extends React.Component {
1689 render() {
1690 return <h1>C1</h1>
1691 }
1692 }
1693 `);
1694 // Reset (function -> class).
1695 expect(container.firstChild).not.toBe(el);
1696 el = container.firstChild;
1697 expect(el.textContent).toBe('C1');
1698 }
1699 });
1700
1701 it('remounts when wrapping export in a HOC', async () => {
1702 if (__DEV__) {
1703 await render(`
1704 export default function App() {
1705 return <h1>A1</h1>;
1706 }
1707 `);
1708 let el = container.firstChild;
1709 expect(el.textContent).toBe('A1');
1710 await patch(`
1711 export default function App() {
1712 return <h1>A2</h1>;
1713 }
1714 `);
1715 // Keep state.
1716 expect(container.firstChild).toBe(el);
1717 expect(el.textContent).toBe('A2');
1718
1719 await patch(`
1720 function hoc(Inner) {
1721 return function Wrapper() {
1722 return <Inner />;
1723 }
1724 }
1725
1726 function App() {
1727 return <h1>B1</h1>;
1728 }
1729
1730 export default hoc(App);
1731 `);
1732 // Reset (wrapped in HOC).
1733 expect(container.firstChild).not.toBe(el);
1734 el = container.firstChild;
1735 expect(el.textContent).toBe('B1');
1736 await patch(`
1737 function hoc(Inner) {
1738 return function Wrapper() {
1739 return <Inner />;
1740 }
1741 }
1742
1743 function App() {
1744 return <h1>B2</h1>;
1745 }
1746
1747 export default hoc(App);
1748 `);
1749 // Keep state.
1750 expect(container.firstChild).toBe(el);
1751 expect(el.textContent).toBe('B2');
1752
1753 await patch(`
1754 export default function App() {
1755 return <h1>C1</h1>;
1756 }
1757 `);
1758 // Reset (unwrapped).
1759 expect(container.firstChild).not.toBe(el);
1760 el = container.firstChild;
1761 expect(el.textContent).toBe('C1');
1762 await patch(`
1763 export default function App() {
1764 return <h1>C2</h1>;
1765 }
1766 `);
1767 expect(container.firstChild).toBe(el);
1768 expect(el.textContent).toBe('C2');
1769 }
1770 });
1771
1772 it('remounts when wrapping export in memo()', async () => {
1773 if (__DEV__) {
1774 await render(`
1775 export default function App() {
1776 return <h1>A1</h1>;
1777 }
1778 `);
1779 let el = container.firstChild;
1780 expect(el.textContent).toBe('A1');
1781 await patch(`
1782 export default function App() {
1783 return <h1>A2</h1>;
1784 }
1785 `);
1786 // Keep state.
1787 expect(container.firstChild).toBe(el);
1788 expect(el.textContent).toBe('A2');
1789
1790 await patch(`
1791 function App() {
1792 return <h1>B1</h1>;
1793 }
1794
1795 export default React.memo(App);
1796 `);
1797 // Reset (wrapped in HOC).
1798 expect(container.firstChild).not.toBe(el);
1799 el = container.firstChild;
1800 expect(el.textContent).toBe('B1');
1801 await patch(`
1802 function App() {
1803 return <h1>B2</h1>;
1804 }
1805
1806 export default React.memo(App);
1807 `);
1808 // Keep state.
1809 expect(container.firstChild).toBe(el);
1810 expect(el.textContent).toBe('B2');
1811
1812 await patch(`
1813 export default function App() {
1814 return <h1>C1</h1>;
1815 }
1816 `);
1817 // Reset (unwrapped).
1818 expect(container.firstChild).not.toBe(el);
1819 el = container.firstChild;
1820 expect(el.textContent).toBe('C1');
1821 await patch(`
1822 export default function App() {
1823 return <h1>C2</h1>;
1824 }
1825 `);
1826 expect(container.firstChild).toBe(el);
1827 expect(el.textContent).toBe('C2');
1828 }
1829 });
1830
1831 it('remounts when wrapping export in forwardRef()', async () => {
1832 if (__DEV__) {
1833 await render(`
1834 export default function App() {
1835 return <h1>A1</h1>;
1836 }
1837 `);
1838 let el = container.firstChild;
1839 expect(el.textContent).toBe('A1');
1840 await patch(`
1841 export default function App() {
1842 return <h1>A2</h1>;
1843 }
1844 `);
1845 // Keep state.
1846 expect(container.firstChild).toBe(el);
1847 expect(el.textContent).toBe('A2');
1848
1849 await patch(`
1850 function App() {
1851 return <h1>B1</h1>;
1852 }
1853
1854 export default React.forwardRef(App);
1855 `);
1856 // Reset (wrapped in HOC).
1857 expect(container.firstChild).not.toBe(el);
1858 el = container.firstChild;
1859 expect(el.textContent).toBe('B1');
1860 await patch(`
1861 function App() {
1862 return <h1>B2</h1>;
1863 }
1864
1865 export default React.forwardRef(App);
1866 `);
1867 // Keep state.
1868 expect(container.firstChild).toBe(el);
1869 expect(el.textContent).toBe('B2');
1870
1871 await patch(`
1872 export default function App() {
1873 return <h1>C1</h1>;
1874 }
1875 `);
1876 // Reset (unwrapped).
1877 expect(container.firstChild).not.toBe(el);
1878 el = container.firstChild;
1879 expect(el.textContent).toBe('C1');
1880 await patch(`
1881 export default function App() {
1882 return <h1>C2</h1>;
1883 }
1884 `);
1885 expect(container.firstChild).toBe(el);
1886 expect(el.textContent).toBe('C2');
1887 }
1888 });
1889
1890 it('resets useMemoCache cache slots', async () => {
1891 if (__DEV__) {
1892 await render(`
1893 const useMemoCache = require('react/compiler-runtime').c;
1894 let cacheMisses = 0;
1895 const cacheMiss = (id) => {
1896 cacheMisses++;
1897 return id;
1898 };
1899 export default function App(t0) {
1900 const $ = useMemoCache(1);
1901 const {reset1} = t0;
1902 let t1;
1903 if ($[0] !== reset1) {
1904 $[0] = t1 = cacheMiss({reset1});
1905 } else {
1906 t1 = $[1];
1907 }
1908 return <h1>{cacheMisses}</h1>;
1909 }
1910 `);
1911 const el = container.firstChild;
1912 expect(el.textContent).toBe('1');
1913 await patch(`
1914 const useMemoCache = require('react/compiler-runtime').c;
1915 let cacheMisses = 0;
1916 const cacheMiss = (id) => {
1917 cacheMisses++;
1918 return id;
1919 };
1920 export default function App(t0) {
1921 const $ = useMemoCache(2);
1922 const {reset1, reset2} = t0;
1923 let t1;
1924 if ($[0] !== reset1) {
1925 $[0] = t1 = cacheMiss({reset1});
1926 } else {
1927 t1 = $[1];
1928 }
1929 let t2;
1930 if ($[1] !== reset2) {
1931 $[1] = t2 = cacheMiss({reset2});
1932 } else {
1933 t2 = $[1];
1934 }
1935 return <h1>{cacheMisses}</h1>;
1936 }
1937 `);
1938 expect(container.firstChild).toBe(el);
1939 // cache size changed between refreshes
1940 expect(el.textContent).toBe('2');
1941 }
1942 });
1943
1944 describe('with inline requires', () => {
1945 beforeEach(() => {
1946 global.FakeModuleSystem = {};
1947 });
1948
1949 afterEach(() => {
1950 delete global.FakeModuleSystem;
1951 });
1952
1953 it('remounts component if custom hook it uses changes order on first edit', async () => {
1954 // This test verifies that remounting works even if calls to custom Hooks
1955 // were transformed with an inline requires transform, like we have on RN.
1956 // Inline requires make it harder to compare previous and next signatures
1957 // because useFancyState inline require always resolves to the newest version.
1958 // We're not actually using inline requires in the test, but it has similar semantics.
1959 if (__DEV__) {
1960 await render(`
1961 const FakeModuleSystem = global.FakeModuleSystem;
1962
1963 FakeModuleSystem.useFancyState = function(initialState) {
1964 return React.useState(initialState);
1965 };
1966
1967 const App = () => {
1968 const [x, setX] = FakeModuleSystem.useFancyState('X');
1969 const [y, setY] = FakeModuleSystem.useFancyState('Y');
1970 return <h1>A{x}{y}</h1>;
1971 };
1972
1973 export default App;
1974 `);
1975 let el = container.firstChild;
1976 expect(el.textContent).toBe('AXY');
1977
1978 await patch(`
1979 const FakeModuleSystem = global.FakeModuleSystem;
1980
1981 FakeModuleSystem.useFancyState = function(initialState) {
1982 React.useEffect(() => {});
1983 return React.useState(initialState);
1984 };
1985
1986 const App = () => {
1987 const [x, setX] = FakeModuleSystem.useFancyState('X');
1988 const [y, setY] = FakeModuleSystem.useFancyState('Y');
1989 return <h1>B{x}{y}</h1>;
1990 };
1991
1992 export default App;
1993 `);
1994 // The useFancyState Hook added an effect,
1995 // so we had to remount the component.
1996 expect(container.firstChild).not.toBe(el);
1997 el = container.firstChild;
1998 expect(el.textContent).toBe('BXY');
1999
2000 await patch(`
2001 const FakeModuleSystem = global.FakeModuleSystem;
2002
2003 FakeModuleSystem.useFancyState = function(initialState) {
2004 React.useEffect(() => {});
2005 return React.useState(initialState);
2006 };
2007
2008 const App = () => {
2009 const [x, setX] = FakeModuleSystem.useFancyState('X');
2010 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2011 return <h1>C{x}{y}</h1>;
2012 };
2013
2014 export default App;
2015 `);
2016 // We didn't change anything except the header text.
2017 // So we don't expect a remount.
2018 expect(container.firstChild).toBe(el);
2019 expect(el.textContent).toBe('CXY');
2020 }
2021 });
2022
2023 it('remounts component if custom hook it uses changes order on second edit', async () => {
2024 if (__DEV__) {
2025 await render(`
2026 const FakeModuleSystem = global.FakeModuleSystem;
2027
2028 FakeModuleSystem.useFancyState = function(initialState) {
2029 return React.useState(initialState);
2030 };
2031
2032 const App = () => {
2033 const [x, setX] = FakeModuleSystem.useFancyState('X');
2034 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2035 return <h1>A{x}{y}</h1>;
2036 };
2037
2038 export default App;
2039 `);
2040 let el = container.firstChild;
2041 expect(el.textContent).toBe('AXY');
2042
2043 await patch(`
2044 const FakeModuleSystem = global.FakeModuleSystem;
2045
2046 FakeModuleSystem.useFancyState = function(initialState) {
2047 return React.useState(initialState);
2048 };
2049
2050 const App = () => {
2051 const [x, setX] = FakeModuleSystem.useFancyState('X');
2052 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2053 return <h1>B{x}{y}</h1>;
2054 };
2055
2056 export default App;
2057 `);
2058 expect(container.firstChild).toBe(el);
2059 expect(el.textContent).toBe('BXY');
2060
2061 await patch(`
2062 const FakeModuleSystem = global.FakeModuleSystem;
2063
2064 FakeModuleSystem.useFancyState = function(initialState) {
2065 React.useEffect(() => {});
2066 return React.useState(initialState);
2067 };
2068
2069 const App = () => {
2070 const [x, setX] = FakeModuleSystem.useFancyState('X');
2071 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2072 return <h1>C{x}{y}</h1>;
2073 };
2074
2075 export default App;
2076 `);
2077 // The useFancyState Hook added an effect,
2078 // so we had to remount the component.
2079 expect(container.firstChild).not.toBe(el);
2080 el = container.firstChild;
2081 expect(el.textContent).toBe('CXY');
2082
2083 await patch(`
2084 const FakeModuleSystem = global.FakeModuleSystem;
2085
2086 FakeModuleSystem.useFancyState = function(initialState) {
2087 React.useEffect(() => {});
2088 return React.useState(initialState);
2089 };
2090
2091 const App = () => {
2092 const [x, setX] = FakeModuleSystem.useFancyState('X');
2093 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2094 return <h1>D{x}{y}</h1>;
2095 };
2096
2097 export default App;
2098 `);
2099 // We didn't change anything except the header text.
2100 // So we don't expect a remount.
2101 expect(container.firstChild).toBe(el);
2102 expect(el.textContent).toBe('DXY');
2103 }
2104 });
2105
2106 it('recovers if evaluating Hook list throws', async () => {
2107 if (__DEV__) {
2108 await render(`
2109 let FakeModuleSystem = null;
2110
2111 global.FakeModuleSystem.useFancyState = function(initialState) {
2112 return React.useState(initialState);
2113 };
2114
2115 const App = () => {
2116 FakeModuleSystem = global.FakeModuleSystem;
2117 const [x, setX] = FakeModuleSystem.useFancyState('X');
2118 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2119 return <h1>A{x}{y}</h1>;
2120 };
2121
2122 export default App;
2123 `);
2124 let el = container.firstChild;
2125 expect(el.textContent).toBe('AXY');
2126
2127 await patch(`
2128 let FakeModuleSystem = null;
2129
2130 global.FakeModuleSystem.useFancyState = function(initialState) {
2131 React.useEffect(() => {});
2132 return React.useState(initialState);
2133 };
2134
2135 const App = () => {
2136 FakeModuleSystem = global.FakeModuleSystem;
2137 const [x, setX] = FakeModuleSystem.useFancyState('X');
2138 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2139 return <h1>B{x}{y}</h1>;
2140 };
2141
2142 export default App;
2143 `);
2144 // We couldn't evaluate the Hook signatures
2145 // so we had to remount the component.
2146 expect(container.firstChild).not.toBe(el);
2147 el = container.firstChild;
2148 expect(el.textContent).toBe('BXY');
2149 }
2150 });
2151
2152 it('remounts component if custom hook it uses changes order behind an indirection', async () => {
2153 if (__DEV__) {
2154 await render(`
2155 const FakeModuleSystem = global.FakeModuleSystem;
2156
2157 FakeModuleSystem.useFancyState = function(initialState) {
2158 return FakeModuleSystem.useIndirection(initialState);
2159 };
2160
2161 FakeModuleSystem.useIndirection = function(initialState) {
2162 return FakeModuleSystem.useOtherIndirection(initialState);
2163 };
2164
2165 FakeModuleSystem.useOtherIndirection = function(initialState) {
2166 return React.useState(initialState);
2167 };
2168
2169 const App = () => {
2170 const [x, setX] = FakeModuleSystem.useFancyState('X');
2171 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2172 return <h1>A{x}{y}</h1>;
2173 };
2174
2175 export default App;
2176 `);
2177 let el = container.firstChild;
2178 expect(el.textContent).toBe('AXY');
2179
2180 await patch(`
2181 const FakeModuleSystem = global.FakeModuleSystem;
2182
2183 FakeModuleSystem.useFancyState = function(initialState) {
2184 return FakeModuleSystem.useIndirection(initialState);
2185 };
2186
2187 FakeModuleSystem.useIndirection = function(initialState) {
2188 return FakeModuleSystem.useOtherIndirection(initialState);
2189 };
2190
2191 FakeModuleSystem.useOtherIndirection = function(initialState) {
2192 React.useEffect(() => {});
2193 return React.useState(initialState);
2194 };
2195
2196 const App = () => {
2197 const [x, setX] = FakeModuleSystem.useFancyState('X');
2198 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2199 return <h1>B{x}{y}</h1>;
2200 };
2201
2202 export default App;
2203 `);
2204
2205 // The useFancyState Hook added an effect,
2206 // so we had to remount the component.
2207 expect(container.firstChild).not.toBe(el);
2208 el = container.firstChild;
2209 expect(el.textContent).toBe('BXY');
2210
2211 await patch(`
2212 const FakeModuleSystem = global.FakeModuleSystem;
2213
2214 FakeModuleSystem.useFancyState = function(initialState) {
2215 return FakeModuleSystem.useIndirection(initialState);
2216 };
2217
2218 FakeModuleSystem.useIndirection = function(initialState) {
2219 return FakeModuleSystem.useOtherIndirection(initialState);
2220 };
2221
2222 FakeModuleSystem.useOtherIndirection = function(initialState) {
2223 React.useEffect(() => {});
2224 return React.useState(initialState);
2225 };
2226
2227 const App = () => {
2228 const [x, setX] = FakeModuleSystem.useFancyState('X');
2229 const [y, setY] = FakeModuleSystem.useFancyState('Y');
2230 return <h1>C{x}{y}</h1>;
2231 };
2232
2233 export default App;
2234 `);
2235 // We didn't change anything except the header text.
2236 // So we don't expect a remount.
2237 expect(container.firstChild).toBe(el);
2238 expect(el.textContent).toBe('CXY');
2239 }
2240 });
2241 });
2242 }
2243
2244 function testTypeScript(render, patch) {
2245 it('reloads component exported in typescript namespace', async () => {
2246 if (__DEV__) {
2247 await render(`
2248 namespace Foo {
2249 export namespace Bar {
2250 export const Child = ({prop}) => {
2251 return <h1>{prop}1</h1>
2252 };
2253 }
2254 }
2255
2256 export default function Parent() {
2257 return <Foo.Bar.Child prop={'A'} />;
2258 }
2259 `);
2260 const el = container.firstChild;
2261 expect(el.textContent).toBe('A1');
2262 await patch(`
2263 namespace Foo {
2264 export namespace Bar {
2265 export const Child = ({prop}) => {
2266 return <h1>{prop}2</h1>
2267 };
2268 }
2269 }
2270
2271 export default function Parent() {
2272 return <Foo.Bar.Child prop={'B'} />;
2273 }
2274 `);
2275 expect(container.firstChild).toBe(el);
2276 expect(el.textContent).toBe('B2');
2277 }
2278 });
2279 }
2280 });