@tanstack/react-router 1.18.2 → 1.18.4

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.
Files changed (52) hide show
  1. package/dist/cjs/Matches.cjs +75 -24
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/Matches.d.cts +2 -3
  4. package/dist/cjs/RouterProvider.cjs +1 -10
  5. package/dist/cjs/RouterProvider.cjs.map +1 -1
  6. package/dist/cjs/awaited.cjs +2 -4
  7. package/dist/cjs/awaited.cjs.map +1 -1
  8. package/dist/cjs/fileRoute.cjs +6 -1
  9. package/dist/cjs/fileRoute.cjs.map +1 -1
  10. package/dist/cjs/fileRoute.d.cts +7 -6
  11. package/dist/cjs/link.cjs.map +1 -1
  12. package/dist/cjs/not-found.cjs.map +1 -1
  13. package/dist/cjs/not-found.d.cts +11 -1
  14. package/dist/cjs/redirects.cjs +1 -1
  15. package/dist/cjs/redirects.cjs.map +1 -1
  16. package/dist/cjs/route.cjs +3 -2
  17. package/dist/cjs/route.cjs.map +1 -1
  18. package/dist/cjs/route.d.cts +25 -24
  19. package/dist/cjs/router.cjs +172 -143
  20. package/dist/cjs/router.cjs.map +1 -1
  21. package/dist/cjs/router.d.cts +12 -8
  22. package/dist/esm/Matches.d.ts +2 -3
  23. package/dist/esm/Matches.js +67 -16
  24. package/dist/esm/Matches.js.map +1 -1
  25. package/dist/esm/RouterProvider.js +1 -10
  26. package/dist/esm/RouterProvider.js.map +1 -1
  27. package/dist/esm/awaited.js +2 -4
  28. package/dist/esm/awaited.js.map +1 -1
  29. package/dist/esm/fileRoute.d.ts +7 -6
  30. package/dist/esm/fileRoute.js +6 -1
  31. package/dist/esm/fileRoute.js.map +1 -1
  32. package/dist/esm/link.js.map +1 -1
  33. package/dist/esm/not-found.d.ts +11 -1
  34. package/dist/esm/not-found.js.map +1 -1
  35. package/dist/esm/redirects.js +1 -1
  36. package/dist/esm/redirects.js.map +1 -1
  37. package/dist/esm/route.d.ts +25 -24
  38. package/dist/esm/route.js +3 -2
  39. package/dist/esm/route.js.map +1 -1
  40. package/dist/esm/router.d.ts +12 -8
  41. package/dist/esm/router.js +163 -134
  42. package/dist/esm/router.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/Matches.tsx +100 -27
  45. package/src/RouterProvider.tsx +1 -12
  46. package/src/awaited.tsx +3 -5
  47. package/src/fileRoute.ts +16 -4
  48. package/src/link.tsx +0 -2
  49. package/src/not-found.tsx +11 -1
  50. package/src/redirects.ts +2 -1
  51. package/src/route.ts +55 -114
  52. package/src/router.ts +267 -175
package/src/router.ts CHANGED
@@ -14,11 +14,11 @@ import {
14
14
  AnySearchSchema,
15
15
  AnyRoute,
16
16
  AnyContext,
17
- AnyPathParams,
18
17
  RouteMask,
19
18
  Route,
20
19
  LoaderFnContext,
21
20
  rootRouteId,
21
+ NotFoundRouteComponent,
22
22
  } from './route'
23
23
  import {
24
24
  FullSearchSchema,
@@ -67,10 +67,10 @@ import {
67
67
  import invariant from 'tiny-invariant'
68
68
  import { AnyRedirect, isRedirect } from './redirects'
69
69
  import { NotFoundError, isNotFound } from './not-found'
70
- import { ResolveRelativePath, ToOptions } from './link'
70
+ import { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
71
71
  import { NoInfer } from '@tanstack/react-store'
72
72
  import warning from 'tiny-warning'
73
- import { DeferredPromiseState } from '.'
73
+ import { DeferredPromiseState } from './defer'
74
74
 
75
75
  //
76
76
 
@@ -85,7 +85,7 @@ export interface Register {
85
85
  // router: Router
86
86
  }
87
87
 
88
- export type AnyRouter = Router<AnyRoute, any>
88
+ export type AnyRouter = Router<AnyRoute, any, any>
89
89
 
90
90
  export type RegisteredRouter = Register extends {
91
91
  router: infer TRouter extends AnyRouter
@@ -125,6 +125,7 @@ export interface RouterOptions<
125
125
  defaultStaleTime?: number
126
126
  defaultPreloadStaleTime?: number
127
127
  defaultPreloadGcTime?: number
128
+ notFoundMode?: 'root' | 'fuzzy'
128
129
  defaultGcTime?: number
129
130
  caseSensitive?: boolean
130
131
  routeTree?: TRouteTree
@@ -142,9 +143,9 @@ export interface RouterOptions<
142
143
  * See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
143
144
  */
144
145
  notFoundRoute?: AnyRoute
146
+ defaultNotFoundComponent?: NotFoundRouteComponent
145
147
  transformer?: RouterTransformer
146
148
  errorSerializer?: RouterErrorSerializer<TSerializedError>
147
- globalNotFound?: RouteComponent
148
149
  }
149
150
 
150
151
  export interface RouterTransformer {
@@ -166,6 +167,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
166
167
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
167
168
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
168
169
  lastUpdated: number
170
+ statusCode: number
169
171
  }
170
172
 
171
173
  export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
@@ -193,7 +195,7 @@ export interface DehydratedRouterState {
193
195
 
194
196
  export type DehydratedRouteMatch = Pick<
195
197
  RouteMatch,
196
- 'id' | 'status' | 'updatedAt' | 'notFoundError' | 'loaderData'
198
+ 'id' | 'status' | 'updatedAt' | 'loaderData'
197
199
  >
198
200
 
199
201
  export interface DehydratedRouter {
@@ -380,6 +382,9 @@ export class Router<
380
382
  this.state.isTransitioning || this.state.isLoading
381
383
  ? 'pending'
382
384
  : 'idle',
385
+ cachedMatches: this.state.cachedMatches.filter(
386
+ (d) => !['redirected'].includes(d.status),
387
+ ),
383
388
  }
384
389
  },
385
390
  })
@@ -586,7 +591,7 @@ export class Router<
586
591
  matchRoutes = <TRouteTree extends AnyRoute>(
587
592
  pathname: string,
588
593
  locationSearch: AnySearchSchema,
589
- opts?: { throwOnError?: boolean; debug?: boolean },
594
+ opts?: { preload?: boolean; throwOnError?: boolean; debug?: boolean },
590
595
  ): RouteMatch<TRouteTree>[] => {
591
596
  let routeParams: Record<string, string> = {}
592
597
 
@@ -611,7 +616,7 @@ export class Router<
611
616
  })
612
617
 
613
618
  let routeCursor: AnyRoute =
614
- foundRoute || (this.routesById as any)['__root__']
619
+ foundRoute || (this.routesById as any)[rootRouteId]
615
620
 
616
621
  let matchedRoutes: AnyRoute[] = [routeCursor]
617
622
 
@@ -639,6 +644,23 @@ export class Router<
639
644
  if (routeCursor) matchedRoutes.unshift(routeCursor)
640
645
  }
641
646
 
647
+ const globalNotFoundRouteId = (() => {
648
+ if (!isGlobalNotFound) {
649
+ return undefined
650
+ }
651
+
652
+ if (this.options.notFoundMode !== 'root') {
653
+ for (let i = matchedRoutes.length - 1; i >= 0; i--) {
654
+ const route = matchedRoutes[i]!
655
+ if (route.children) {
656
+ return route.id
657
+ }
658
+ }
659
+ }
660
+
661
+ return rootRouteId
662
+ })()
663
+
642
664
  // Existing matches are matches that are already loaded along with
643
665
  // pending matches that are still loading
644
666
 
@@ -677,6 +699,7 @@ export class Router<
677
699
  // which is used to uniquely identify the route match in state
678
700
 
679
701
  const parentMatch = matches[index - 1]
702
+ const isLast = index === matchedRoutes.length - 1
680
703
 
681
704
  const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
682
705
  // Validate the search params and stabilize them
@@ -737,7 +760,7 @@ export class Router<
737
760
  // Waste not, want not. If we already have a match for this route,
738
761
  // reuse it. This is important for layout routes, which might stick
739
762
  // around between navigation actions that only change leaf routes.
740
- const existingMatch = getRouteMatch(this.state, matchId)
763
+ let existingMatch = getRouteMatch(this.state, matchId)
741
764
 
742
765
  const cause = this.state.matches.find((d) => d.id === matchId)
743
766
  ? 'stay'
@@ -747,10 +770,6 @@ export class Router<
747
770
  ? {
748
771
  ...existingMatch,
749
772
  cause,
750
- notFoundError:
751
- isGlobalNotFound && route.id === rootRouteId
752
- ? { global: true }
753
- : undefined,
754
773
  params: routeParams,
755
774
  }
756
775
  : {
@@ -775,15 +794,16 @@ export class Router<
775
794
  loaderDeps,
776
795
  invalid: false,
777
796
  preload: false,
778
- notFoundError:
779
- isGlobalNotFound && route.id === rootRouteId
780
- ? { global: true }
781
- : undefined,
782
797
  links: route.options.links?.(),
783
798
  scripts: route.options.scripts?.(),
784
799
  staticData: route.options.staticData || {},
785
800
  }
786
801
 
802
+ if (!opts?.preload) {
803
+ // If we have a global not found, mark the right match as global not found
804
+ match.globalNotFound = globalNotFoundRouteId === route.id
805
+ }
806
+
787
807
  // Regardless of whether we're reusing an existing match or creating
788
808
  // a new one, we need to update the match's search params
789
809
  match.search = replaceEqualDeep(match.search, preMatchSearch)
@@ -796,9 +816,7 @@ export class Router<
796
816
  return matches as any
797
817
  }
798
818
 
799
- cancelMatch = (id: string) => {
800
- getRouteMatch(this.state, id)?.abortController?.abort()
801
- }
819
+ cancelMatch = (id: string) => {}
802
820
 
803
821
  cancelMatches = () => {
804
822
  this.state.pendingMatches?.forEach((match) => {
@@ -813,16 +831,23 @@ export class Router<
813
831
  } = {},
814
832
  matches?: AnyRouteMatch[],
815
833
  ): ParsedLocation => {
834
+ // if (dest.href) {
835
+ // return {
836
+ // pathname: dest.href,
837
+ // search: {},
838
+ // searchStr: '',
839
+ // state: {},
840
+ // hash: '',
841
+ // href: dest.href,
842
+ // unmaskOnReload: dest.unmaskOnReload,
843
+ // }
844
+ // }
845
+
816
846
  const relevantMatches = this.state.pendingMatches || this.state.matches
817
847
  const fromSearch =
818
848
  relevantMatches[relevantMatches.length - 1]?.search ||
819
849
  this.latestLocation.search
820
850
 
821
- let pathname = this.resolvePathWithBase(
822
- dest.from ?? this.latestLocation.pathname,
823
- `${dest.to ?? ''}`,
824
- )
825
-
826
851
  const fromMatches = this.matchRoutes(
827
852
  this.latestLocation.pathname,
828
853
  fromSearch,
@@ -831,6 +856,15 @@ export class Router<
831
856
  fromMatches?.find((e) => e.routeId === d.routeId),
832
857
  )
833
858
 
859
+ const fromRoute = this.looseRoutesById[last(fromMatches)?.routeId]
860
+
861
+ let pathname = dest.to
862
+ ? this.resolvePathWithBase(
863
+ dest.from ?? this.latestLocation.pathname,
864
+ `${dest.to}`,
865
+ )
866
+ : fromRoute?.fullPath
867
+
834
868
  const prevParams = { ...last(fromMatches)?.params }
835
869
 
836
870
  let nextParams =
@@ -1002,7 +1036,7 @@ export class Router<
1002
1036
 
1003
1037
  // If the next urls are the same and we're not replacing,
1004
1038
  // do nothing
1005
- if (!isSameUrl || !next.replace) {
1039
+ if (!isSameUrl) {
1006
1040
  let { maskedLocation, ...nextHistory } = next
1007
1041
 
1008
1042
  if (maskedLocation) {
@@ -1097,18 +1131,19 @@ export class Router<
1097
1131
 
1098
1132
  loadMatches = async ({
1099
1133
  checkLatest,
1134
+ location,
1100
1135
  matches,
1101
1136
  preload,
1102
1137
  }: {
1103
1138
  checkLatest: () => Promise<void> | undefined
1139
+ location: ParsedLocation
1104
1140
  matches: AnyRouteMatch[]
1105
1141
  preload?: boolean
1106
1142
  }): Promise<RouteMatch[]> => {
1107
1143
  let latestPromise
1108
1144
  let firstBadMatchIndex: number | undefined
1109
1145
 
1110
- const updateMatch = (match: AnyRouteMatch) => {
1111
- // const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
1146
+ const updateMatch = (match: AnyRouteMatch, opts?: { remove?: boolean }) => {
1112
1147
  const isPending = this.state.pendingMatches?.find(
1113
1148
  (d) => d.id === match.id,
1114
1149
  )
@@ -1123,29 +1158,45 @@ export class Router<
1123
1158
 
1124
1159
  this.__store.setState((s) => ({
1125
1160
  ...s,
1126
- [matchesKey]: s[matchesKey]?.map((d) =>
1127
- d.id === match.id ? match : d,
1128
- ),
1161
+ [matchesKey]: opts?.remove
1162
+ ? s[matchesKey]?.filter((d) => d.id !== match.id)
1163
+ : s[matchesKey]?.map((d) => (d.id === match.id ? match : d)),
1129
1164
  }))
1130
1165
  }
1131
1166
 
1167
+ const handleMatchSpecialError = (match: AnyRouteMatch, err: any) => {
1168
+ match = {
1169
+ ...match,
1170
+ status: isRedirect(err)
1171
+ ? 'redirected'
1172
+ : isNotFound(err)
1173
+ ? 'notFound'
1174
+ : 'error',
1175
+ isFetching: false,
1176
+ error: err,
1177
+ }
1178
+
1179
+ updateMatch(match)
1180
+
1181
+ if (!err.routeId) {
1182
+ err.routeId = match.routeId
1183
+ }
1184
+
1185
+ throw err
1186
+ }
1187
+
1132
1188
  // Check each match middleware to see if the route can be accessed
1133
1189
  for (let [index, match] of matches.entries()) {
1134
1190
  const parentMatch = matches[index - 1]
1135
1191
  const route = this.looseRoutesById[match.routeId]!
1136
1192
  const abortController = new AbortController()
1137
1193
 
1138
- const handleError = (err: any, code: string) => {
1194
+ const handleSerialError = (err: any, code: string) => {
1139
1195
  err.routerCode = code
1140
1196
  firstBadMatchIndex = firstBadMatchIndex ?? index
1141
1197
 
1142
- if (isRedirect(err)) {
1143
- throw err
1144
- }
1145
-
1146
- if (isNotFound(err)) {
1147
- err.routeId = match.routeId
1148
- throw err
1198
+ if (isRedirect(err) || isNotFound(err)) {
1199
+ handleMatchSpecialError(match, err)
1149
1200
  }
1150
1201
 
1151
1202
  try {
@@ -1153,8 +1204,8 @@ export class Router<
1153
1204
  } catch (errorHandlerErr) {
1154
1205
  err = errorHandlerErr
1155
1206
 
1156
- if (isRedirect(errorHandlerErr)) {
1157
- throw errorHandlerErr
1207
+ if (isRedirect(err) || isNotFound(err)) {
1208
+ handleMatchSpecialError(match, errorHandlerErr)
1158
1209
  }
1159
1210
  }
1160
1211
 
@@ -1167,15 +1218,19 @@ export class Router<
1167
1218
  }
1168
1219
  }
1169
1220
 
1170
- try {
1171
- if (match.paramsError) {
1172
- handleError(match.paramsError, 'PARSE_PARAMS')
1173
- }
1221
+ if (match.paramsError) {
1222
+ handleSerialError(match.paramsError, 'PARSE_PARAMS')
1223
+ }
1174
1224
 
1175
- if (match.searchError) {
1176
- handleError(match.searchError, 'VALIDATE_SEARCH')
1177
- }
1225
+ if (match.searchError) {
1226
+ handleSerialError(match.searchError, 'VALIDATE_SEARCH')
1227
+ }
1178
1228
 
1229
+ // if (match.globalNotFound && !preload) {
1230
+ // handleSerialError(notFound({ _global: true }), 'NOT_FOUND')
1231
+ // }
1232
+
1233
+ try {
1179
1234
  const parentContext = parentMatch?.context ?? this.options.context ?? {}
1180
1235
 
1181
1236
  const pendingMs =
@@ -1192,16 +1247,15 @@ export class Router<
1192
1247
  params: match.params,
1193
1248
  preload: !!preload,
1194
1249
  context: parentContext,
1195
- location: this.state.location,
1196
- // TOOD: just expose state and router, etc
1250
+ location,
1197
1251
  navigate: (opts) =>
1198
1252
  this.navigate({ ...opts, from: match.pathname } as any),
1199
1253
  buildLocation: this.buildLocation,
1200
1254
  cause: preload ? 'preload' : match.cause,
1201
1255
  })) ?? ({} as any)
1202
1256
 
1203
- if (isRedirect(beforeLoadContext)) {
1204
- throw beforeLoadContext
1257
+ if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
1258
+ handleSerialError(beforeLoadContext, 'BEFORE_LOAD')
1205
1259
  }
1206
1260
 
1207
1261
  const context = {
@@ -1217,7 +1271,7 @@ export class Router<
1217
1271
  pendingPromise,
1218
1272
  }
1219
1273
  } catch (err) {
1220
- handleError(err, 'BEFORE_LOAD')
1274
+ handleSerialError(err, 'BEFORE_LOAD')
1221
1275
  break
1222
1276
  }
1223
1277
  }
@@ -1232,13 +1286,8 @@ export class Router<
1232
1286
  const route = this.looseRoutesById[match.routeId]!
1233
1287
 
1234
1288
  const handleError = (err: any) => {
1235
- if (isRedirect(err)) {
1236
- throw err
1237
- }
1238
-
1239
- if (isNotFound(err)) {
1240
- err.routeId = match.routeId
1241
- throw err
1289
+ if (isRedirect(err) || isNotFound(err)) {
1290
+ handleMatchSpecialError(match, err)
1242
1291
  }
1243
1292
  }
1244
1293
 
@@ -1254,11 +1303,6 @@ export class Router<
1254
1303
  route.options.pendingMs ?? this.options.defaultPendingMs
1255
1304
  const pendingMinMs =
1256
1305
  route.options.pendingMinMs ?? this.options.defaultPendingMinMs
1257
- const shouldPending =
1258
- !preload &&
1259
- typeof pendingMs === 'number' &&
1260
- (route.options.pendingComponent ??
1261
- this.options.defaultPendingComponent)
1262
1306
 
1263
1307
  const loaderContext: LoaderFnContext = {
1264
1308
  params: match.params,
@@ -1267,72 +1311,69 @@ export class Router<
1267
1311
  parentMatchPromise,
1268
1312
  abortController: match.abortController,
1269
1313
  context: match.context,
1270
- location: this.state.location,
1314
+ location,
1271
1315
  navigate: (opts) =>
1272
1316
  this.navigate({ ...opts, from: match.pathname } as any),
1273
1317
  cause: preload ? 'preload' : match.cause,
1318
+ route,
1274
1319
  }
1275
1320
 
1276
1321
  const fetch = async () => {
1277
- if (match.isFetching) {
1278
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
1279
- } else {
1280
- // If the user doesn't want the route to reload, just
1281
- // resolve with the existing loader data
1282
-
1283
- if (match.fetchCount && match.status === 'success') {
1284
- resolve()
1322
+ try {
1323
+ if (match.isFetching) {
1324
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
1325
+ } else {
1326
+ // If the user doesn't want the route to reload, just
1327
+ // resolve with the existing loader data
1328
+
1329
+ // if (match.fetchCount && match.status === 'success') {
1330
+ // resolve()
1331
+ // }
1332
+
1333
+ // Otherwise, load the route
1334
+ matches[index] = match = {
1335
+ ...match,
1336
+ isFetching: true,
1337
+ fetchCount: match.fetchCount + 1,
1338
+ }
1339
+
1340
+ const lazyPromise =
1341
+ route.lazyFn?.().then((lazyRoute) => {
1342
+ Object.assign(route.options, lazyRoute.options)
1343
+ }) || Promise.resolve()
1344
+
1345
+ // If for some reason lazy resolves more lazy components...
1346
+ // We'll wait for that before pre attempt to preload any
1347
+ // components themselves.
1348
+ const componentsPromise = lazyPromise.then(() =>
1349
+ Promise.all(
1350
+ componentTypes.map(async (type) => {
1351
+ const component = route.options[type]
1352
+
1353
+ if ((component as any)?.preload) {
1354
+ await (component as any).preload()
1355
+ }
1356
+ }),
1357
+ ),
1358
+ )
1359
+
1360
+ // Kick off the loader!
1361
+ const loaderPromise = route.options.loader?.(loaderContext)
1362
+
1363
+ loadPromise = Promise.all([
1364
+ componentsPromise,
1365
+ loaderPromise,
1366
+ lazyPromise,
1367
+ ]).then((d) => d[1])
1285
1368
  }
1286
1369
 
1287
- // Otherwise, load the route
1288
1370
  matches[index] = match = {
1289
1371
  ...match,
1290
- isFetching: true,
1291
- fetchCount: match.fetchCount + 1,
1292
- }
1293
-
1294
- const lazyPromise =
1295
- route.lazyFn?.().then((lazyRoute) => {
1296
- Object.assign(route.options, lazyRoute.options)
1297
- }) || Promise.resolve()
1298
-
1299
- // If for some reason lazy resolves more lazy components...
1300
- // We'll wait for that before pre attempt to preload any
1301
- // components themselves.
1302
- const componentsPromise = lazyPromise.then(() =>
1303
- Promise.all(
1304
- componentTypes.map(async (type) => {
1305
- const component = route.options[type]
1306
-
1307
- if ((component as any)?.preload) {
1308
- await (component as any).preload()
1309
- }
1310
- }),
1311
- ),
1312
- )
1313
-
1314
- // wrap loader into an async function to be able to catch synchronous exceptions
1315
- async function loader() {
1316
- return await route.options.loader?.(loaderContext)
1372
+ loadPromise,
1317
1373
  }
1318
- // Kick off the loader!
1319
- const loaderPromise = loader()
1320
-
1321
- loadPromise = Promise.all([
1322
- componentsPromise,
1323
- loaderPromise,
1324
- lazyPromise,
1325
- ]).then((d) => d[1])
1326
- }
1327
-
1328
- matches[index] = match = {
1329
- ...match,
1330
- loadPromise,
1331
- }
1332
1374
 
1333
- updateMatch(match)
1375
+ updateMatch(match)
1334
1376
 
1335
- try {
1336
1377
  const loaderData = await loadPromise
1337
1378
  if ((latestPromise = checkLatest())) return await latestPromise
1338
1379
 
@@ -1367,6 +1408,7 @@ export class Router<
1367
1408
  }
1368
1409
  } catch (error) {
1369
1410
  if ((latestPromise = checkLatest())) return await latestPromise
1411
+
1370
1412
  handleError(error)
1371
1413
 
1372
1414
  try {
@@ -1414,10 +1456,44 @@ export class Router<
1414
1456
  !!preload && !this.state.matches.find((d) => d.id === match.id),
1415
1457
  }
1416
1458
 
1417
- try {
1418
- if (match.status !== 'success') {
1419
- // If we need to potentially show the pending component,
1420
- // start a timer to show it after the pendingMs
1459
+ // If the route is successful and still fresh, just resolve
1460
+ if (
1461
+ match.status === 'success' &&
1462
+ (match.invalid || (shouldReload ?? age > staleAge))
1463
+ ) {
1464
+ ;(async () => {
1465
+ try {
1466
+ await fetch()
1467
+ } catch (err) {
1468
+ console.info('Background Fetching Error', err)
1469
+
1470
+ if (isRedirect(err)) {
1471
+ const isActive = (
1472
+ this.state.pendingMatches || this.state.matches
1473
+ ).find((d) => d.id === match.id)
1474
+
1475
+ // Redirects should not be persisted
1476
+ handleError(err)
1477
+
1478
+ // If the route is still active, redirect
1479
+ if (isActive) {
1480
+ this.handleRedirect(err)
1481
+ }
1482
+ }
1483
+ }
1484
+ })()
1485
+
1486
+ return resolve()
1487
+ }
1488
+
1489
+ const shouldPending =
1490
+ !preload &&
1491
+ typeof pendingMs === 'number' &&
1492
+ (route.options.pendingComponent ??
1493
+ this.options.defaultPendingComponent)
1494
+
1495
+ if (match.status !== 'success') {
1496
+ try {
1421
1497
  if (shouldPending) {
1422
1498
  match.pendingPromise?.then(async () => {
1423
1499
  if ((latestPromise = checkLatest())) return latestPromise
@@ -1433,14 +1509,10 @@ export class Router<
1433
1509
  })
1434
1510
  }
1435
1511
 
1436
- // Critical Fetching, we need to await
1437
1512
  await fetch()
1438
- } else if (match.invalid || (shouldReload ?? age > staleAge)) {
1439
- // Background Fetching, no need to wait
1440
- fetch()
1513
+ } catch (err) {
1514
+ reject(err)
1441
1515
  }
1442
- } catch (err) {
1443
- reject(err)
1444
1516
  }
1445
1517
 
1446
1518
  resolve()
@@ -1511,18 +1583,28 @@ export class Router<
1511
1583
  })
1512
1584
 
1513
1585
  try {
1586
+ let redirected: AnyRedirect
1587
+ let notFound: NotFoundError
1588
+
1514
1589
  try {
1515
1590
  // Load the matches
1516
1591
  await this.loadMatches({
1517
1592
  matches: pendingMatches,
1593
+ location: next,
1518
1594
  checkLatest: () => this.checkLatest(promise),
1519
1595
  })
1520
1596
  } catch (err) {
1521
1597
  if (isRedirect(err)) {
1598
+ redirected = err
1522
1599
  this.handleRedirect(err)
1523
1600
  } else if (isNotFound(err)) {
1601
+ notFound = err
1524
1602
  this.handleNotFound(pendingMatches, err)
1525
1603
  }
1604
+
1605
+ // Swallow all other errors that happen inside
1606
+ // of loadMatches. These errors will be handled
1607
+ // as state on each match.
1526
1608
  }
1527
1609
 
1528
1610
  // Only apply the latest transition
@@ -1552,6 +1634,12 @@ export class Router<
1552
1634
  ...s.cachedMatches,
1553
1635
  ...exitingMatches.filter((d) => d.status !== 'error'),
1554
1636
  ],
1637
+ statusCode:
1638
+ redirected?.code || notFound
1639
+ ? 404
1640
+ : s.matches.some((d) => d.status === 'error')
1641
+ ? 500
1642
+ : 200,
1555
1643
  }))
1556
1644
  this.cleanCache()
1557
1645
  })
@@ -1583,6 +1671,8 @@ export class Router<
1583
1671
  return latestPromise
1584
1672
  }
1585
1673
 
1674
+ console.log('Load Error', err)
1675
+
1586
1676
  reject(err)
1587
1677
  }
1588
1678
  })
@@ -1596,12 +1686,9 @@ export class Router<
1596
1686
  if (!err.href) {
1597
1687
  err.href = this.buildLocation(err as any).href
1598
1688
  }
1599
-
1600
- if (isServer) {
1601
- throw err
1689
+ if (!isServer) {
1690
+ this.navigate({ ...(err as any), replace: true })
1602
1691
  }
1603
-
1604
- this.navigate(err as any)
1605
1692
  }
1606
1693
 
1607
1694
  cleanCache = () => {
@@ -1630,13 +1717,19 @@ export class Router<
1630
1717
  })
1631
1718
  }
1632
1719
 
1633
- preloadRoute = async (
1634
- navigateOpts: ToOptions<TRouteTree> = this.state.location as any,
1635
- ) => {
1636
- let next = this.buildLocation(navigateOpts as any)
1720
+ preloadRoute = async <
1721
+ TFrom extends RoutePaths<TRouteTree> | string = string,
1722
+ TTo extends string = '',
1723
+ TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
1724
+ TMaskTo extends string = '',
1725
+ >(
1726
+ opts: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
1727
+ ): Promise<AnyRouteMatch[] | undefined> => {
1728
+ let next = this.buildLocation(opts as any)
1637
1729
 
1638
1730
  let matches = this.matchRoutes(next.pathname, next.search, {
1639
1731
  throwOnError: true,
1732
+ preload: true,
1640
1733
  })
1641
1734
 
1642
1735
  const loadedMatchIds = Object.fromEntries(
@@ -1661,17 +1754,18 @@ export class Router<
1661
1754
  try {
1662
1755
  matches = await this.loadMatches({
1663
1756
  matches,
1757
+ location: next,
1664
1758
  preload: true,
1665
1759
  checkLatest: () => undefined,
1666
1760
  })
1667
1761
 
1668
1762
  return matches
1669
1763
  } catch (err) {
1670
- // Preload errors are not fatal, but we should still log them
1671
- if (!isRedirect(err) && !isNotFound(err)) {
1672
- console.error(err)
1764
+ if (isRedirect(err)) {
1765
+ return await this.preloadRoute(err as any)
1673
1766
  }
1674
-
1767
+ // Preload errors are not fatal, but we should still log them
1768
+ console.error(err)
1675
1769
  return undefined
1676
1770
  }
1677
1771
  }
@@ -1801,15 +1895,7 @@ export class Router<
1801
1895
  return {
1802
1896
  state: {
1803
1897
  dehydratedMatches: this.state.matches.map((d) => ({
1804
- ...pick(d, [
1805
- 'id',
1806
- 'status',
1807
- 'updatedAt',
1808
- 'loaderData',
1809
- // Not-founds that occur during SSR don't require the client to load data before
1810
- // triggering in order to prevent the flicker of the loading component
1811
- 'notFoundError',
1812
- ]),
1898
+ ...pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
1813
1899
  // If an error occurs server-side during SSRing,
1814
1900
  // send a small subset of the error to the client
1815
1901
  error: d.error
@@ -1879,43 +1965,48 @@ export class Router<
1879
1965
  })
1880
1966
  }
1881
1967
 
1882
- // Finds a match that has a notFoundComponent
1883
1968
  handleNotFound = (matches: AnyRouteMatch[], err: NotFoundError) => {
1884
1969
  const matchesByRouteId = Object.fromEntries(
1885
1970
  matches.map((match) => [match.routeId, match]),
1886
1971
  ) as Record<string, AnyRouteMatch>
1887
1972
 
1888
- if (!err.global && err.routeId) {
1889
- // If the err contains a routeId, start searching up from that route
1890
- let currentRoute = this.looseRoutesById[err.routeId]
1891
-
1892
- if (currentRoute) {
1893
- // Go up the tree until we find a route with a notFoundComponent
1894
- while (!currentRoute.options.notFoundComponent) {
1895
- currentRoute = currentRoute?.parentRoute
1896
-
1897
- invariant(
1898
- currentRoute,
1899
- 'Found invalid route tree while trying to find not-found handler.',
1900
- )
1973
+ // Start at the route that errored or default to the root route
1974
+ let routeCursor =
1975
+ (err.global
1976
+ ? this.looseRoutesById[rootRouteId]
1977
+ : this.looseRoutesById[err.routeId]) ||
1978
+ this.looseRoutesById[rootRouteId]!
1979
+
1980
+ // Go up the tree until we find a route with a notFoundComponent or we hit the root
1981
+ while (
1982
+ !routeCursor.options.notFoundComponent &&
1983
+ !this.options.defaultNotFoundComponent &&
1984
+ routeCursor.id !== rootRouteId
1985
+ ) {
1986
+ routeCursor = routeCursor?.parentRoute
1901
1987
 
1902
- if (currentRoute.id === rootRouteId) break
1903
- }
1988
+ invariant(
1989
+ routeCursor,
1990
+ 'Found invalid route tree while trying to find not-found handler.',
1991
+ )
1992
+ }
1904
1993
 
1905
- const match = matchesByRouteId[currentRoute.id]
1906
- invariant(match, 'Could not find match for route: ' + currentRoute.id)
1907
- match.notFoundError = err
1994
+ let match = matchesByRouteId[routeCursor.id]
1908
1995
 
1909
- return
1910
- }
1911
- }
1996
+ invariant(match, 'Could not find match for route: ' + routeCursor.id)
1912
1997
 
1913
- // Otherwise, just set the notFoundError on the root route
1914
- matchesByRouteId[rootRouteId]!.notFoundError = err
1998
+ // Assign the error to the match
1999
+ Object.assign(match, {
2000
+ status: 'notFound',
2001
+ error: err,
2002
+ isFetching: false,
2003
+ } as AnyRouteMatch)
1915
2004
  }
1916
2005
 
1917
2006
  hasNotFoundMatch = () => {
1918
- return this.__store.state.matches.some((d) => d.notFoundError)
2007
+ return this.__store.state.matches.some(
2008
+ (d) => d.status === 'notFound' || d.globalNotFound,
2009
+ )
1919
2010
  }
1920
2011
 
1921
2012
  // resolveMatchPromise = (matchId: string, key: string, value: any) => {
@@ -1957,6 +2048,7 @@ export function getInitialRouterState(
1957
2048
  pendingMatches: [],
1958
2049
  cachedMatches: [],
1959
2050
  lastUpdated: 0,
2051
+ statusCode: 200,
1960
2052
  }
1961
2053
  }
1962
2054