@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.
@@ -472,9 +472,13 @@ function useLoaderData(opts) {
472
472
  });
473
473
  }
474
474
 
475
- const routerContext = /*#__PURE__*/React.createContext(null);
475
+ let routerContext = /*#__PURE__*/React.createContext(null);
476
476
  if (typeof document !== 'undefined') {
477
- window.__TSR_ROUTER_CONTEXT__ = routerContext;
477
+ if (window.__TSR_ROUTER_CONTEXT__) {
478
+ routerContext = window.__TSR_ROUTER_CONTEXT__;
479
+ } else {
480
+ window.__TSR_ROUTER_CONTEXT__ = routerContext;
481
+ }
478
482
  }
479
483
  function RouterProvider({
480
484
  router,
@@ -584,7 +588,7 @@ function Transitioner() {
584
588
  return null;
585
589
  }
586
590
  function getRouteMatch(state, id) {
587
- return [...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
591
+ return [...state.preloadMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
588
592
  }
589
593
  function useRouterState(opts) {
590
594
  const router = useRouter();
@@ -1653,11 +1657,18 @@ class Router {
1653
1657
  return [parentSearch, searchError];
1654
1658
  }
1655
1659
  })();
1660
+
1661
+ // This is where we need to call route.options.loaderDeps() to get any additional
1662
+ // deps that the route's loader function might need to run. We need to do this
1663
+ // before we create the match so that we can pass the deps to the route's
1664
+ // potential key function which is used to uniquely identify the route match in state
1665
+
1666
+ const loaderDeps = route.options.loaderDeps?.({
1667
+ search: preMatchSearch
1668
+ }) ?? '';
1669
+ const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : '';
1656
1670
  const interpolatedPath = interpolatePath(route.fullPath, routeParams);
1657
- const matchId = interpolatePath(route.id, routeParams, true) + (route.options.key?.({
1658
- search: preMatchSearch,
1659
- location: this.state.location
1660
- }) ?? '');
1671
+ const matchId = interpolatePath(route.id, routeParams, true) + loaderDepsHash;
1661
1672
 
1662
1673
  // Waste not, want not. If we already have a match for this route,
1663
1674
  // reuse it. This is important for layout routes, which might stick
@@ -1688,8 +1699,9 @@ class Router {
1688
1699
  context: undefined,
1689
1700
  abortController: new AbortController(),
1690
1701
  shouldReloadDeps: undefined,
1691
- fetchedAt: 0,
1692
- cause
1702
+ fetchCount: 0,
1703
+ cause,
1704
+ loaderDeps
1693
1705
  };
1694
1706
 
1695
1707
  // Regardless of whether we're reusing an existing match or creating
@@ -1897,10 +1909,13 @@ class Router {
1897
1909
  }) => {
1898
1910
  let latestPromise;
1899
1911
  let firstBadMatchIndex;
1900
- const updatePendingMatch = match => {
1912
+ const updateMatch = match => {
1913
+ const isPreload = this.state.preloadMatches.find(d => d.id === match.id);
1914
+ const isPending = this.state.pendingMatches?.find(d => d.id === match.id);
1915
+ const matchesKey = isPreload ? 'preloadMatches' : isPending ? 'pendingMatches' : 'matches';
1901
1916
  this.__store.setState(s => ({
1902
1917
  ...s,
1903
- pendingMatches: s.pendingMatches?.map(d => d.id === match.id ? match : d)
1918
+ [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
1904
1919
  }));
1905
1920
  };
1906
1921
 
@@ -1953,7 +1968,7 @@ class Router {
1953
1968
  from: match.pathname
1954
1969
  }),
1955
1970
  buildLocation: this.buildLocation,
1956
- cause: match.cause
1971
+ cause: preload ? 'preload' : match.cause
1957
1972
  })) ?? {};
1958
1973
  if (isRedirect(beforeLoadContext)) {
1959
1974
  throw beforeLoadContext;
@@ -1983,7 +1998,7 @@ class Router {
1983
1998
  const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
1984
1999
  const matchPromises = [];
1985
2000
  validResolvedMatches.forEach((match, index) => {
1986
- matchPromises.push((async () => {
2001
+ matchPromises.push(new Promise(async resolve => {
1987
2002
  const parentMatchPromise = matchPromises[index - 1];
1988
2003
  const route = this.looseRoutesById[match.routeId];
1989
2004
  const handleErrorAndRedirect = err => {
@@ -1998,92 +2013,95 @@ class Router {
1998
2013
  let loadPromise;
1999
2014
  matches[index] = match = {
2000
2015
  ...match,
2001
- fetchedAt: Date.now(),
2002
2016
  showPending: false
2003
2017
  };
2018
+ let didShowPending = false;
2004
2019
  const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
2005
- let pendingPromise;
2006
- if (!preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent)) {
2007
- pendingPromise = new Promise(r => setTimeout(r, pendingMs));
2008
- }
2009
- if (match.isFetching) {
2010
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
2011
- } else {
2012
- const loaderContext = {
2013
- params: match.params,
2014
- search: match.search,
2015
- preload: !!preload,
2016
- parentMatchPromise,
2017
- abortController: match.abortController,
2018
- context: match.context,
2019
- location: this.state.location,
2020
- navigate: opts => this.navigate({
2021
- ...opts,
2022
- from: match.pathname
2023
- }),
2024
- cause: match.cause
2025
- };
2020
+ const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
2021
+ const shouldPending = !preload && pendingMs && (route.options.pendingComponent ?? this.options.defaultPendingComponent);
2022
+ const fetch = async () => {
2023
+ if (match.isFetching) {
2024
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
2025
+ } else {
2026
+ const loaderContext = {
2027
+ params: match.params,
2028
+ deps: match.loaderDeps,
2029
+ preload: !!preload,
2030
+ parentMatchPromise,
2031
+ abortController: match.abortController,
2032
+ context: match.context,
2033
+ location: this.state.location,
2034
+ navigate: opts => this.navigate({
2035
+ ...opts,
2036
+ from: match.pathname
2037
+ }),
2038
+ cause: preload ? 'preload' : match.cause
2039
+ };
2026
2040
 
2027
- // Default to reloading the route all the time
2028
- let shouldReload = true;
2029
- let shouldReloadDeps = typeof route.options.shouldReload === 'function' ? route.options.shouldReload?.(loaderContext) : !!(route.options.shouldReload ?? true);
2030
- if (match.cause === 'enter' || invalidate) {
2031
- match.shouldReloadDeps = shouldReloadDeps;
2032
- } else if (match.cause === 'stay') {
2033
- if (typeof shouldReloadDeps === 'object') {
2034
- // compare the deps to see if they've changed
2035
- shouldReload = !deepEqual(shouldReloadDeps, match.shouldReloadDeps);
2036
- match.shouldReloadDeps = shouldReloadDeps;
2041
+ // Default to reloading the route all the time
2042
+ let shouldLoad = true;
2043
+ const shouldReloadFn = route.options.shouldReload;
2044
+ let shouldReloadDeps = typeof shouldReloadFn === 'function' ? shouldReloadFn(loaderContext) : !!(shouldReloadFn ?? true);
2045
+ const compareDeps = () => {
2046
+ if (typeof shouldReloadDeps === 'object') {
2047
+ // compare the deps to see if they've changed
2048
+ shouldLoad = !deepEqual(shouldReloadDeps, match.shouldReloadDeps);
2049
+ } else {
2050
+ shouldLoad = !!shouldReloadDeps;
2051
+ }
2052
+ };
2053
+
2054
+ // If it's the first preload, or the route is entering, or we're
2055
+ // invalidating, we definitely need to load the route
2056
+ if (invalidate) ; else if (preload) {
2057
+ if (!match.fetchCount) ; else {
2058
+ compareDeps();
2059
+ }
2060
+ } else if (match.cause === 'enter') {
2061
+ if (!match.fetchCount) ; else {
2062
+ compareDeps();
2063
+ }
2037
2064
  } else {
2038
- shouldReload = !!shouldReloadDeps;
2065
+ compareDeps();
2066
+ }
2067
+ if (typeof shouldReloadDeps === 'object') {
2068
+ matches[index] = match = {
2069
+ ...match,
2070
+ shouldReloadDeps
2071
+ };
2039
2072
  }
2040
- }
2041
2073
 
2042
- // If the user doesn't want the route to reload, just
2043
- // resolve with the existing loader data
2074
+ // If the user doesn't want the route to reload, just
2075
+ // resolve with the existing loader data
2044
2076
 
2045
- if (!shouldReload) {
2046
- loadPromise = Promise.resolve(match.loaderData);
2047
- } else {
2048
- // Otherwise, load the route
2049
- matches[index] = match = {
2050
- ...match,
2051
- isFetching: true
2052
- };
2053
- const componentsPromise = Promise.all(componentTypes.map(async type => {
2054
- const component = route.options[type];
2055
- if (component?.preload) {
2056
- await component.preload();
2077
+ if (!shouldLoad) {
2078
+ loadPromise = Promise.resolve(match.loaderData);
2079
+ } else {
2080
+ if (match.fetchCount && match.status === 'success') {
2081
+ resolve();
2057
2082
  }
2058
- }));
2059
- const loaderPromise = route.options.loader?.(loaderContext);
2060
- loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2061
- }
2062
- }
2063
- matches[index] = match = {
2064
- ...match,
2065
- loadPromise
2066
- };
2067
- if (!preload) {
2068
- updatePendingMatch(match);
2069
- }
2070
- let didShowPending = false;
2071
- const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
2072
- await new Promise(async resolve => {
2073
- // If the route has a pending component and a pendingMs option,
2074
- // forcefully show the pending component
2075
- if (pendingPromise) {
2076
- pendingPromise.then(() => {
2077
- if (latestPromise = checkLatest()) return;
2078
- didShowPending = true;
2083
+
2084
+ // Otherwise, load the route
2079
2085
  matches[index] = match = {
2080
2086
  ...match,
2081
- showPending: true
2087
+ isFetching: true,
2088
+ fetchCount: match.fetchCount + 1
2082
2089
  };
2083
- updatePendingMatch(match);
2084
- resolve();
2085
- });
2090
+ const componentsPromise = Promise.all(componentTypes.map(async type => {
2091
+ const component = route.options[type];
2092
+ if (component?.preload) {
2093
+ await component.preload();
2094
+ }
2095
+ }));
2096
+ const loaderPromise = route.options.loader?.(loaderContext);
2097
+ loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2098
+ }
2086
2099
  }
2100
+ matches[index] = match = {
2101
+ ...match,
2102
+ loadPromise
2103
+ };
2104
+ updateMatch(match);
2087
2105
  try {
2088
2106
  const loaderData = await loadPromise;
2089
2107
  if (latestPromise = checkLatest()) return await latestPromise;
@@ -2124,18 +2142,38 @@ class Router {
2124
2142
  // we already moved the pendingMatches to the matches
2125
2143
  // state, so we need to update that specific match
2126
2144
  if (didShowPending && pendingMinMs && match.showPending) {
2127
- this.__store.setState(s => ({
2128
- ...s,
2129
- matches: s.matches?.map(d => d.id === match.id ? match : d)
2130
- }));
2145
+ updateMatch(match);
2131
2146
  }
2132
2147
  }
2133
- if (!preload) {
2134
- updatePendingMatch(match);
2148
+ updateMatch(match);
2149
+ };
2150
+ if (match.fetchCount && match.status === 'success') {
2151
+ // Background Fetching
2152
+ fetch();
2153
+ } else {
2154
+ // Critical Fetching
2155
+
2156
+ // If we need to potentially show the pending component,
2157
+ // start a timer to show it after the pendingMs
2158
+ if (shouldPending) {
2159
+ new Promise(r => setTimeout(r, pendingMs)).then(async () => {
2160
+ if (latestPromise = checkLatest()) return latestPromise;
2161
+ didShowPending = true;
2162
+ matches[index] = match = {
2163
+ ...match,
2164
+ showPending: true
2165
+ };
2166
+ updateMatch(match);
2167
+ resolve();
2168
+ });
2135
2169
  }
2136
- resolve();
2137
- });
2138
- })());
2170
+ await fetch();
2171
+ }
2172
+ resolve();
2173
+ // No Fetching
2174
+
2175
+ resolve();
2176
+ }));
2139
2177
  });
2140
2178
  await Promise.all(matchPromises);
2141
2179
  return matches;
@@ -2158,20 +2196,32 @@ class Router {
2158
2196
  toLocation: next,
2159
2197
  pathChanged: pathDidChange
2160
2198
  });
2161
-
2162
- // Match the routes
2163
- let pendingMatches = this.matchRoutes(next.pathname, next.search, {
2164
- debug: true
2165
- });
2199
+ let pendingMatches;
2166
2200
  const previousMatches = this.state.matches;
2201
+ this.__store.batch(() => {
2202
+ this.__store.setState(s => ({
2203
+ ...s,
2204
+ preloadMatches: s.preloadMatches.filter(d => {
2205
+ return Date.now() - d.updatedAt < (this.options.defaultPreloadMaxAge ?? 3000);
2206
+ })
2207
+ }));
2167
2208
 
2168
- // Ingest the new matches
2169
- this.__store.setState(s => ({
2170
- ...s,
2171
- isLoading: true,
2172
- location: next,
2173
- pendingMatches
2174
- }));
2209
+ // Match the routes
2210
+ pendingMatches = this.matchRoutes(next.pathname, next.search, {
2211
+ debug: true
2212
+ });
2213
+
2214
+ // Ingest the new matches
2215
+ this.__store.setState(s => ({
2216
+ ...s,
2217
+ isLoading: true,
2218
+ location: next,
2219
+ pendingMatches,
2220
+ preloadMatches: s.preloadMatches.filter(d => {
2221
+ return !pendingMatches.find(e => e.id === d.id);
2222
+ })
2223
+ }));
2224
+ });
2175
2225
  try {
2176
2226
  try {
2177
2227
  // Load the matches
@@ -2229,6 +2279,17 @@ class Router {
2229
2279
  let matches = this.matchRoutes(next.pathname, next.search, {
2230
2280
  throwOnError: true
2231
2281
  });
2282
+ const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.preloadMatches]?.map(d => [d.id, true]));
2283
+ this.__store.batch(() => {
2284
+ matches.forEach(match => {
2285
+ if (!loadedMatchIds[match.id]) {
2286
+ this.__store.setState(s => ({
2287
+ ...s,
2288
+ preloadMatches: [...s.preloadMatches, match]
2289
+ }));
2290
+ }
2291
+ });
2292
+ });
2232
2293
  matches = await this.loadMatches({
2233
2294
  matches,
2234
2295
  preload: true,
@@ -2291,7 +2352,7 @@ class Router {
2291
2352
  dehydrate = () => {
2292
2353
  return {
2293
2354
  state: {
2294
- dehydratedMatches: this.state.matches.map(d => pick(d, ['fetchedAt', 'id', 'status', 'updatedAt', 'loaderData']))
2355
+ dehydratedMatches: this.state.matches.map(d => pick(d, ['id', 'status', 'updatedAt', 'loaderData']))
2295
2356
  }
2296
2357
  };
2297
2358
  };
@@ -2354,6 +2415,7 @@ function getInitialRouterState(location) {
2354
2415
  location,
2355
2416
  matches: [],
2356
2417
  pendingMatches: [],
2418
+ preloadMatches: [],
2357
2419
  lastUpdated: Date.now()
2358
2420
  };
2359
2421
  }