@tanstack/react-router 0.0.1-beta.279 → 0.0.1-beta.280

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.
@@ -23,7 +23,6 @@ export interface RouteMatch<TRouteTree extends AnyRoute = AnyRoute, TRouteId ext
23
23
  context: RouteById<TRouteTree, TRouteId>['types']['allContext'];
24
24
  search: FullSearchSchema<TRouteTree> & RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'];
25
25
  fetchCount: number;
26
- shouldReloadDeps: any;
27
26
  abortController: AbortController;
28
27
  cause: 'preload' | 'enter' | 'stay';
29
28
  loaderDeps: RouteById<TRouteTree, TRouteId>['types']['loaderDeps'];
@@ -23,6 +23,10 @@ export declare class FileRoute<TFilePath extends keyof FileRoutesByPath, TParent
23
23
  pendingComponent?: import("./route").RouteComponent<any> | undefined;
24
24
  pendingMs?: number | undefined;
25
25
  pendingMinMs?: number | undefined;
26
+ staleTime?: number | undefined;
27
+ gcTime?: number | undefined;
28
+ preloadStaleTime?: number | undefined;
29
+ preloadGcTime?: number | undefined;
26
30
  preSearchFilters?: import("./route").SearchFilter<TFullSearchSchema, TFullSearchSchema>[] | undefined;
27
31
  postSearchFilters?: import("./route").SearchFilter<TFullSearchSchema, TFullSearchSchema>[] | undefined;
28
32
  onError?: ((err: any) => void) | undefined;
@@ -34,7 +34,6 @@ export type ParamsFallback<TPath extends string, TParams> = unknown extends TPar
34
34
  export type BaseRouteOptions<TParentRoute extends AnyRoute = AnyRoute, TCustomId extends string = string, TPath extends string = string, TSearchSchema extends Record<string, any> = {}, TFullSearchSchema extends Record<string, any> = TSearchSchema, TParams extends AnyPathParams = {}, TAllParams = ParamsFallback<TPath, TParams>, TRouteContext extends RouteContext = RouteContext, TAllContext extends Record<string, any> = AnyContext, TLoaderDeps extends Record<string, any> = {}, TLoaderData extends any = unknown> = RoutePathOptions<TCustomId, TPath> & {
35
35
  getParentRoute: () => TParentRoute;
36
36
  validateSearch?: SearchSchemaValidator<TSearchSchema>;
37
- shouldReload?: boolean | ((match: LoaderFnContext<TAllParams, TFullSearchSchema, TAllContext, TRouteContext>) => any);
38
37
  } & (keyof PickRequired<RouteContext> extends never ? {
39
38
  beforeLoad?: BeforeLoadFn<TFullSearchSchema, TParentRoute, TAllParams, TRouteContext>;
40
39
  } : {
@@ -70,6 +69,10 @@ export type UpdatableRouteOptions<TFullSearchSchema extends Record<string, any>>
70
69
  pendingComponent?: RouteComponent;
71
70
  pendingMs?: number;
72
71
  pendingMinMs?: number;
72
+ staleTime?: number;
73
+ gcTime?: number;
74
+ preloadStaleTime?: number;
75
+ preloadGcTime?: number;
73
76
  preSearchFilters?: SearchFilter<TFullSearchSchema>[];
74
77
  postSearchFilters?: SearchFilter<TFullSearchSchema>[];
75
78
  onError?: (err: any) => void;
@@ -42,7 +42,10 @@ export interface RouterOptions<TRouteTree extends AnyRoute, TDehydrated extends
42
42
  defaultPendingComponent?: RouteComponent;
43
43
  defaultPendingMs?: number;
44
44
  defaultPendingMinMs?: number;
45
- defaultPreloadMaxAge?: number;
45
+ defaultStaleTime?: number;
46
+ defaultPreloadStaleTime?: number;
47
+ defaultPreloadGcTime?: number;
48
+ defaultGcTime?: number;
46
49
  caseSensitive?: boolean;
47
50
  routeTree?: TRouteTree;
48
51
  basepath?: string;
@@ -65,7 +68,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
65
68
  isTransitioning: boolean;
66
69
  matches: RouteMatch<TRouteTree>[];
67
70
  pendingMatches?: RouteMatch<TRouteTree>[];
68
- preloadMatches: RouteMatch<TRouteTree>[];
71
+ cachedMatches: RouteMatch<TRouteTree>[];
69
72
  location: ParsedLocation<FullSearchSchema<TRouteTree>>;
70
73
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>;
71
74
  lastUpdated: number;
@@ -127,7 +130,6 @@ export declare class Router<TRouteTree extends AnyRoute = AnyRoute, TDehydrated
127
130
  navigateTimeout: Timeout | null;
128
131
  latestLoadPromise: Promise<void>;
129
132
  subscribers: Set<RouterListener<RouterEvent>>;
130
- pendingMatches: AnyRouteMatch[];
131
133
  injectedHtml: InjectedHtmlEntry[];
132
134
  dehydratedData?: TDehydrated;
133
135
  __store: Store<RouterState<TRouteTree>>;
@@ -1192,7 +1192,6 @@
1192
1192
  }
1193
1193
  }
1194
1194
  }
1195
- router.pendingMatches = [];
1196
1195
  router.__store.setState(s => ({
1197
1196
  ...s,
1198
1197
  isTransitioning: false,
@@ -1208,7 +1207,7 @@
1208
1207
  return null;
1209
1208
  }
1210
1209
  function getRouteMatch(state, id) {
1211
- return [...state.preloadMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1210
+ return [...state.cachedMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1212
1211
  }
1213
1212
  function useRouterState(opts) {
1214
1213
  const router = useRouter();
@@ -1755,7 +1754,7 @@
1755
1754
  // Combine the matches based on user router.options
1756
1755
  const pathTest = activeOptions?.exact ? s.location.pathname === next.pathname : pathIsFuzzyEqual;
1757
1756
  const hashTest = activeOptions?.includeHash ? s.location.hash === next.hash : true;
1758
- const searchTest = activeOptions?.includeSearch ?? true ? deepEqual(s.location.search, next.search, true) : true;
1757
+ const searchTest = activeOptions?.includeSearch ?? true ? deepEqual(s.location.search, next.search, !activeOptions?.exact) : true;
1759
1758
 
1760
1759
  // The final "active" test
1761
1760
  return pathTest && hashTest && searchTest;
@@ -2002,7 +2001,6 @@
2002
2001
  navigateTimeout = null;
2003
2002
  latestLoadPromise = Promise.resolve();
2004
2003
  subscribers = new Set();
2005
- pendingMatches = [];
2006
2004
  injectedHtml = [];
2007
2005
 
2008
2006
  // Must build in constructor
@@ -2326,7 +2324,6 @@
2326
2324
  routeContext: undefined,
2327
2325
  context: undefined,
2328
2326
  abortController: new AbortController(),
2329
- shouldReloadDeps: undefined,
2330
2327
  fetchCount: 0,
2331
2328
  cause,
2332
2329
  loaderDeps
@@ -2538,9 +2535,10 @@
2538
2535
  let latestPromise;
2539
2536
  let firstBadMatchIndex;
2540
2537
  const updateMatch = match => {
2541
- // const isPreload = this.state.preloadMatches.find((d) => d.id === match.id)
2538
+ // const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
2542
2539
  const isPending = this.state.pendingMatches?.find(d => d.id === match.id);
2543
- const matchesKey = preload ? 'preloadMatches' : isPending ? 'pendingMatches' : 'matches';
2540
+ const isMatched = this.state.matches.find(d => d.id === match.id);
2541
+ const matchesKey = isPending ? 'pendingMatches' : isMatched ? 'matches' : 'cachedMatches';
2544
2542
  this.__store.setState(s => ({
2545
2543
  ...s,
2546
2544
  [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
@@ -2665,65 +2663,24 @@
2665
2663
  }),
2666
2664
  cause: preload ? 'preload' : match.cause
2667
2665
  };
2668
-
2669
- // Default to reloading the route all the time
2670
- let shouldLoad = true;
2671
- const shouldReloadFn = route.options.shouldReload;
2672
- let shouldReloadDeps = typeof shouldReloadFn === 'function' ? shouldReloadFn(loaderContext) : !!(shouldReloadFn ?? true);
2673
- const compareDeps = () => {
2674
- if (typeof shouldReloadDeps === 'object') {
2675
- // compare the deps to see if they've changed
2676
- shouldLoad = !deepEqual(shouldReloadDeps, match.shouldReloadDeps);
2677
- } else {
2678
- shouldLoad = !!shouldReloadDeps;
2679
- }
2680
- };
2681
-
2682
- // If it's the first preload, or the route is entering, or we're
2683
- // invalidating, we definitely need to load the route
2684
- if (invalidate) ; else if (preload) {
2685
- if (!match.fetchCount) ; else {
2686
- compareDeps();
2687
- }
2688
- } else if (match.cause === 'enter') {
2689
- if (!match.fetchCount) ; else {
2690
- compareDeps();
2691
- }
2692
- } else {
2693
- compareDeps();
2694
- }
2695
- if (typeof shouldReloadDeps === 'object') {
2696
- matches[index] = match = {
2697
- ...match,
2698
- shouldReloadDeps
2699
- };
2666
+ if (match.fetchCount && match.status === 'success') {
2667
+ resolve();
2700
2668
  }
2701
2669
 
2702
- // If the user doesn't want the route to reload, just
2703
- // resolve with the existing loader data
2704
-
2705
- if (!shouldLoad) {
2706
- loadPromise = Promise.resolve(match.loaderData);
2707
- } else {
2708
- if (match.fetchCount && match.status === 'success') {
2709
- resolve();
2670
+ // Otherwise, load the route
2671
+ matches[index] = match = {
2672
+ ...match,
2673
+ isFetching: true,
2674
+ fetchCount: match.fetchCount + 1
2675
+ };
2676
+ const componentsPromise = Promise.all(componentTypes.map(async type => {
2677
+ const component = route.options[type];
2678
+ if (component?.preload) {
2679
+ await component.preload();
2710
2680
  }
2711
-
2712
- // Otherwise, load the route
2713
- matches[index] = match = {
2714
- ...match,
2715
- isFetching: true,
2716
- fetchCount: match.fetchCount + 1
2717
- };
2718
- const componentsPromise = Promise.all(componentTypes.map(async type => {
2719
- const component = route.options[type];
2720
- if (component?.preload) {
2721
- await component.preload();
2722
- }
2723
- }));
2724
- const loaderPromise = route.options.loader?.(loaderContext);
2725
- loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2726
- }
2681
+ }));
2682
+ const loaderPromise = route.options.loader?.(loaderContext);
2683
+ loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2727
2684
  }
2728
2685
  matches[index] = match = {
2729
2686
  ...match,
@@ -2762,24 +2719,23 @@
2762
2719
  ...match,
2763
2720
  error,
2764
2721
  status: 'error',
2765
- isFetching: false,
2766
- updatedAt: Date.now()
2722
+ isFetching: false
2767
2723
  };
2768
- } finally {
2769
- // If we showed the pending component, that means
2770
- // we already moved the pendingMatches to the matches
2771
- // state, so we need to update that specific match
2772
- if (didShowPending && pendingMinMs && match.showPending) {
2773
- updateMatch(match);
2774
- }
2775
2724
  }
2776
2725
  updateMatch(match);
2777
2726
  };
2778
- if (match.fetchCount && match.status === 'success') {
2779
- // Background Fetching
2780
- fetch();
2727
+
2728
+ // This is where all of the stale-while-revalidate magic happens
2729
+ const age = Date.now() - match.updatedAt;
2730
+ let staleAge = preload ? route.options.preloadStaleTime ?? this.options.defaultPreloadStaleTime ?? 30_000 // 30 seconds for preloads by default
2731
+ : route.options.staleTime ?? this.options.defaultStaleTime ?? 0;
2732
+ if (match.status === 'success') {
2733
+ // Background Fetching, no need to wait
2734
+ if (age > staleAge) {
2735
+ fetch();
2736
+ }
2781
2737
  } else {
2782
- // Critical Fetching
2738
+ // Critical Fetching, we need to await
2783
2739
 
2784
2740
  // If we need to potentially show the pending component,
2785
2741
  // start a timer to show it after the pendingMs
@@ -2798,9 +2754,6 @@
2798
2754
  await fetch();
2799
2755
  }
2800
2756
  resolve();
2801
- // No Fetching
2802
-
2803
- resolve();
2804
2757
  }));
2805
2758
  });
2806
2759
  await Promise.all(matchPromises);
@@ -2827,12 +2780,16 @@
2827
2780
  let pendingMatches;
2828
2781
  const previousMatches = this.state.matches;
2829
2782
  this.__store.batch(() => {
2830
- this.__store.setState(s => ({
2831
- ...s,
2832
- preloadMatches: s.preloadMatches.filter(d => {
2833
- return Date.now() - d.updatedAt < (this.options.defaultPreloadMaxAge ?? 3000);
2834
- })
2835
- }));
2783
+ // This is where all of the garbage collection magic happens
2784
+ this.__store.setState(s => {
2785
+ return {
2786
+ ...s,
2787
+ cachedMatches: s.cachedMatches.filter(d => {
2788
+ const route = this.looseRoutesById[d.routeId];
2789
+ return d.status !== 'error' && Date.now() - d.updatedAt < (route.options.gcTime ?? this.options.defaultGcTime ?? 5 * 60 * 1000);
2790
+ })
2791
+ };
2792
+ });
2836
2793
 
2837
2794
  // Match the routes
2838
2795
  pendingMatches = this.matchRoutes(next.pathname, next.search, {
@@ -2840,12 +2797,13 @@
2840
2797
  });
2841
2798
 
2842
2799
  // Ingest the new matches
2800
+ // If a cached moved to pendingMatches, remove it from cachedMatches
2843
2801
  this.__store.setState(s => ({
2844
2802
  ...s,
2845
2803
  isLoading: true,
2846
2804
  location: next,
2847
2805
  pendingMatches,
2848
- preloadMatches: s.preloadMatches.filter(d => {
2806
+ cachedMatches: s.cachedMatches.filter(d => {
2849
2807
  return !pendingMatches.find(e => e.id === d.id);
2850
2808
  })
2851
2809
  }));
@@ -2867,19 +2825,23 @@
2867
2825
  if (latestPromise = this.checkLatest(promise)) {
2868
2826
  return latestPromise;
2869
2827
  }
2870
- const exitingMatchIds = previousMatches.filter(id => !this.pendingMatches.includes(id));
2871
- const enteringMatchIds = this.pendingMatches.filter(id => !previousMatches.includes(id));
2872
- const stayingMatchIds = previousMatches.filter(id => this.pendingMatches.includes(id));
2828
+ const exitingMatches = previousMatches.filter(match => !pendingMatches.find(d => d.id === match.id));
2829
+ const enteringMatches = pendingMatches.filter(match => !previousMatches.find(d => d.id === match.id));
2830
+ const stayingMatches = previousMatches.filter(match => pendingMatches.find(d => d.id === match.id));
2831
+
2832
+ // Commit the pending matches. If a previous match was
2833
+ // removed, place it in the cachedMatches
2873
2834
  this.__store.setState(s => ({
2874
2835
  ...s,
2875
2836
  isLoading: false,
2876
2837
  matches: pendingMatches,
2877
- pendingMatches: undefined
2838
+ pendingMatches: undefined,
2839
+ cachedMatches: [...s.cachedMatches, ...exitingMatches.filter(d => d.status !== 'error')]
2878
2840
  }))
2879
2841
 
2880
2842
  //
2881
2843
  ;
2882
- [[exitingMatchIds, 'onLeave'], [enteringMatchIds, 'onEnter'], [stayingMatchIds, 'onStay']].forEach(([matches, hook]) => {
2844
+ [[exitingMatches, 'onLeave'], [enteringMatches, 'onEnter'], [stayingMatches, 'onStay']].forEach(([matches, hook]) => {
2883
2845
  matches.forEach(match => {
2884
2846
  this.looseRoutesById[match.routeId].options[hook]?.(match);
2885
2847
  });
@@ -2907,13 +2869,13 @@
2907
2869
  let matches = this.matchRoutes(next.pathname, next.search, {
2908
2870
  throwOnError: true
2909
2871
  });
2910
- const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.preloadMatches]?.map(d => [d.id, true]));
2872
+ const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.cachedMatches]?.map(d => [d.id, true]));
2911
2873
  this.__store.batch(() => {
2912
2874
  matches.forEach(match => {
2913
2875
  if (!loadedMatchIds[match.id]) {
2914
2876
  this.__store.setState(s => ({
2915
2877
  ...s,
2916
- preloadMatches: [...s.preloadMatches, match]
2878
+ cachedMatches: [...s.cachedMatches, match]
2917
2879
  }));
2918
2880
  }
2919
2881
  });
@@ -3043,7 +3005,7 @@
3043
3005
  location,
3044
3006
  matches: [],
3045
3007
  pendingMatches: [],
3046
- preloadMatches: [],
3008
+ cachedMatches: [],
3047
3009
  lastUpdated: Date.now()
3048
3010
  };
3049
3011
  }