@@ -485,6 +485,371 @@ describe("<Navigate>", () => {
485
485
</div>"
486
486
` ) ;
487
487
} ) ;
488
+
489
+ it ( "handles sync relative navigations in StrictMode using a data router" , async ( ) => {
490
+ const router = createMemoryRouter ( [
491
+ {
492
+ path : "/" ,
493
+ children : [
494
+ {
495
+ index : true ,
496
+ // This is a relative navigation from the current location of /a.
497
+ // Ensure we don't route from / -> /b -> /b/b
498
+ Component : ( ) => < Navigate to = { "b" } replace /> ,
499
+ } ,
500
+ {
501
+ path : "b" ,
502
+ element : < h1 > Page B</ h1 > ,
503
+ } ,
504
+ ] ,
505
+ } ,
506
+ ] ) ;
507
+
508
+ let { container } = render (
509
+ < React . StrictMode >
510
+ < RouterProvider router = { router } />
511
+ </ React . StrictMode >
512
+ ) ;
513
+
514
+ await waitFor ( ( ) => screen . getByText ( "Page B" ) ) ;
515
+
516
+ expect ( getHtml ( container ) ) . toMatchInlineSnapshot ( `
517
+ "<div>
518
+ <h1>
519
+ Page B
520
+ </h1>
521
+ </div>"
522
+ ` ) ;
523
+ } ) ;
524
+
525
+ it ( "handles async relative navigations in StrictMode using a data router" , async ( ) => {
526
+ const router = createMemoryRouter (
527
+ [
528
+ {
529
+ path : "/a" ,
530
+ children : [
531
+ {
532
+ index : true ,
533
+ // This is a relative navigation from the current location of /a.
534
+ // Ensure we don't route from /a -> /a/b -> /a/b/b
535
+ Component : ( ) => < Navigate to = { "b" } replace /> ,
536
+ } ,
537
+ {
538
+ path : "b" ,
539
+ async loader ( ) {
540
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
541
+ return null ;
542
+ } ,
543
+ element : < h1 > Page B</ h1 > ,
544
+ } ,
545
+ ] ,
546
+ } ,
547
+ ] ,
548
+ { initialEntries : [ "/a" ] }
549
+ ) ;
550
+
551
+ let { container } = render (
552
+ < React . StrictMode >
553
+ < RouterProvider router = { router } />
554
+ </ React . StrictMode >
555
+ ) ;
556
+
557
+ await waitFor ( ( ) => screen . getByText ( "Page B" ) ) ;
558
+
559
+ expect ( getHtml ( container ) ) . toMatchInlineSnapshot ( `
560
+ "<div>
561
+ <h1>
562
+ Page B
563
+ </h1>
564
+ </div>"
565
+ ` ) ;
566
+ } ) ;
567
+
568
+ it ( "handles setState in render in StrictMode using a data router (sync loader)" , async ( ) => {
569
+ let renders : number [ ] = [ ] ;
570
+ const router = createMemoryRouter ( [
571
+ {
572
+ path : "/" ,
573
+ children : [
574
+ {
575
+ index : true ,
576
+ Component ( ) {
577
+ let [ count , setCount ] = React . useState ( 0 ) ;
578
+ if ( count === 0 ) {
579
+ setCount ( 1 ) ;
580
+ }
581
+ return < Navigate to = { "b" } replace state = { { count } } /> ;
582
+ } ,
583
+ } ,
584
+ {
585
+ path : "b" ,
586
+ Component ( ) {
587
+ let { state } = useLocation ( ) as { state : { count : number } } ;
588
+ renders . push ( state . count ) ;
589
+ return (
590
+ < >
591
+ < h1 > Page B</ h1 >
592
+ < p > { state . count } </ p >
593
+ </ >
594
+ ) ;
595
+ } ,
596
+ } ,
597
+ ] ,
598
+ } ,
599
+ ] ) ;
600
+
601
+ let navigateSpy = jest . spyOn ( router , "navigate" ) ;
602
+
603
+ let { container } = render (
604
+ < React . StrictMode >
605
+ < RouterProvider router = { router } />
606
+ </ React . StrictMode >
607
+ ) ;
608
+
609
+ await waitFor ( ( ) => screen . getByText ( "Page B" ) ) ;
610
+
611
+ expect ( getHtml ( container ) ) . toMatchInlineSnapshot ( `
612
+ "<div>
613
+ <h1>
614
+ Page B
615
+ </h1>
616
+ <p>
617
+ 1
618
+ </p>
619
+ </div>"
620
+ ` ) ;
621
+ expect ( navigateSpy ) . toHaveBeenCalledTimes ( 2 ) ;
622
+ expect ( navigateSpy . mock . calls [ 0 ] ) . toMatchObject ( [
623
+ { pathname : "/b" } ,
624
+ { state : { count : 1 } } ,
625
+ ] ) ;
626
+ expect ( navigateSpy . mock . calls [ 1 ] ) . toMatchObject ( [
627
+ { pathname : "/b" } ,
628
+ { state : { count : 1 } } ,
629
+ ] ) ;
630
+ expect ( renders ) . toEqual ( [ 1 , 1 ] ) ;
631
+ } ) ;
632
+
633
+ it ( "handles setState in effect in StrictMode using a data router (sync loader)" , async ( ) => {
634
+ let renders : number [ ] = [ ] ;
635
+ const router = createMemoryRouter ( [
636
+ {
637
+ path : "/" ,
638
+ children : [
639
+ {
640
+ index : true ,
641
+ Component ( ) {
642
+ // When state managed by react and changes during render, we'll
643
+ // only "see" the value from the first pass through here in our
644
+ // effects
645
+ let [ count , setCount ] = React . useState ( 0 ) ;
646
+ React . useEffect ( ( ) => {
647
+ if ( count === 0 ) {
648
+ setCount ( 1 ) ;
649
+ }
650
+ } , [ count ] ) ;
651
+ return < Navigate to = { "b" } replace state = { { count } } /> ;
652
+ } ,
653
+ } ,
654
+ {
655
+ path : "b" ,
656
+ Component ( ) {
657
+ let { state } = useLocation ( ) as { state : { count : number } } ;
658
+ renders . push ( state . count ) ;
659
+ return (
660
+ < >
661
+ < h1 > Page B</ h1 >
662
+ < p > { state . count } </ p >
663
+ </ >
664
+ ) ;
665
+ } ,
666
+ } ,
667
+ ] ,
668
+ } ,
669
+ ] ) ;
670
+
671
+ let navigateSpy = jest . spyOn ( router , "navigate" ) ;
672
+
673
+ let { container } = render (
674
+ < React . StrictMode >
675
+ < RouterProvider router = { router } />
676
+ </ React . StrictMode >
677
+ ) ;
678
+
679
+ await waitFor ( ( ) => screen . getByText ( "Page B" ) ) ;
680
+
681
+ expect ( getHtml ( container ) ) . toMatchInlineSnapshot ( `
682
+ "<div>
683
+ <h1>
684
+ Page B
685
+ </h1>
686
+ <p>
687
+ 0
688
+ </p>
689
+ </div>"
690
+ ` ) ;
691
+ expect ( navigateSpy ) . toHaveBeenCalledTimes ( 2 ) ;
692
+ expect ( navigateSpy . mock . calls [ 0 ] ) . toMatchObject ( [
693
+ { pathname : "/b" } ,
694
+ { state : { count : 0 } } ,
695
+ ] ) ;
696
+ expect ( navigateSpy . mock . calls [ 1 ] ) . toMatchObject ( [
697
+ { pathname : "/b" } ,
698
+ { state : { count : 0 } } ,
699
+ ] ) ;
700
+ expect ( renders ) . toEqual ( [ 0 , 0 ] ) ;
701
+ } ) ;
702
+
703
+ it ( "handles setState in render in StrictMode using a data router (async loader)" , async ( ) => {
704
+ let renders : number [ ] = [ ] ;
705
+ const router = createMemoryRouter ( [
706
+ {
707
+ path : "/" ,
708
+ children : [
709
+ {
710
+ index : true ,
711
+ Component ( ) {
712
+ let [ count , setCount ] = React . useState ( 0 ) ;
713
+ if ( count === 0 ) {
714
+ setCount ( 1 ) ;
715
+ }
716
+ return < Navigate to = { "b" } replace state = { { count } } /> ;
717
+ } ,
718
+ } ,
719
+ {
720
+ path : "b" ,
721
+ async loader ( ) {
722
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
723
+ return null ;
724
+ } ,
725
+ Component ( ) {
726
+ let { state } = useLocation ( ) as { state : { count : number } } ;
727
+ renders . push ( state . count ) ;
728
+ return (
729
+ < >
730
+ < h1 > Page B</ h1 >
731
+ < p > { state . count } </ p >
732
+ </ >
733
+ ) ;
734
+ } ,
735
+ } ,
736
+ ] ,
737
+ } ,
738
+ ] ) ;
739
+
740
+ let navigateSpy = jest . spyOn ( router , "navigate" ) ;
741
+
742
+ let { container } = render (
743
+ < React . StrictMode >
744
+ < RouterProvider router = { router } />
745
+ </ React . StrictMode >
746
+ ) ;
747
+
748
+ await waitFor ( ( ) => screen . getByText ( "Page B" ) ) ;
749
+
750
+ expect ( getHtml ( container ) ) . toMatchInlineSnapshot ( `
751
+ "<div>
752
+ <h1>
753
+ Page B
754
+ </h1>
755
+ <p>
756
+ 1
757
+ </p>
758
+ </div>"
759
+ ` ) ;
760
+ expect ( navigateSpy ) . toHaveBeenCalledTimes ( 2 ) ;
761
+ expect ( navigateSpy . mock . calls [ 0 ] ) . toMatchObject ( [
762
+ { pathname : "/b" } ,
763
+ { state : { count : 1 } } ,
764
+ ] ) ;
765
+ expect ( navigateSpy . mock . calls [ 1 ] ) . toMatchObject ( [
766
+ { pathname : "/b" } ,
767
+ { state : { count : 1 } } ,
768
+ ] ) ;
769
+ // /a/b rendered with the same state value both times
770
+ expect ( renders ) . toEqual ( [ 1 , 1 ] ) ;
771
+ } ) ;
772
+
773
+ it ( "handles setState in effect in StrictMode using a data router (async loader)" , async ( ) => {
774
+ let renders : number [ ] = [ ] ;
775
+ const router = createMemoryRouter ( [
776
+ {
777
+ path : "/" ,
778
+ children : [
779
+ {
780
+ index : true ,
781
+ Component ( ) {
782
+ // When state managed by react and changes during render, we'll
783
+ // only "see" the value from the first pass through here in our
784
+ // effects
785
+ let [ count , setCount ] = React . useState ( 0 ) ;
786
+ React . useEffect ( ( ) => {
787
+ if ( count === 0 ) {
788
+ setCount ( 1 ) ;
789
+ }
790
+ } , [ count ] ) ;
791
+ return < Navigate to = { "b" } replace state = { { count } } /> ;
792
+ } ,
793
+ } ,
794
+ {
795
+ path : "b" ,
796
+ async loader ( ) {
797
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
798
+ return null ;
799
+ } ,
800
+ Component ( ) {
801
+ let { state } = useLocation ( ) as { state : { count : number } } ;
802
+ renders . push ( state . count ) ;
803
+ return (
804
+ < >
805
+ < h1 > Page B</ h1 >
806
+ < p > { state . count } </ p >
807
+ </ >
808
+ ) ;
809
+ } ,
810
+ } ,
811
+ ] ,
812
+ } ,
813
+ ] ) ;
814
+
815
+ let navigateSpy = jest . spyOn ( router , "navigate" ) ;
816
+
817
+ let { container } = render (
818
+ < React . StrictMode >
819
+ < RouterProvider router = { router } />
820
+ </ React . StrictMode >
821
+ ) ;
822
+
823
+ await waitFor ( ( ) => screen . getByText ( "Page B" ) ) ;
824
+
825
+ expect ( getHtml ( container ) ) . toMatchInlineSnapshot ( `
826
+ "<div>
827
+ <h1>
828
+ Page B
829
+ </h1>
830
+ <p>
831
+ 1
832
+ </p>
833
+ </div>"
834
+ ` ) ;
835
+ expect ( navigateSpy ) . toHaveBeenCalledTimes ( 3 ) ;
836
+ expect ( navigateSpy . mock . calls [ 0 ] ) . toMatchObject ( [
837
+ { pathname : "/b" } ,
838
+ { state : { count : 0 } } ,
839
+ ] ) ;
840
+ expect ( navigateSpy . mock . calls [ 1 ] ) . toMatchObject ( [
841
+ { pathname : "/b" } ,
842
+ { state : { count : 0 } } ,
843
+ ] ) ;
844
+ // StrictMode only applies the double-effect execution on component mount,
845
+ // not component update
846
+ expect ( navigateSpy . mock . calls [ 2 ] ) . toMatchObject ( [
847
+ { pathname : "/b" } ,
848
+ { state : { count : 1 } } ,
849
+ ] ) ;
850
+ // /a/b rendered with the latest state value both times
851
+ expect ( renders ) . toEqual ( [ 1 , 1 ] ) ;
852
+ } ) ;
488
853
} ) ;
489
854
490
855
function getHtml ( container : HTMLElement ) {
0 commit comments