@tanstack/react-router 0.0.1-beta.273 → 0.0.1-beta.275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -318,11 +318,18 @@ class Router {
318
318
  return [parentSearch, searchError];
319
319
  }
320
320
  })();
321
+
322
+ // This is where we need to call route.options.loaderDeps() to get any additional
323
+ // deps that the route's loader function might need to run. We need to do this
324
+ // before we create the match so that we can pass the deps to the route's
325
+ // potential key function which is used to uniquely identify the route match in state
326
+
327
+ const loaderDeps = route.options.loaderDeps?.({
328
+ search: preMatchSearch
329
+ }) ?? '';
330
+ const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : '';
321
331
  const interpolatedPath = path.interpolatePath(route.fullPath, routeParams);
322
- const matchId = path.interpolatePath(route.id, routeParams, true) + (route.options.key?.({
323
- search: preMatchSearch,
324
- location: this.state.location
325
- }) ?? '');
332
+ const matchId = path.interpolatePath(route.id, routeParams, true) + loaderDepsHash;
326
333
 
327
334
  // Waste not, want not. If we already have a match for this route,
328
335
  // reuse it. This is important for layout routes, which might stick
@@ -353,8 +360,9 @@ class Router {
353
360
  context: undefined,
354
361
  abortController: new AbortController(),
355
362
  shouldReloadDeps: undefined,
356
- fetchedAt: 0,
357
- cause
363
+ fetchCount: 0,
364
+ cause,
365
+ loaderDeps
358
366
  };
359
367
 
360
368
  // Regardless of whether we're reusing an existing match or creating
@@ -562,10 +570,13 @@ class Router {
562
570
  }) => {
563
571
  let latestPromise;
564
572
  let firstBadMatchIndex;
565
- const updatePendingMatch = match => {
573
+ const updateMatch = match => {
574
+ const isPreload = this.state.preloadMatches.find(d => d.id === match.id);
575
+ const isPending = this.state.pendingMatches?.find(d => d.id === match.id);
576
+ const matchesKey = isPreload ? 'preloadMatches' : isPending ? 'pendingMatches' : 'matches';
566
577
  this.__store.setState(s => ({
567
578
  ...s,
568
- pendingMatches: s.pendingMatches?.map(d => d.id === match.id ? match : d)
579
+ [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
569
580
  }));
570
581
  };
571
582
 
@@ -618,7 +629,7 @@ class Router {
618
629
  from: match.pathname
619
630
  }),
620
631
  buildLocation: this.buildLocation,
621
- cause: match.cause
632
+ cause: preload ? 'preload' : match.cause
622
633
  })) ?? {};
623
634
  if (redirects.isRedirect(beforeLoadContext)) {
624
635
  throw beforeLoadContext;
@@ -648,7 +659,7 @@ class Router {
648
659
  const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
649
660
  const matchPromises = [];
650
661
  validResolvedMatches.forEach((match, index) => {
651
- matchPromises.push((async () => {
662
+ matchPromises.push(new Promise(async resolve => {
652
663
  const parentMatchPromise = matchPromises[index - 1];
653
664
  const route = this.looseRoutesById[match.routeId];
654
665
  const handleErrorAndRedirect = err => {
@@ -663,92 +674,95 @@ class Router {
663
674
  let loadPromise;
664
675
  matches[index] = match = {
665
676
  ...match,
666
- fetchedAt: Date.now(),
667
677
  showPending: false
668
678
  };
679
+ let didShowPending = false;
669
680
  const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
670
- let pendingPromise;
671
- if (!preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent)) {
672
- pendingPromise = new Promise(r => setTimeout(r, pendingMs));
673
- }
674
- if (match.isFetching) {
675
- loadPromise = RouterProvider.getRouteMatch(this.state, match.id)?.loadPromise;
676
- } else {
677
- const loaderContext = {
678
- params: match.params,
679
- search: match.search,
680
- preload: !!preload,
681
- parentMatchPromise,
682
- abortController: match.abortController,
683
- context: match.context,
684
- location: this.state.location,
685
- navigate: opts => this.navigate({
686
- ...opts,
687
- from: match.pathname
688
- }),
689
- cause: match.cause
690
- };
681
+ const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
682
+ const shouldPending = !preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent);
683
+ const fetch = async () => {
684
+ if (match.isFetching) {
685
+ loadPromise = RouterProvider.getRouteMatch(this.state, match.id)?.loadPromise;
686
+ } else {
687
+ const loaderContext = {
688
+ params: match.params,
689
+ deps: match.loaderDeps,
690
+ preload: !!preload,
691
+ parentMatchPromise,
692
+ abortController: match.abortController,
693
+ context: match.context,
694
+ location: this.state.location,
695
+ navigate: opts => this.navigate({
696
+ ...opts,
697
+ from: match.pathname
698
+ }),
699
+ cause: preload ? 'preload' : match.cause
700
+ };
691
701
 
692
- // Default to reloading the route all the time
693
- let shouldReload = true;
694
- let shouldReloadDeps = typeof route.options.shouldReload === 'function' ? route.options.shouldReload?.(loaderContext) : !!(route.options.shouldReload ?? true);
695
- if (match.cause === 'enter' || invalidate) {
696
- match.shouldReloadDeps = shouldReloadDeps;
697
- } else if (match.cause === 'stay') {
698
- if (typeof shouldReloadDeps === 'object') {
699
- // compare the deps to see if they've changed
700
- shouldReload = !utils.deepEqual(shouldReloadDeps, match.shouldReloadDeps);
701
- match.shouldReloadDeps = shouldReloadDeps;
702
+ // Default to reloading the route all the time
703
+ let shouldLoad = true;
704
+ const shouldReloadFn = route.options.shouldReload;
705
+ let shouldReloadDeps = typeof shouldReloadFn === 'function' ? shouldReloadFn(loaderContext) : !!(shouldReloadFn ?? true);
706
+ const compareDeps = () => {
707
+ if (typeof shouldReloadDeps === 'object') {
708
+ // compare the deps to see if they've changed
709
+ shouldLoad = !utils.deepEqual(shouldReloadDeps, match.shouldReloadDeps);
710
+ } else {
711
+ shouldLoad = !!shouldReloadDeps;
712
+ }
713
+ };
714
+
715
+ // If it's the first preload, or the route is entering, or we're
716
+ // invalidating, we definitely need to load the route
717
+ if (invalidate) ; else if (preload) {
718
+ if (!match.fetchCount) ; else {
719
+ compareDeps();
720
+ }
721
+ } else if (match.cause === 'enter') {
722
+ if (!match.fetchCount) ; else {
723
+ compareDeps();
724
+ }
702
725
  } else {
703
- shouldReload = !!shouldReloadDeps;
726
+ compareDeps();
727
+ }
728
+ if (typeof shouldReloadDeps === 'object') {
729
+ matches[index] = match = {
730
+ ...match,
731
+ shouldReloadDeps
732
+ };
704
733
  }
705
- }
706
734
 
707
- // If the user doesn't want the route to reload, just
708
- // resolve with the existing loader data
735
+ // If the user doesn't want the route to reload, just
736
+ // resolve with the existing loader data
709
737
 
710
- if (!shouldReload) {
711
- loadPromise = Promise.resolve(match.loaderData);
712
- } else {
713
- // Otherwise, load the route
714
- matches[index] = match = {
715
- ...match,
716
- isFetching: true
717
- };
718
- const componentsPromise = Promise.all(componentTypes.map(async type => {
719
- const component = route.options[type];
720
- if (component?.preload) {
721
- await component.preload();
738
+ if (!shouldLoad) {
739
+ loadPromise = Promise.resolve(match.loaderData);
740
+ } else {
741
+ if (match.fetchCount && match.status === 'success') {
742
+ resolve();
722
743
  }
723
- }));
724
- const loaderPromise = route.options.loader?.(loaderContext);
725
- loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
726
- }
727
- }
728
- matches[index] = match = {
729
- ...match,
730
- loadPromise
731
- };
732
- if (!preload) {
733
- updatePendingMatch(match);
734
- }
735
- let didShowPending = false;
736
- const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
737
- await new Promise(async resolve => {
738
- // If the route has a pending component and a pendingMs option,
739
- // forcefully show the pending component
740
- if (pendingPromise) {
741
- pendingPromise.then(() => {
742
- if (latestPromise = checkLatest()) return;
743
- didShowPending = true;
744
+
745
+ // Otherwise, load the route
744
746
  matches[index] = match = {
745
747
  ...match,
746
- showPending: true
748
+ isFetching: true,
749
+ fetchCount: match.fetchCount + 1
747
750
  };
748
- updatePendingMatch(match);
749
- resolve();
750
- });
751
+ const componentsPromise = Promise.all(componentTypes.map(async type => {
752
+ const component = route.options[type];
753
+ if (component?.preload) {
754
+ await component.preload();
755
+ }
756
+ }));
757
+ const loaderPromise = route.options.loader?.(loaderContext);
758
+ loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
759
+ }
751
760
  }
761
+ matches[index] = match = {
762
+ ...match,
763
+ loadPromise
764
+ };
765
+ updateMatch(match);
752
766
  try {
753
767
  const loaderData = await loadPromise;
754
768
  if (latestPromise = checkLatest()) return await latestPromise;
@@ -789,18 +803,38 @@ class Router {
789
803
  // we already moved the pendingMatches to the matches
790
804
  // state, so we need to update that specific match
791
805
  if (didShowPending && pendingMinMs && match.showPending) {
792
- this.__store.setState(s => ({
793
- ...s,
794
- matches: s.matches?.map(d => d.id === match.id ? match : d)
795
- }));
806
+ updateMatch(match);
796
807
  }
797
808
  }
798
- if (!preload) {
799
- updatePendingMatch(match);
809
+ updateMatch(match);
810
+ };
811
+ if (match.fetchCount && match.status === 'success') {
812
+ // Background Fetching
813
+ fetch();
814
+ } else {
815
+ // Critical Fetching
816
+
817
+ // If we need to potentially show the pending component,
818
+ // start a timer to show it after the pendingMs
819
+ if (shouldPending) {
820
+ new Promise(r => setTimeout(r, pendingMs)).then(async () => {
821
+ if (latestPromise = checkLatest()) return latestPromise;
822
+ didShowPending = true;
823
+ matches[index] = match = {
824
+ ...match,
825
+ showPending: true
826
+ };
827
+ updateMatch(match);
828
+ resolve();
829
+ });
800
830
  }
801
- resolve();
802
- });
803
- })());
831
+ await fetch();
832
+ }
833
+ resolve();
834
+ // No Fetching
835
+
836
+ resolve();
837
+ }));
804
838
  });
805
839
  await Promise.all(matchPromises);
806
840
  return matches;
@@ -823,20 +857,32 @@ class Router {
823
857
  toLocation: next,
824
858
  pathChanged: pathDidChange
825
859
  });
826
-
827
- // Match the routes
828
- let pendingMatches = this.matchRoutes(next.pathname, next.search, {
829
- debug: true
830
- });
860
+ let pendingMatches;
831
861
  const previousMatches = this.state.matches;
862
+ this.__store.batch(() => {
863
+ this.__store.setState(s => ({
864
+ ...s,
865
+ preloadMatches: s.preloadMatches.filter(d => {
866
+ return Date.now() - d.updatedAt < (this.options.defaultPreloadMaxAge ?? 3000);
867
+ })
868
+ }));
832
869
 
833
- // Ingest the new matches
834
- this.__store.setState(s => ({
835
- ...s,
836
- isLoading: true,
837
- location: next,
838
- pendingMatches
839
- }));
870
+ // Match the routes
871
+ pendingMatches = this.matchRoutes(next.pathname, next.search, {
872
+ debug: true
873
+ });
874
+
875
+ // Ingest the new matches
876
+ this.__store.setState(s => ({
877
+ ...s,
878
+ isLoading: true,
879
+ location: next,
880
+ pendingMatches,
881
+ preloadMatches: s.preloadMatches.filter(d => {
882
+ return !pendingMatches.find(e => e.id === d.id);
883
+ })
884
+ }));
885
+ });
840
886
  try {
841
887
  try {
842
888
  // Load the matches
@@ -894,6 +940,17 @@ class Router {
894
940
  let matches = this.matchRoutes(next.pathname, next.search, {
895
941
  throwOnError: true
896
942
  });
943
+ const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.preloadMatches]?.map(d => [d.id, true]));
944
+ this.__store.batch(() => {
945
+ matches.forEach(match => {
946
+ if (!loadedMatchIds[match.id]) {
947
+ this.__store.setState(s => ({
948
+ ...s,
949
+ preloadMatches: [...s.preloadMatches, match]
950
+ }));
951
+ }
952
+ });
953
+ });
897
954
  matches = await this.loadMatches({
898
955
  matches,
899
956
  preload: true,
@@ -956,7 +1013,7 @@ class Router {
956
1013
  dehydrate = () => {
957
1014
  return {
958
1015
  state: {
959
- dehydratedMatches: this.state.matches.map(d => utils.pick(d, ['fetchedAt', 'id', 'status', 'updatedAt', 'loaderData']))
1016
+ dehydratedMatches: this.state.matches.map(d => utils.pick(d, ['id', 'status', 'updatedAt', 'loaderData']))
960
1017
  }
961
1018
  };
962
1019
  };
@@ -1019,6 +1076,7 @@ function getInitialRouterState(location) {
1019
1076
  location,
1020
1077
  matches: [],
1021
1078
  pendingMatches: [],
1079
+ preloadMatches: [],
1022
1080
  lastUpdated: Date.now()
1023
1081
  };
1024
1082
  }