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

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.
@@ -22,10 +22,10 @@ export interface RouteMatch<TRouteTree extends AnyRoute = AnyRoute, TRouteId ext
22
22
  routeContext: RouteById<TRouteTree, TRouteId>['types']['routeContext'];
23
23
  context: RouteById<TRouteTree, TRouteId>['types']['allContext'];
24
24
  search: FullSearchSchema<TRouteTree> & RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'];
25
- fetchedAt: number;
25
+ fetchCount: number;
26
26
  shouldReloadDeps: any;
27
27
  abortController: AbortController;
28
- cause: 'enter' | 'stay';
28
+ cause: 'preload' | 'enter' | 'stay';
29
29
  }
30
30
  export type AnyRouteMatch = RouteMatch<any>;
31
31
  export declare function Matches(): React.JSX.Element;
@@ -22,7 +22,7 @@ export type NavigateFn<TRouteTree extends AnyRoute> = <TFrom extends RoutePaths<
22
22
  export type MatchRouteFn<TRouteTree extends AnyRoute> = <TFrom extends RoutePaths<TRouteTree> = '/', TTo extends string = '', TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>>(location: ToOptions<TRouteTree, TFrom, TTo>, opts?: MatchRouteOptions) => false | RouteById<TRouteTree, TResolved>['types']['allParams'];
23
23
  export type BuildLocationFn<TRouteTree extends AnyRoute> = (opts: BuildNextOptions) => ParsedLocation;
24
24
  export type InjectedHtmlEntry = string | (() => Promise<string> | string);
25
- export declare const routerContext: React.Context<Router<any, Record<string, any>>>;
25
+ export declare let routerContext: React.Context<Router<any, Record<string, any>>>;
26
26
  export declare function RouterProvider<TRouteTree extends AnyRoute = RegisteredRouter['routeTree'], TDehydrated extends Record<string, any> = Record<string, any>>({ router, ...rest }: RouterProps<TRouteTree, TDehydrated>): React.JSX.Element;
27
27
  export declare function getRouteMatch<TRouteTree extends AnyRoute>(state: RouterState<TRouteTree>, id: string): undefined | RouteMatch<TRouteTree>;
28
28
  export declare function useRouterState<TSelected = RouterState<RegisteredRouter['routeTree']>>(opts?: {
@@ -61,7 +61,7 @@ type BeforeLoadFn<TFullSearchSchema extends Record<string, any>, TParentRoute ex
61
61
  location: ParsedLocation;
62
62
  navigate: NavigateFn<AnyRoute>;
63
63
  buildLocation: BuildLocationFn<AnyRoute>;
64
- cause: 'enter' | 'stay';
64
+ cause: 'preload' | 'enter' | 'stay';
65
65
  }) => Promise<TRouteContext> | TRouteContext | void;
66
66
  export type UpdatableRouteOptions<TFullSearchSchema extends Record<string, any>> = MetaOptions & {
67
67
  caseSensitive?: boolean;
@@ -102,7 +102,7 @@ export interface LoaderFnContext<TAllParams = {}, TFullSearchSchema extends Reco
102
102
  location: ParsedLocation<TFullSearchSchema>;
103
103
  navigate: (opts: NavigateOptions<AnyRoute>) => Promise<void>;
104
104
  parentMatchPromise?: Promise<void>;
105
- cause: 'enter' | 'stay';
105
+ cause: 'preload' | 'enter' | 'stay';
106
106
  }
107
107
  export type SearchFilter<T, U = T> = (prev: T) => U;
108
108
  export type ResolveId<TParentRoute, TCustomId extends string, TPath extends string> = TParentRoute extends {
@@ -41,6 +41,7 @@ export interface RouterOptions<TRouteTree extends AnyRoute, TDehydrated extends
41
41
  defaultPendingComponent?: RouteComponent;
42
42
  defaultPendingMs?: number;
43
43
  defaultPendingMinMs?: number;
44
+ defaultPreloadMaxAge?: number;
44
45
  caseSensitive?: boolean;
45
46
  routeTree?: TRouteTree;
46
47
  basepath?: string;
@@ -63,6 +64,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
63
64
  isTransitioning: boolean;
64
65
  matches: RouteMatch<TRouteTree>[];
65
66
  pendingMatches?: RouteMatch<TRouteTree>[];
67
+ preloadMatches: RouteMatch<TRouteTree>[];
66
68
  location: ParsedLocation<FullSearchSchema<TRouteTree>>;
67
69
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>;
68
70
  lastUpdated: number;
@@ -87,7 +89,7 @@ export interface BuildNextOptions {
87
89
  export interface DehydratedRouterState {
88
90
  dehydratedMatches: DehydratedRouteMatch[];
89
91
  }
90
- export type DehydratedRouteMatch = Pick<RouteMatch, 'fetchedAt' | 'id' | 'status' | 'updatedAt'>;
92
+ export type DehydratedRouteMatch = Pick<RouteMatch, 'id' | 'status' | 'updatedAt'>;
91
93
  export interface DehydratedRouter {
92
94
  state: DehydratedRouterState;
93
95
  }
@@ -1080,9 +1080,13 @@
1080
1080
  });
1081
1081
  }
1082
1082
 
1083
- const routerContext = /*#__PURE__*/React__namespace.createContext(null);
1083
+ exports.routerContext = /*#__PURE__*/React__namespace.createContext(null);
1084
1084
  if (typeof document !== 'undefined') {
1085
- window.__TSR_ROUTER_CONTEXT__ = routerContext;
1085
+ if (window.__TSR_ROUTER_CONTEXT__) {
1086
+ exports.routerContext = window.__TSR_ROUTER_CONTEXT__;
1087
+ } else {
1088
+ window.__TSR_ROUTER_CONTEXT__ = exports.routerContext;
1089
+ }
1086
1090
  }
1087
1091
  function RouterProvider({
1088
1092
  router,
@@ -1098,7 +1102,7 @@
1098
1102
  }
1099
1103
  });
1100
1104
  const matches = router.options.InnerWrap ? /*#__PURE__*/React__namespace.createElement(router.options.InnerWrap, null, /*#__PURE__*/React__namespace.createElement(Matches, null)) : /*#__PURE__*/React__namespace.createElement(Matches, null);
1101
- const provider = /*#__PURE__*/React__namespace.createElement(routerContext.Provider, {
1105
+ const provider = /*#__PURE__*/React__namespace.createElement(exports.routerContext.Provider, {
1102
1106
  value: router
1103
1107
  }, matches, /*#__PURE__*/React__namespace.createElement(Transitioner, null));
1104
1108
  if (router.options.Wrap) {
@@ -1192,14 +1196,14 @@
1192
1196
  return null;
1193
1197
  }
1194
1198
  function getRouteMatch(state, id) {
1195
- return [...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1199
+ return [...state.preloadMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1196
1200
  }
1197
1201
  function useRouterState(opts) {
1198
1202
  const router = useRouter();
1199
1203
  return useStore(router.__store, opts?.select);
1200
1204
  }
1201
1205
  function useRouter() {
1202
- const resolvedContext = typeof document !== 'undefined' ? window.__TSR_ROUTER_CONTEXT__ || routerContext : routerContext;
1206
+ const resolvedContext = typeof document !== 'undefined' ? window.__TSR_ROUTER_CONTEXT__ || exports.routerContext : exports.routerContext;
1203
1207
  const value = React__namespace.useContext(resolvedContext);
1204
1208
  warning(value, 'useRouter must be used inside a <RouterProvider> component!');
1205
1209
  return value;
@@ -2296,7 +2300,7 @@
2296
2300
  context: undefined,
2297
2301
  abortController: new AbortController(),
2298
2302
  shouldReloadDeps: undefined,
2299
- fetchedAt: 0,
2303
+ fetchCount: 0,
2300
2304
  cause
2301
2305
  };
2302
2306
 
@@ -2505,10 +2509,13 @@
2505
2509
  }) => {
2506
2510
  let latestPromise;
2507
2511
  let firstBadMatchIndex;
2508
- const updatePendingMatch = match => {
2512
+ const updateMatch = match => {
2513
+ const isPreload = this.state.preloadMatches.find(d => d.id === match.id);
2514
+ const isPending = this.state.pendingMatches?.find(d => d.id === match.id);
2515
+ const matchesKey = isPreload ? 'preloadMatches' : isPending ? 'pendingMatches' : 'matches';
2509
2516
  this.__store.setState(s => ({
2510
2517
  ...s,
2511
- pendingMatches: s.pendingMatches?.map(d => d.id === match.id ? match : d)
2518
+ [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
2512
2519
  }));
2513
2520
  };
2514
2521
 
@@ -2561,7 +2568,7 @@
2561
2568
  from: match.pathname
2562
2569
  }),
2563
2570
  buildLocation: this.buildLocation,
2564
- cause: match.cause
2571
+ cause: preload ? 'preload' : match.cause
2565
2572
  })) ?? {};
2566
2573
  if (isRedirect(beforeLoadContext)) {
2567
2574
  throw beforeLoadContext;
@@ -2591,7 +2598,7 @@
2591
2598
  const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
2592
2599
  const matchPromises = [];
2593
2600
  validResolvedMatches.forEach((match, index) => {
2594
- matchPromises.push((async () => {
2601
+ matchPromises.push(new Promise(async resolve => {
2595
2602
  const parentMatchPromise = matchPromises[index - 1];
2596
2603
  const route = this.looseRoutesById[match.routeId];
2597
2604
  const handleErrorAndRedirect = err => {
@@ -2606,92 +2613,95 @@
2606
2613
  let loadPromise;
2607
2614
  matches[index] = match = {
2608
2615
  ...match,
2609
- fetchedAt: Date.now(),
2610
2616
  showPending: false
2611
2617
  };
2618
+ let didShowPending = false;
2612
2619
  const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
2613
- let pendingPromise;
2614
- if (!preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent)) {
2615
- pendingPromise = new Promise(r => setTimeout(r, pendingMs));
2616
- }
2617
- if (match.isFetching) {
2618
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
2619
- } else {
2620
- const loaderContext = {
2621
- params: match.params,
2622
- search: match.search,
2623
- preload: !!preload,
2624
- parentMatchPromise,
2625
- abortController: match.abortController,
2626
- context: match.context,
2627
- location: this.state.location,
2628
- navigate: opts => this.navigate({
2629
- ...opts,
2630
- from: match.pathname
2631
- }),
2632
- cause: match.cause
2633
- };
2620
+ const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
2621
+ const shouldPending = !preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent);
2622
+ const fetch = async () => {
2623
+ if (match.isFetching) {
2624
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
2625
+ } else {
2626
+ const loaderContext = {
2627
+ params: match.params,
2628
+ search: match.search,
2629
+ preload: !!preload,
2630
+ parentMatchPromise,
2631
+ abortController: match.abortController,
2632
+ context: match.context,
2633
+ location: this.state.location,
2634
+ navigate: opts => this.navigate({
2635
+ ...opts,
2636
+ from: match.pathname
2637
+ }),
2638
+ cause: preload ? 'preload' : match.cause
2639
+ };
2634
2640
 
2635
- // Default to reloading the route all the time
2636
- let shouldReload = true;
2637
- let shouldReloadDeps = typeof route.options.shouldReload === 'function' ? route.options.shouldReload?.(loaderContext) : !!(route.options.shouldReload ?? true);
2638
- if (match.cause === 'enter' || invalidate) {
2639
- match.shouldReloadDeps = shouldReloadDeps;
2640
- } else if (match.cause === 'stay') {
2641
- if (typeof shouldReloadDeps === 'object') {
2642
- // compare the deps to see if they've changed
2643
- shouldReload = !deepEqual(shouldReloadDeps, match.shouldReloadDeps);
2644
- match.shouldReloadDeps = shouldReloadDeps;
2641
+ // Default to reloading the route all the time
2642
+ let shouldLoad = true;
2643
+ const shouldReloadFn = route.options.shouldReload;
2644
+ let shouldReloadDeps = typeof shouldReloadFn === 'function' ? shouldReloadFn(loaderContext) : !!(shouldReloadFn ?? true);
2645
+ const compareDeps = () => {
2646
+ if (typeof shouldReloadDeps === 'object') {
2647
+ // compare the deps to see if they've changed
2648
+ shouldLoad = !deepEqual(shouldReloadDeps, match.shouldReloadDeps);
2649
+ } else {
2650
+ shouldLoad = !!shouldReloadDeps;
2651
+ }
2652
+ };
2653
+
2654
+ // If it's the first preload, or the route is entering, or we're
2655
+ // invalidating, we definitely need to load the route
2656
+ if (invalidate) ; else if (preload) {
2657
+ if (!match.fetchCount) ; else {
2658
+ compareDeps();
2659
+ }
2660
+ } else if (match.cause === 'enter') {
2661
+ if (!match.fetchCount) ; else {
2662
+ compareDeps();
2663
+ }
2645
2664
  } else {
2646
- shouldReload = !!shouldReloadDeps;
2665
+ compareDeps();
2666
+ }
2667
+ if (typeof shouldReloadDeps === 'object') {
2668
+ matches[index] = match = {
2669
+ ...match,
2670
+ shouldReloadDeps
2671
+ };
2647
2672
  }
2648
- }
2649
2673
 
2650
- // If the user doesn't want the route to reload, just
2651
- // resolve with the existing loader data
2674
+ // If the user doesn't want the route to reload, just
2675
+ // resolve with the existing loader data
2652
2676
 
2653
- if (!shouldReload) {
2654
- loadPromise = Promise.resolve(match.loaderData);
2655
- } else {
2656
- // Otherwise, load the route
2657
- matches[index] = match = {
2658
- ...match,
2659
- isFetching: true
2660
- };
2661
- const componentsPromise = Promise.all(componentTypes.map(async type => {
2662
- const component = route.options[type];
2663
- if (component?.preload) {
2664
- await component.preload();
2677
+ if (!shouldLoad) {
2678
+ loadPromise = Promise.resolve(match.loaderData);
2679
+ } else {
2680
+ if (match.fetchCount && match.status === 'success') {
2681
+ resolve();
2665
2682
  }
2666
- }));
2667
- const loaderPromise = route.options.loader?.(loaderContext);
2668
- loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2669
- }
2670
- }
2671
- matches[index] = match = {
2672
- ...match,
2673
- loadPromise
2674
- };
2675
- if (!preload) {
2676
- updatePendingMatch(match);
2677
- }
2678
- let didShowPending = false;
2679
- const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
2680
- await new Promise(async resolve => {
2681
- // If the route has a pending component and a pendingMs option,
2682
- // forcefully show the pending component
2683
- if (pendingPromise) {
2684
- pendingPromise.then(() => {
2685
- if (latestPromise = checkLatest()) return;
2686
- didShowPending = true;
2683
+
2684
+ // Otherwise, load the route
2687
2685
  matches[index] = match = {
2688
2686
  ...match,
2689
- showPending: true
2687
+ isFetching: true,
2688
+ fetchCount: match.fetchCount + 1
2690
2689
  };
2691
- updatePendingMatch(match);
2692
- resolve();
2693
- });
2690
+ const componentsPromise = Promise.all(componentTypes.map(async type => {
2691
+ const component = route.options[type];
2692
+ if (component?.preload) {
2693
+ await component.preload();
2694
+ }
2695
+ }));
2696
+ const loaderPromise = route.options.loader?.(loaderContext);
2697
+ loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2698
+ }
2694
2699
  }
2700
+ matches[index] = match = {
2701
+ ...match,
2702
+ loadPromise
2703
+ };
2704
+ updateMatch(match);
2695
2705
  try {
2696
2706
  const loaderData = await loadPromise;
2697
2707
  if (latestPromise = checkLatest()) return await latestPromise;
@@ -2732,18 +2742,38 @@
2732
2742
  // we already moved the pendingMatches to the matches
2733
2743
  // state, so we need to update that specific match
2734
2744
  if (didShowPending && pendingMinMs && match.showPending) {
2735
- this.__store.setState(s => ({
2736
- ...s,
2737
- matches: s.matches?.map(d => d.id === match.id ? match : d)
2738
- }));
2745
+ updateMatch(match);
2739
2746
  }
2740
2747
  }
2741
- if (!preload) {
2742
- updatePendingMatch(match);
2748
+ updateMatch(match);
2749
+ };
2750
+ if (match.fetchCount && match.status === 'success') {
2751
+ // Background Fetching
2752
+ fetch();
2753
+ } else {
2754
+ // Critical Fetching
2755
+
2756
+ // If we need to potentially show the pending component,
2757
+ // start a timer to show it after the pendingMs
2758
+ if (shouldPending) {
2759
+ new Promise(r => setTimeout(r, pendingMs)).then(async () => {
2760
+ if (latestPromise = checkLatest()) return latestPromise;
2761
+ didShowPending = true;
2762
+ matches[index] = match = {
2763
+ ...match,
2764
+ showPending: true
2765
+ };
2766
+ updateMatch(match);
2767
+ resolve();
2768
+ });
2743
2769
  }
2744
- resolve();
2745
- });
2746
- })());
2770
+ await fetch();
2771
+ }
2772
+ resolve();
2773
+ // No Fetching
2774
+
2775
+ resolve();
2776
+ }));
2747
2777
  });
2748
2778
  await Promise.all(matchPromises);
2749
2779
  return matches;
@@ -2766,20 +2796,32 @@
2766
2796
  toLocation: next,
2767
2797
  pathChanged: pathDidChange
2768
2798
  });
2769
-
2770
- // Match the routes
2771
- let pendingMatches = this.matchRoutes(next.pathname, next.search, {
2772
- debug: true
2773
- });
2799
+ let pendingMatches;
2774
2800
  const previousMatches = this.state.matches;
2801
+ this.__store.batch(() => {
2802
+ this.__store.setState(s => ({
2803
+ ...s,
2804
+ preloadMatches: s.preloadMatches.filter(d => {
2805
+ return Date.now() - d.updatedAt < (this.options.defaultPreloadMaxAge ?? 3000);
2806
+ })
2807
+ }));
2775
2808
 
2776
- // Ingest the new matches
2777
- this.__store.setState(s => ({
2778
- ...s,
2779
- isLoading: true,
2780
- location: next,
2781
- pendingMatches
2782
- }));
2809
+ // Match the routes
2810
+ pendingMatches = this.matchRoutes(next.pathname, next.search, {
2811
+ debug: true
2812
+ });
2813
+
2814
+ // Ingest the new matches
2815
+ this.__store.setState(s => ({
2816
+ ...s,
2817
+ isLoading: true,
2818
+ location: next,
2819
+ pendingMatches,
2820
+ preloadMatches: s.preloadMatches.filter(d => {
2821
+ return !pendingMatches.find(e => e.id === d.id);
2822
+ })
2823
+ }));
2824
+ });
2783
2825
  try {
2784
2826
  try {
2785
2827
  // Load the matches
@@ -2837,6 +2879,17 @@
2837
2879
  let matches = this.matchRoutes(next.pathname, next.search, {
2838
2880
  throwOnError: true
2839
2881
  });
2882
+ const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.preloadMatches]?.map(d => [d.id, true]));
2883
+ this.__store.batch(() => {
2884
+ matches.forEach(match => {
2885
+ if (!loadedMatchIds[match.id]) {
2886
+ this.__store.setState(s => ({
2887
+ ...s,
2888
+ preloadMatches: [...s.preloadMatches, match]
2889
+ }));
2890
+ }
2891
+ });
2892
+ });
2840
2893
  matches = await this.loadMatches({
2841
2894
  matches,
2842
2895
  preload: true,
@@ -2899,7 +2952,7 @@
2899
2952
  dehydrate = () => {
2900
2953
  return {
2901
2954
  state: {
2902
- dehydratedMatches: this.state.matches.map(d => pick(d, ['fetchedAt', 'id', 'status', 'updatedAt', 'loaderData']))
2955
+ dehydratedMatches: this.state.matches.map(d => pick(d, ['id', 'status', 'updatedAt', 'loaderData']))
2903
2956
  }
2904
2957
  };
2905
2958
  };
@@ -2962,6 +3015,7 @@
2962
3015
  location,
2963
3016
  matches: [],
2964
3017
  pendingMatches: [],
3018
+ preloadMatches: [],
2965
3019
  lastUpdated: Date.now()
2966
3020
  };
2967
3021
  }
@@ -3255,7 +3309,6 @@
3255
3309
  exports.resolvePath = resolvePath;
3256
3310
  exports.rootRouteId = rootRouteId;
3257
3311
  exports.rootRouteWithContext = rootRouteWithContext;
3258
- exports.routerContext = routerContext;
3259
3312
  exports.shallow = shallow;
3260
3313
  exports.stringifySearchWith = stringifySearchWith;
3261
3314
  exports.trimPath = trimPath;