@tanstack/react-router 0.0.1-beta.272 → 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) {
@@ -1169,9 +1173,11 @@
1169
1173
  pathChanged: routerState.location.href !== routerState.resolvedLocation?.href
1170
1174
  });
1171
1175
  if (document.querySelector) {
1172
- const el = document.getElementById(routerState.location.hash);
1173
- if (el) {
1174
- el.scrollIntoView();
1176
+ if (routerState.location.hash !== '') {
1177
+ const el = document.getElementById(routerState.location.hash);
1178
+ if (el) {
1179
+ el.scrollIntoView();
1180
+ }
1175
1181
  }
1176
1182
  }
1177
1183
  router.pendingMatches = [];
@@ -1190,14 +1196,14 @@
1190
1196
  return null;
1191
1197
  }
1192
1198
  function getRouteMatch(state, id) {
1193
- return [...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1199
+ return [...state.preloadMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1194
1200
  }
1195
1201
  function useRouterState(opts) {
1196
1202
  const router = useRouter();
1197
1203
  return useStore(router.__store, opts?.select);
1198
1204
  }
1199
1205
  function useRouter() {
1200
- 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;
1201
1207
  const value = React__namespace.useContext(resolvedContext);
1202
1208
  warning(value, 'useRouter must be used inside a <RouterProvider> component!');
1203
1209
  return value;
@@ -2294,7 +2300,7 @@
2294
2300
  context: undefined,
2295
2301
  abortController: new AbortController(),
2296
2302
  shouldReloadDeps: undefined,
2297
- fetchedAt: 0,
2303
+ fetchCount: 0,
2298
2304
  cause
2299
2305
  };
2300
2306
 
@@ -2503,10 +2509,13 @@
2503
2509
  }) => {
2504
2510
  let latestPromise;
2505
2511
  let firstBadMatchIndex;
2506
- 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';
2507
2516
  this.__store.setState(s => ({
2508
2517
  ...s,
2509
- pendingMatches: s.pendingMatches?.map(d => d.id === match.id ? match : d)
2518
+ [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
2510
2519
  }));
2511
2520
  };
2512
2521
 
@@ -2559,7 +2568,7 @@
2559
2568
  from: match.pathname
2560
2569
  }),
2561
2570
  buildLocation: this.buildLocation,
2562
- cause: match.cause
2571
+ cause: preload ? 'preload' : match.cause
2563
2572
  })) ?? {};
2564
2573
  if (isRedirect(beforeLoadContext)) {
2565
2574
  throw beforeLoadContext;
@@ -2589,7 +2598,7 @@
2589
2598
  const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
2590
2599
  const matchPromises = [];
2591
2600
  validResolvedMatches.forEach((match, index) => {
2592
- matchPromises.push((async () => {
2601
+ matchPromises.push(new Promise(async resolve => {
2593
2602
  const parentMatchPromise = matchPromises[index - 1];
2594
2603
  const route = this.looseRoutesById[match.routeId];
2595
2604
  const handleErrorAndRedirect = err => {
@@ -2604,92 +2613,95 @@
2604
2613
  let loadPromise;
2605
2614
  matches[index] = match = {
2606
2615
  ...match,
2607
- fetchedAt: Date.now(),
2608
2616
  showPending: false
2609
2617
  };
2618
+ let didShowPending = false;
2610
2619
  const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
2611
- let pendingPromise;
2612
- if (!preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent)) {
2613
- pendingPromise = new Promise(r => setTimeout(r, pendingMs));
2614
- }
2615
- if (match.isFetching) {
2616
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
2617
- } else {
2618
- const loaderContext = {
2619
- params: match.params,
2620
- search: match.search,
2621
- preload: !!preload,
2622
- parentMatchPromise,
2623
- abortController: match.abortController,
2624
- context: match.context,
2625
- location: this.state.location,
2626
- navigate: opts => this.navigate({
2627
- ...opts,
2628
- from: match.pathname
2629
- }),
2630
- cause: match.cause
2631
- };
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
+ };
2632
2640
 
2633
- // Default to reloading the route all the time
2634
- let shouldReload = true;
2635
- let shouldReloadDeps = typeof route.options.shouldReload === 'function' ? route.options.shouldReload?.(loaderContext) : !!(route.options.shouldReload ?? true);
2636
- if (match.cause === 'enter' || invalidate) {
2637
- match.shouldReloadDeps = shouldReloadDeps;
2638
- } else if (match.cause === 'stay') {
2639
- if (typeof shouldReloadDeps === 'object') {
2640
- // compare the deps to see if they've changed
2641
- shouldReload = !deepEqual(shouldReloadDeps, match.shouldReloadDeps);
2642
- 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
+ }
2643
2664
  } else {
2644
- shouldReload = !!shouldReloadDeps;
2665
+ compareDeps();
2666
+ }
2667
+ if (typeof shouldReloadDeps === 'object') {
2668
+ matches[index] = match = {
2669
+ ...match,
2670
+ shouldReloadDeps
2671
+ };
2645
2672
  }
2646
- }
2647
2673
 
2648
- // If the user doesn't want the route to reload, just
2649
- // 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
2650
2676
 
2651
- if (!shouldReload) {
2652
- loadPromise = Promise.resolve(match.loaderData);
2653
- } else {
2654
- // Otherwise, load the route
2655
- matches[index] = match = {
2656
- ...match,
2657
- isFetching: true
2658
- };
2659
- const componentsPromise = Promise.all(componentTypes.map(async type => {
2660
- const component = route.options[type];
2661
- if (component?.preload) {
2662
- await component.preload();
2677
+ if (!shouldLoad) {
2678
+ loadPromise = Promise.resolve(match.loaderData);
2679
+ } else {
2680
+ if (match.fetchCount && match.status === 'success') {
2681
+ resolve();
2663
2682
  }
2664
- }));
2665
- const loaderPromise = route.options.loader?.(loaderContext);
2666
- loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2667
- }
2668
- }
2669
- matches[index] = match = {
2670
- ...match,
2671
- loadPromise
2672
- };
2673
- if (!preload) {
2674
- updatePendingMatch(match);
2675
- }
2676
- let didShowPending = false;
2677
- const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
2678
- await new Promise(async resolve => {
2679
- // If the route has a pending component and a pendingMs option,
2680
- // forcefully show the pending component
2681
- if (pendingPromise) {
2682
- pendingPromise.then(() => {
2683
- if (latestPromise = checkLatest()) return;
2684
- didShowPending = true;
2683
+
2684
+ // Otherwise, load the route
2685
2685
  matches[index] = match = {
2686
2686
  ...match,
2687
- showPending: true
2687
+ isFetching: true,
2688
+ fetchCount: match.fetchCount + 1
2688
2689
  };
2689
- updatePendingMatch(match);
2690
- resolve();
2691
- });
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
+ }
2692
2699
  }
2700
+ matches[index] = match = {
2701
+ ...match,
2702
+ loadPromise
2703
+ };
2704
+ updateMatch(match);
2693
2705
  try {
2694
2706
  const loaderData = await loadPromise;
2695
2707
  if (latestPromise = checkLatest()) return await latestPromise;
@@ -2730,18 +2742,38 @@
2730
2742
  // we already moved the pendingMatches to the matches
2731
2743
  // state, so we need to update that specific match
2732
2744
  if (didShowPending && pendingMinMs && match.showPending) {
2733
- this.__store.setState(s => ({
2734
- ...s,
2735
- matches: s.matches?.map(d => d.id === match.id ? match : d)
2736
- }));
2745
+ updateMatch(match);
2737
2746
  }
2738
2747
  }
2739
- if (!preload) {
2740
- 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
+ });
2741
2769
  }
2742
- resolve();
2743
- });
2744
- })());
2770
+ await fetch();
2771
+ }
2772
+ resolve();
2773
+ // No Fetching
2774
+
2775
+ resolve();
2776
+ }));
2745
2777
  });
2746
2778
  await Promise.all(matchPromises);
2747
2779
  return matches;
@@ -2764,20 +2796,32 @@
2764
2796
  toLocation: next,
2765
2797
  pathChanged: pathDidChange
2766
2798
  });
2767
-
2768
- // Match the routes
2769
- let pendingMatches = this.matchRoutes(next.pathname, next.search, {
2770
- debug: true
2771
- });
2799
+ let pendingMatches;
2772
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
+ }));
2773
2808
 
2774
- // Ingest the new matches
2775
- this.__store.setState(s => ({
2776
- ...s,
2777
- isLoading: true,
2778
- location: next,
2779
- pendingMatches
2780
- }));
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
+ });
2781
2825
  try {
2782
2826
  try {
2783
2827
  // Load the matches
@@ -2835,6 +2879,17 @@
2835
2879
  let matches = this.matchRoutes(next.pathname, next.search, {
2836
2880
  throwOnError: true
2837
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
+ });
2838
2893
  matches = await this.loadMatches({
2839
2894
  matches,
2840
2895
  preload: true,
@@ -2897,7 +2952,7 @@
2897
2952
  dehydrate = () => {
2898
2953
  return {
2899
2954
  state: {
2900
- 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']))
2901
2956
  }
2902
2957
  };
2903
2958
  };
@@ -2960,6 +3015,7 @@
2960
3015
  location,
2961
3016
  matches: [],
2962
3017
  pendingMatches: [],
3018
+ preloadMatches: [],
2963
3019
  lastUpdated: Date.now()
2964
3020
  };
2965
3021
  }
@@ -3253,7 +3309,6 @@
3253
3309
  exports.resolvePath = resolvePath;
3254
3310
  exports.rootRouteId = rootRouteId;
3255
3311
  exports.rootRouteWithContext = rootRouteWithContext;
3256
- exports.routerContext = routerContext;
3257
3312
  exports.shallow = shallow;
3258
3313
  exports.stringifySearchWith = stringifySearchWith;
3259
3314
  exports.trimPath = trimPath;