@tanstack/router-core 1.166.2 → 1.166.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.166.2",
3
+ "version": "1.166.6",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -33,6 +33,7 @@ type InnerLoadContext = {
33
33
  updateMatch: UpdateMatchFn
34
34
  matches: Array<AnyRouteMatch>
35
35
  preload?: boolean
36
+ forceStaleReload?: boolean
36
37
  onReady?: () => Promise<void>
37
38
  sync?: boolean
38
39
  }
@@ -166,7 +167,10 @@ const shouldSkipLoader = (
166
167
  inner: InnerLoadContext,
167
168
  matchId: string,
168
169
  ): boolean => {
169
- const match = inner.router.getMatch(matchId)!
170
+ const match = inner.router.getMatch(matchId)
171
+ if (!match) {
172
+ return true
173
+ }
170
174
  // upon hydration, we skip the loader if the match has been dehydrated on the server
171
175
  if (!(isServer ?? inner.router.isServer) && match._nonReactive.dehydrated) {
172
176
  return true
@@ -179,6 +183,21 @@ const shouldSkipLoader = (
179
183
  return false
180
184
  }
181
185
 
186
+ const syncMatchContext = (
187
+ inner: InnerLoadContext,
188
+ matchId: string,
189
+ index: number,
190
+ ): void => {
191
+ const nextContext = buildMatchContext(inner, index)
192
+
193
+ inner.updateMatch(matchId, (prev) => {
194
+ return {
195
+ ...prev,
196
+ context: nextContext,
197
+ }
198
+ })
199
+ }
200
+
182
201
  const handleSerialError = (
183
202
  inner: InnerLoadContext,
184
203
  index: number,
@@ -479,8 +498,6 @@ const executeBeforeLoad = (
479
498
 
480
499
  batch(() => {
481
500
  pending()
482
- // Only store __beforeLoadContext here, don't update context yet
483
- // Context will be updated in loadRouteMatch after loader completes
484
501
  inner.updateMatch(matchId, (prev) => ({
485
502
  ...prev,
486
503
  __beforeLoadContext: beforeLoadContext,
@@ -736,6 +753,10 @@ const runLoader = async (
736
753
  onErrorError,
737
754
  )
738
755
  }
756
+ if (!isRedirect(error) && !isNotFound(error)) {
757
+ await loadRouteChunk(route, ['errorComponent'])
758
+ }
759
+
739
760
  inner.updateMatch(matchId, (prev) => ({
740
761
  ...prev,
741
762
  error,
@@ -762,6 +783,7 @@ const loadRouteMatch = async (
762
783
  async function handleLoader(
763
784
  preload: boolean,
764
785
  prevMatch: AnyRouteMatch,
786
+ previousRouteMatchId: string | undefined,
765
787
  match: AnyRouteMatch,
766
788
  route: AnyRoute,
767
789
  ) {
@@ -787,8 +809,15 @@ const loadRouteMatch = async (
787
809
 
788
810
  // If the route is successful and still fresh, just resolve
789
811
  const { status, invalid } = match
812
+ const staleMatchShouldReload =
813
+ age > staleAge &&
814
+ (!!inner.forceStaleReload ||
815
+ match.cause === 'enter' ||
816
+ (previousRouteMatchId !== undefined &&
817
+ previousRouteMatchId !== match.id))
790
818
  loaderShouldRunAsync =
791
- status === 'success' && (invalid || (shouldReload ?? age > staleAge))
819
+ status === 'success' &&
820
+ (invalid || (shouldReload ?? staleMatchShouldReload))
792
821
  if (preload && route.options.preload === false) {
793
822
  // Do nothing
794
823
  } else if (loaderShouldRunAsync && !inner.sync) {
@@ -808,6 +837,8 @@ const loadRouteMatch = async (
808
837
  })()
809
838
  } else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
810
839
  await runLoader(inner, matchPromises, matchId, index, route)
840
+ } else {
841
+ syncMatchContext(inner, matchId, index)
811
842
  }
812
843
  }
813
844
 
@@ -817,11 +848,22 @@ const loadRouteMatch = async (
817
848
  const route = inner.router.looseRoutesById[routeId]!
818
849
 
819
850
  if (shouldSkipLoader(inner, matchId)) {
851
+ const match = inner.router.getMatch(matchId)
852
+ if (!match) {
853
+ return inner.matches[index]!
854
+ }
855
+
856
+ syncMatchContext(inner, matchId, index)
857
+
820
858
  if (isServer ?? inner.router.isServer) {
821
859
  return inner.router.getMatch(matchId)!
822
860
  }
823
861
  } else {
824
862
  const prevMatch = inner.router.getMatch(matchId)! // This is where all of the stale-while-revalidate magic happens
863
+ const previousRouteMatchId =
864
+ inner.router.state.matches[index]?.routeId === routeId
865
+ ? inner.router.state.matches[index]!.id
866
+ : inner.router.state.matches.find((d) => d.routeId === routeId)?.id
825
867
  const preload = resolvePreload(inner, matchId)
826
868
 
827
869
  // there is a loaderPromise, so we are in the middle of a load
@@ -840,7 +882,13 @@ const loadRouteMatch = async (
840
882
  }
841
883
 
842
884
  if (match.status === 'pending') {
843
- await handleLoader(preload, prevMatch, match, route)
885
+ await handleLoader(
886
+ preload,
887
+ prevMatch,
888
+ previousRouteMatchId,
889
+ match,
890
+ route,
891
+ )
844
892
  }
845
893
  } else {
846
894
  const nextPreload =
@@ -854,7 +902,7 @@ const loadRouteMatch = async (
854
902
  }))
855
903
  }
856
904
 
857
- await handleLoader(preload, prevMatch, match, route)
905
+ await handleLoader(preload, prevMatch, previousRouteMatchId, match, route)
858
906
  }
859
907
  }
860
908
  const match = inner.router.getMatch(matchId)!
@@ -886,6 +934,7 @@ export async function loadMatches(arg: {
886
934
  location: ParsedLocation
887
935
  matches: Array<AnyRouteMatch>
888
936
  preload?: boolean
937
+ forceStaleReload?: boolean
889
938
  onReady?: () => Promise<void>
890
939
  updateMatch: UpdateMatchFn
891
940
  sync?: boolean
@@ -1031,7 +1080,7 @@ export async function loadMatches(arg: {
1031
1080
 
1032
1081
  // Ensure the rendering boundary route chunk (and its lazy components, including
1033
1082
  // lazy notFoundComponent) is loaded before we continue to head execution/render.
1034
- await loadRouteChunk(boundaryRoute)
1083
+ await loadRouteChunk(boundaryRoute, ['notFoundComponent'])
1035
1084
  } else if (!inner.preload) {
1036
1085
  // Clear stale root global-not-found state on normal navigations that do not
1037
1086
  // throw notFound. This must live here (instead of only in runLoader success)
@@ -1061,7 +1110,7 @@ export async function loadMatches(arg: {
1061
1110
  inner.router.looseRoutesById[
1062
1111
  inner.matches[inner.firstBadMatchIndex]!.routeId
1063
1112
  ]!
1064
- await loadRouteChunk(errorRoute)
1113
+ await loadRouteChunk(errorRoute, ['errorComponent'])
1065
1114
  }
1066
1115
 
1067
1116
  // serially execute heads once after loaders/notFound handling, ensuring
@@ -1100,7 +1149,29 @@ export async function loadMatches(arg: {
1100
1149
  return inner.matches
1101
1150
  }
1102
1151
 
1103
- export async function loadRouteChunk(route: AnyRoute) {
1152
+ export type RouteComponentType =
1153
+ | 'component'
1154
+ | 'errorComponent'
1155
+ | 'pendingComponent'
1156
+ | 'notFoundComponent'
1157
+
1158
+ function preloadRouteComponents(
1159
+ route: AnyRoute,
1160
+ componentTypesToLoad: Array<RouteComponentType>,
1161
+ ): Promise<void> | undefined {
1162
+ const preloads = componentTypesToLoad
1163
+ .map((type) => (route.options[type] as any)?.preload?.())
1164
+ .filter(Boolean)
1165
+
1166
+ if (preloads.length === 0) return undefined
1167
+
1168
+ return Promise.all(preloads) as any as Promise<void>
1169
+ }
1170
+
1171
+ export function loadRouteChunk(
1172
+ route: AnyRoute,
1173
+ componentTypesToLoad: Array<RouteComponentType> = componentTypes,
1174
+ ) {
1104
1175
  if (!route._lazyLoaded && route._lazyPromise === undefined) {
1105
1176
  if (route.lazyFn) {
1106
1177
  route._lazyPromise = route.lazyFn().then((lazyRoute) => {
@@ -1115,30 +1186,34 @@ export async function loadRouteChunk(route: AnyRoute) {
1115
1186
  }
1116
1187
  }
1117
1188
 
1118
- // If for some reason lazy resolves more lazy components...
1119
- // We'll wait for that before we attempt to preload the
1120
- // components themselves.
1121
- if (!route._componentsLoaded && route._componentsPromise === undefined) {
1122
- const loadComponents = () => {
1123
- const preloads = []
1124
- for (const type of componentTypes) {
1125
- const preload = (route.options[type] as any)?.preload
1126
- if (preload) preloads.push(preload())
1127
- }
1128
- if (preloads.length)
1129
- return Promise.all(preloads).then(() => {
1130
- route._componentsLoaded = true
1131
- route._componentsPromise = undefined // gc promise, we won't need it anymore
1132
- })
1133
- route._componentsLoaded = true
1134
- route._componentsPromise = undefined // gc promise, we won't need it anymore
1135
- return
1136
- }
1137
- route._componentsPromise = route._lazyPromise
1138
- ? route._lazyPromise.then(loadComponents)
1139
- : loadComponents()
1140
- }
1141
- return route._componentsPromise
1189
+ const runAfterLazy = () =>
1190
+ route._componentsLoaded
1191
+ ? undefined
1192
+ : componentTypesToLoad === componentTypes
1193
+ ? (() => {
1194
+ if (route._componentsPromise === undefined) {
1195
+ const componentsPromise = preloadRouteComponents(
1196
+ route,
1197
+ componentTypes,
1198
+ )
1199
+
1200
+ if (componentsPromise) {
1201
+ route._componentsPromise = componentsPromise.then(() => {
1202
+ route._componentsLoaded = true
1203
+ route._componentsPromise = undefined // gc promise, we won't need it anymore
1204
+ })
1205
+ } else {
1206
+ route._componentsLoaded = true
1207
+ }
1208
+ }
1209
+
1210
+ return route._componentsPromise
1211
+ })()
1212
+ : preloadRouteComponents(route, componentTypesToLoad)
1213
+
1214
+ return route._lazyPromise
1215
+ ? route._lazyPromise.then(runAfterLazy)
1216
+ : runAfterLazy()
1142
1217
  }
1143
1218
 
1144
1219
  function makeMaybe<TValue, TError>(
@@ -1160,7 +1235,7 @@ export function routeNeedsPreload(route: AnyRoute) {
1160
1235
  return false
1161
1236
  }
1162
1237
 
1163
- export const componentTypes = [
1238
+ export const componentTypes: Array<RouteComponentType> = [
1164
1239
  'component',
1165
1240
  'errorComponent',
1166
1241
  'pendingComponent',
package/src/router.ts CHANGED
@@ -2364,6 +2364,7 @@ export class RouterCore<
2364
2364
  let redirect: AnyRedirect | undefined
2365
2365
  let notFound: NotFoundError | undefined
2366
2366
  let loadPromise: Promise<void>
2367
+ const previousLocation = this.state.resolvedLocation ?? this.state.location
2367
2368
 
2368
2369
  // eslint-disable-next-line prefer-const
2369
2370
  loadPromise = new Promise<void>((resolve) => {
@@ -2394,6 +2395,7 @@ export class RouterCore<
2394
2395
  await loadMatches({
2395
2396
  router: this,
2396
2397
  sync: opts?.sync,
2398
+ forceStaleReload: previousLocation.href === next.href,
2397
2399
  matches: this.state.pendingMatches as Array<AnyRouteMatch>,
2398
2400
  location: next,
2399
2401
  updateMatch: this.updateMatch,