57
}).code;
58
const exportsObj = {};
59
// eslint-disable-next-line no-new-func
60
- new Function('React', 'exports', '__register__', compiled)(
60
+ new Function('React', 'exports', '__register__', '__signature__', compiled)(
61
React,
62
exportsObj,
63
__register__,
64
+ __signature__,
65
);
66
return exportsObj.default;
67
}
76
function patch(source) {
77
execute(source);
78
const hotUpdate = ReactFreshRuntime.prepareUpdate();
78
- scheduleHotUpdate(lastRoot, hotUpdate);
79
+ act(() => {
80
+ scheduleHotUpdate(lastRoot, hotUpdate);
81
+ });
82
}
83
84
function __register__(type, id) {
85
ReactFreshRuntime.register(type, id);
86
}
87
88
+ function __signature__(type, key, getCustomHooks) {
89
+ ReactFreshRuntime.setSignature(type, key, getCustomHooks);
90
+ return type;
91
+ }
92
+
93
it('reloads function declarations', () => {
94
if (__DEV__) {
95
render(`
252
expect(el.textContent).toBe('B2');
253
}
254
});
255
+
256
+ it('resets state when renaming a state variable', () => {
257
+ if (__DEV__) {
258
+ render(`
259
+ const {useState} = React;
260
+
261
+ export default function App() {
262
+ const [foo, setFoo] = useState(1);
263
+ return <h1>A{foo}</h1>;
264
+ }
265
+ `);
266
+ const el = container.firstChild;
267
+ expect(el.textContent).toBe('A1');
268
+
269
+ patch(`
270
+ const {useState} = React;
271
+
272
+ export default function App() {
273
+ const [foo, setFoo] = useState('ignored');
274
+ return <h1>B{foo}</h1>;
275
+ }
276
+ `);
277
+ // Same state variable name, so state is preserved.
278
+ expect(container.firstChild).toBe(el);
279
+ expect(el.textContent).toBe('B1');
280
+
281
+ patch(`
282
+ const {useState} = React;
283
+
284
+ export default function App() {
285
+ const [bar, setBar] = useState(2);
286
+ return <h1>C{bar}</h1>;
287
+ }
288
+ `);
289
+ // Different state variable name, so state is reset.
290
+ expect(container.firstChild).not.toBe(el);
291
+ const newEl = container.firstChild;
292
+ expect(newEl.textContent).toBe('C2');
293
+ }
294
+ });
295
+
296
+ it('resets state when renaming a state variable in a HOC', () => {
297
+ if (__DEV__) {
298
+ render(`
299
+ const {useState} = React;
300
+
301
+ function hoc(Wrapped) {
302
+ return function Generated() {
303
+ const [foo, setFoo] = useState(1);
304
+ return <Wrapped value={foo} />;
305
+ };
306
+ }
307
+
308
+ export default hoc(({ value }) => {
309
+ return <h1>A{value}</h1>;
310
+ });
311
+ `);
312
+ const el = container.firstChild;
313
+ expect(el.textContent).toBe('A1');
314
+
315
+ patch(`
316
+ const {useState} = React;
317
+
318
+ function hoc(Wrapped) {
319
+ return function Generated() {
320
+ const [foo, setFoo] = useState('ignored');
321
+ return <Wrapped value={foo} />;
322
+ };
323
+ }
324
+
325
+ export default hoc(({ value }) => {
326
+ return <h1>B{value}</h1>;
327
+ });
328
+ `);
329
+ // Same state variable name, so state is preserved.
330
+ expect(container.firstChild).toBe(el);
331
+ expect(el.textContent).toBe('B1');
332
+
333
+ patch(`
334
+ const {useState} = React;
335
+
336
+ function hoc(Wrapped) {
337
+ return function Generated() {
338
+ const [bar, setBar] = useState(2);
339
+ return <Wrapped value={bar} />;
340
+ };
341
+ }
342
+
343
+ export default hoc(({ value }) => {
344
+ return <h1>C{value}</h1>;
345
+ });
346
+ `);
347
+ // Different state variable name, so state is reset.
348
+ expect(container.firstChild).not.toBe(el);
349
+ const newEl = container.firstChild;
350
+ expect(newEl.textContent).toBe('C2');
351
+ }
352
+ });
353
+
354
+ it('resets state when renaming a state variable in a HOC with indirection', () => {
355
+ if (__DEV__) {
356
+ render(`
357
+ const {useState} = React;
358
+
359
+ function hoc(Wrapped) {
360
+ return function Generated() {
361
+ const [foo, setFoo] = useState(1);
362
+ return <Wrapped value={foo} />;
363
+ };
364
+ }
365
+
366
+ function Indirection({ value }) {
367
+ return <h1>A{value}</h1>;
368
+ }
369
+
370
+ export default hoc(Indirection);
371
+ `);
372
+ const el = container.firstChild;
373
+ expect(el.textContent).toBe('A1');
374
+
375
+ patch(`
376
+ const {useState} = React;
377
+
378
+ function hoc(Wrapped) {
379
+ return function Generated() {
380
+ const [foo, setFoo] = useState('ignored');
381
+ return <Wrapped value={foo} />;
382
+ };
383
+ }
384
+
385
+ function Indirection({ value }) {
386
+ return <h1>B{value}</h1>;
387
+ }
388
+
389
+ export default hoc(Indirection);
390
+ `);
391
+ // Same state variable name, so state is preserved.
392
+ expect(container.firstChild).toBe(el);
393
+ expect(el.textContent).toBe('B1');
394
+
395
+ patch(`
396
+ const {useState} = React;
397
+
398
+ function hoc(Wrapped) {
399
+ return function Generated() {
400
+ const [bar, setBar] = useState(2);
401
+ return <Wrapped value={bar} />;
402
+ };
403
+ }
404
+
405
+ function Indirection({ value }) {
406
+ return <h1>C{value}</h1>;
407
+ }
408
+
409
+ export default hoc(Indirection);
410
+ `);
411
+ // Different state variable name, so state is reset.
412
+ expect(container.firstChild).not.toBe(el);
413
+ const newEl = container.firstChild;
414
+ expect(newEl.textContent).toBe('C2');
415
+ }
416
+ });
417
+
418
+ it('resets effects while preserving state', () => {
419
+ if (__DEV__) {
420
+ render(`
421
+ const {useState} = React;
422
+
423
+ export default function App() {
424
+ const [value, setValue] = useState(0);
425
+ return <h1>A{value}</h1>;
426
+ }
427
+ `);
428
+ let el = container.firstChild;
429
+ expect(el.textContent).toBe('A0');
430
+
431
+ // Add an effect.
432
+ patch(`
433
+ const {useState} = React;
434
+
435
+ export default function App() {
436
+ const [value, setValue] = useState(0);
437
+ React.useEffect(() => {
438
+ const id = setInterval(() => {
439
+ setValue(v => v + 1);
440
+ }, 1000);
441
+ return () => clearInterval(id);
442
+ }, []);
443
+ return <h1>B{value}</h1>;
444
+ }
445
+ `);
446
+ // We added an effect, thereby changing Hook order.
447
+ // This causes a remount.
448
+ expect(container.firstChild).not.toBe(el);
449
+ el = container.firstChild;
450
+ expect(el.textContent).toBe('B0');
451
+
452
+ act(() => {
453
+ jest.advanceTimersByTime(1000);
454
+ });
455
+ expect(el.textContent).toBe('B1');
456
+
457
+ patch(`
458
+ const {useState} = React;
459
+
460
+ export default function App() {
461
+ const [value, setValue] = useState(0);
462
+ React.useEffect(() => {
463
+ const id = setInterval(() => {
464
+ setValue(v => v + 10);
465
+ }, 1000);
466
+ return () => clearInterval(id);
467
+ }, []);
468
+ return <h1>C{value}</h1>;
469
+ }
470
+ `);
471
+ // Same Hooks are called, so state is preserved.
472
+ expect(container.firstChild).toBe(el);
473
+ expect(el.textContent).toBe('C1');
474
+
475
+ // Effects are always reset, so timer was reinstalled.
476
+ // The new version increments by 10 rather than 1.
477
+ act(() => {
478
+ jest.advanceTimersByTime(1000);
479
+ });
480
+ expect(el.textContent).toBe('C11');
481
+
482
+ patch(`
483
+ const {useState} = React;
484
+
485
+ export default function App() {
486
+ const [value, setValue] = useState(0);
487
+ return <h1>D{value}</h1>;
488
+ }
489
+ `);
490
+ // Removing the effect changes the signature
491
+ // and causes the component to remount.
492
+ expect(container.firstChild).not.toBe(el);
493
+ el = container.firstChild;
494
+ expect(el.textContent).toBe('D0');
495
+ }
496
+ });
497
+
498
+ it('does not get confused when custom hooks are reordered', () => {
499
+ if (__DEV__) {
500
+ render(`
501
+ function useFancyState(initialState) {
502
+ return React.useState(initialState);
503
+ }
504
+
505
+ const App = () => {
506
+ const [x, setX] = useFancyState('X');
507
+ const [y, setY] = useFancyState('Y');
508
+ return <h1>A{x}{y}</h1>;
509
+ };
510
+
511
+ export default App;
512
+ `);
513
+ let el = container.firstChild;
514
+ expect(el.textContent).toBe('AXY');
515
+
516
+ patch(`
517
+ function useFancyState(initialState) {
518
+ return React.useState(initialState);
519
+ }
520
+
521
+ const App = () => {
522
+ const [x, setX] = useFancyState('X');
523
+ const [y, setY] = useFancyState('Y');
524
+ return <h1>B{x}{y}</h1>;
525
+ };
526
+
527
+ export default App;
528
+ `);
529
+ // Same state variables, so no remount.
530
+ expect(container.firstChild).toBe(el);
531
+ expect(el.textContent).toBe('BXY');
532
+
533
+ patch(`
534
+ function useFancyState(initialState) {
535
+ return React.useState(initialState);
536
+ }
537
+
538
+ const App = () => {
539
+ const [y, setY] = useFancyState('Y');
540
+ const [x, setX] = useFancyState('X');
541
+ return <h1>B{x}{y}</h1>;
542
+ };
543
+
544
+ export default App;
545
+ `);
546
+ // Hooks were re-ordered. This causes a remount.
547
+ // Therefore, Hook calls don't accidentally share state.
548
+ expect(container.firstChild).not.toBe(el);
549
+ el = container.firstChild;
550
+ expect(el.textContent).toBe('BXY');
551
+ }
552
+ });
553
+
554
+ it('remounts component if custom hook it uses changes order', () => {
555
+ if (__DEV__) {
556
+ render(`
557
+ const App = () => {
558
+ const [x, setX] = useFancyState('X');
559
+ const [y, setY] = useFancyState('Y');
560
+ return <h1>A{x}{y}</h1>;
561
+ };
562
+
563
+ const useFancyState = (initialState) => {
564
+ const result = useIndirection(initialState);
565
+ return result;
566
+ };
567
+
568
+ function useIndirection(initialState) {
569
+ return React.useState(initialState);
570
+ }
571
+
572
+ export default App;
573
+ `);
574
+ let el = container.firstChild;
575
+ expect(el.textContent).toBe('AXY');
576
+
577
+ patch(`
578
+ const App = () => {
579
+ const [x, setX] = useFancyState('X');
580
+ const [y, setY] = useFancyState('Y');
581
+ return <h1>B{x}{y}</h1>;
582
+ };
583
+
584
+ const useFancyState = (initialState) => {
585
+ const result = useIndirection();
586
+ return result;
587
+ };
588
+
589
+ function useIndirection(initialState) {
590
+ return React.useState(initialState);
591
+ }
592
+
593
+ export default App;
594
+ `);
595
+ // We didn't change anything except the header text.
596
+ // So we don't expect a remount.
597
+ expect(container.firstChild).toBe(el);
598
+ expect(el.textContent).toBe('BXY');
599
+
600
+ patch(`
601
+ const App = () => {
602
+ const [x, setX] = useFancyState('X');
603
+ const [y, setY] = useFancyState('Y');
604
+ return <h1>C{x}{y}</h1>;
605
+ };
606
+
607
+ const useFancyState = (initialState) => {
608
+ const result = useIndirection(initialState);
609
+ return result;
610
+ };
611
+
612
+ function useIndirection(initialState) {
613
+ React.useEffect(() => {});
614
+ return React.useState(initialState);
615
+ }
616
+
617
+ export default App;
618
+ `);
619
+ // The useIndirection Hook added an affect,
620
+ // so we had to remount the component.
621
+ expect(container.firstChild).not.toBe(el);
622
+ el = container.firstChild;
623
+ expect(el.textContent).toBe('CXY');
624
+
625
+ patch(`
626
+ const App = () => {
627
+ const [x, setX] = useFancyState('X');
628
+ const [y, setY] = useFancyState('Y');
629
+ return <h1>D{x}{y}</h1>;
630
+ };
631
+
632
+ const useFancyState = (initialState) => {
633
+ const result = useIndirection();
634
+ return result;
635
+ };
636
+
637
+ function useIndirection(initialState) {
638
+ React.useEffect(() => {});
639
+ return React.useState(initialState);
640
+ }
641
+
642
+ export default App;
643
+ `);
644
+ // We didn't change anything except the header text.
645
+ // So we don't expect a remount.
646
+ expect(container.firstChild).toBe(el);
647
+ expect(el.textContent).toBe('DXY');
648
+ }
649
+ });
650
+
651
+ it('does not lose the inferred arrow names', () => {
652
+ if (__DEV__) {
653
+ render(`
654
+ const Parent = () => {
655
+ return <Child/>;
656
+ };
657
+
658
+ const Child = () => {
659
+ useMyThing();
660
+ return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
661
+ };
662
+
663
+ const useMyThing = () => {
664
+ React.useState();
665
+ };
666
+
667
+ export default Parent;
668
+ `);
669
+ expect(container.textContent).toBe('Parent Child useMyThing');
670
+ }
671
+ });
672
+
673
+ it('does not lose the inferred function names', () => {
674
+ if (__DEV__) {
675
+ render(`
676
+ var Parent = function() {
677
+ return <Child/>;
678
+ };
679
+
680
+ var Child = function() {
681
+ useMyThing();
682
+ return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
683
+ };
684
+
685
+ var useMyThing = function() {
686
+ React.useState();
687
+ };
688
+
689
+ export default Parent;
690
+ `);
691
+ expect(container.textContent).toBe('Parent Child useMyThing');
692
+ }
693
+ });
694
});