@tanstack/react-router 1.28.1 → 1.28.2

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 (45) hide show
  1. package/dist/cjs/Matches.cjs +45 -23
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/Matches.d.cts +5 -6
  4. package/dist/cjs/fileRoute.d.cts +4 -4
  5. package/dist/cjs/index.cjs +0 -1
  6. package/dist/cjs/index.cjs.map +1 -1
  7. package/dist/cjs/index.d.cts +1 -1
  8. package/dist/cjs/link.cjs.map +1 -1
  9. package/dist/cjs/link.d.cts +2 -0
  10. package/dist/cjs/redirects.cjs.map +1 -1
  11. package/dist/cjs/redirects.d.cts +3 -1
  12. package/dist/cjs/route.cjs.map +1 -1
  13. package/dist/cjs/route.d.cts +2 -2
  14. package/dist/cjs/router.cjs +374 -327
  15. package/dist/cjs/router.cjs.map +1 -1
  16. package/dist/cjs/router.d.cts +2 -2
  17. package/dist/cjs/utils.cjs +20 -2
  18. package/dist/cjs/utils.cjs.map +1 -1
  19. package/dist/cjs/utils.d.cts +6 -1
  20. package/dist/esm/Matches.d.ts +5 -6
  21. package/dist/esm/Matches.js +46 -24
  22. package/dist/esm/Matches.js.map +1 -1
  23. package/dist/esm/fileRoute.d.ts +4 -4
  24. package/dist/esm/index.d.ts +1 -1
  25. package/dist/esm/index.js +1 -2
  26. package/dist/esm/link.d.ts +2 -0
  27. package/dist/esm/link.js.map +1 -1
  28. package/dist/esm/redirects.d.ts +3 -1
  29. package/dist/esm/redirects.js.map +1 -1
  30. package/dist/esm/route.d.ts +2 -2
  31. package/dist/esm/route.js.map +1 -1
  32. package/dist/esm/router.d.ts +2 -2
  33. package/dist/esm/router.js +375 -328
  34. package/dist/esm/router.js.map +1 -1
  35. package/dist/esm/utils.d.ts +6 -1
  36. package/dist/esm/utils.js +20 -2
  37. package/dist/esm/utils.js.map +1 -1
  38. package/package.json +4 -2
  39. package/src/Matches.tsx +73 -35
  40. package/src/index.tsx +0 -1
  41. package/src/link.tsx +2 -0
  42. package/src/redirects.ts +4 -2
  43. package/src/route.ts +2 -7
  44. package/src/router.ts +498 -426
  45. package/src/utils.ts +31 -2
package/src/router.ts CHANGED
@@ -5,10 +5,10 @@ import warning from 'tiny-warning'
5
5
  import { rootRouteId } from './route'
6
6
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
7
7
  import {
8
+ createControlledPromise,
8
9
  deepEqual,
9
10
  escapeJSON,
10
11
  functionalUpdate,
11
- isServer,
12
12
  last,
13
13
  pick,
14
14
  replaceEqualDeep,
@@ -171,7 +171,6 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
171
171
  cachedMatches: Array<RouteMatch<TRouteTree>>
172
172
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
173
173
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
174
- lastUpdated: number
175
174
  statusCode: number
176
175
  redirect?: ResolvedRedirect
177
176
  }
@@ -193,6 +192,7 @@ export interface BuildNextOptions {
193
192
  unmaskOnReload?: boolean
194
193
  }
195
194
  from?: string
195
+ _fromLocation?: ParsedLocation
196
196
  }
197
197
 
198
198
  export interface DehydratedRouterState {
@@ -321,6 +321,8 @@ export class Router<
321
321
  }
322
322
  }
323
323
 
324
+ isServer = typeof document === 'undefined'
325
+
324
326
  // These are default implementations that can optionally be overridden
325
327
  // by the router provider once rendered. We provide these so that the
326
328
  // router can be used in a non-react environment if necessary
@@ -609,7 +611,7 @@ export class Router<
609
611
  matchRoutes = <TRouteTree extends AnyRoute>(
610
612
  pathname: string,
611
613
  locationSearch: AnySearchSchema,
612
- opts?: { preload?: boolean; throwOnError?: boolean; debug?: boolean },
614
+ opts?: { preload?: boolean; throwOnError?: boolean },
613
615
  ): Array<RouteMatch<TRouteTree>> => {
614
616
  let routeParams: Record<string, string> = {}
615
617
 
@@ -784,38 +786,68 @@ export class Router<
784
786
  ? 'stay'
785
787
  : 'enter'
786
788
 
787
- const match: AnyRouteMatch = existingMatch
788
- ? {
789
- ...existingMatch,
790
- cause,
791
- params: routeParams,
792
- }
793
- : {
794
- id: matchId,
795
- routeId: route.id,
796
- params: routeParams,
797
- pathname: joinPaths([this.basepath, interpolatedPath]),
798
- updatedAt: Date.now(),
799
- search: {} as any,
800
- searchError: undefined,
801
- status: 'pending',
802
- showPending: false,
803
- isFetching: false,
804
- error: undefined,
805
- paramsError: parseErrors[index],
806
- loadPromise: Promise.resolve(),
807
- routeContext: undefined!,
808
- context: undefined!,
809
- abortController: new AbortController(),
810
- fetchCount: 0,
811
- cause,
812
- loaderDeps,
813
- invalid: false,
814
- preload: false,
815
- links: route.options.links?.(),
816
- scripts: route.options.scripts?.(),
817
- staticData: route.options.staticData || {},
818
- }
789
+ let match: AnyRouteMatch
790
+
791
+ if (existingMatch) {
792
+ match = {
793
+ ...existingMatch,
794
+ cause,
795
+ params: routeParams,
796
+ }
797
+ } else {
798
+ const status =
799
+ route.options.loader || route.options.beforeLoad
800
+ ? 'pending'
801
+ : 'success'
802
+
803
+ const loadPromise = createControlledPromise<void>()
804
+
805
+ // If it's already a success, resolve the load promise
806
+ if (status === 'success') {
807
+ loadPromise.resolve()
808
+ }
809
+
810
+ match = {
811
+ id: matchId,
812
+ routeId: route.id,
813
+ params: routeParams,
814
+ pathname: joinPaths([this.basepath, interpolatedPath]),
815
+ updatedAt: Date.now(),
816
+ search: {} as any,
817
+ searchError: undefined,
818
+ status: 'pending',
819
+ isFetching: false,
820
+ error: undefined,
821
+ paramsError: parseErrors[index],
822
+ loaderPromise: Promise.resolve(),
823
+ loadPromise,
824
+ routeContext: undefined!,
825
+ context: undefined!,
826
+ abortController: new AbortController(),
827
+ fetchCount: 0,
828
+ cause,
829
+ loaderDeps,
830
+ invalid: false,
831
+ preload: false,
832
+ links: route.options.links?.(),
833
+ scripts: route.options.scripts?.(),
834
+ staticData: route.options.staticData || {},
835
+ }
836
+ }
837
+
838
+ // If it's already a success, update the meta and headers
839
+ // These may get updated again if the match is refreshed
840
+ // due to being stale
841
+ if (match.status === 'success') {
842
+ match.meta = route.options.meta?.({
843
+ params: match.params,
844
+ loaderData: match.loaderData,
845
+ })
846
+
847
+ match.headers = route.options.headers?.({
848
+ loaderData: match.loaderData,
849
+ })
850
+ }
819
851
 
820
852
  if (!opts?.preload) {
821
853
  // If we have a global not found, mark the right match as global not found
@@ -851,28 +883,13 @@ export class Router<
851
883
  } = {},
852
884
  matches?: Array<AnyRouteMatch>,
853
885
  ): ParsedLocation => {
854
- // if (dest.href) {
855
- // return {
856
- // pathname: dest.href,
857
- // search: {},
858
- // searchStr: '',
859
- // state: {},
860
- // hash: '',
861
- // href: dest.href,
862
- // unmaskOnReload: dest.unmaskOnReload,
863
- // }
864
- // }
865
-
866
- const relevantMatches = this.state.pendingMatches || this.state.matches
867
- const fromSearch =
868
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
869
- relevantMatches[relevantMatches.length - 1]?.search ||
870
- this.latestLocation.search
871
-
872
- const fromMatches = this.matchRoutes(
873
- this.latestLocation.pathname,
874
- fromSearch,
875
- )
886
+ const fromPath = dest.from || this.latestLocation.pathname
887
+ let fromSearch = dest._fromLocation?.search || this.latestLocation.search
888
+
889
+ const fromMatches = this.matchRoutes(fromPath, fromSearch)
890
+
891
+ fromSearch = last(fromMatches)?.search || this.latestLocation.search
892
+
876
893
  const stayingMatches = matches?.filter((d) =>
877
894
  fromMatches.find((e) => e.routeId === d.routeId),
878
895
  )
@@ -1162,12 +1179,14 @@ export class Router<
1162
1179
  let latestPromise
1163
1180
  let firstBadMatchIndex: number | undefined
1164
1181
 
1165
- const updateMatch = (match: AnyRouteMatch, opts?: { remove?: boolean }) => {
1166
- const isPending = this.state.pendingMatches?.find(
1167
- (d) => d.id === match.id,
1168
- )
1169
-
1170
- const isMatched = this.state.matches.find((d) => d.id === match.id)
1182
+ const updateMatch = (
1183
+ id: string,
1184
+ updater: (match: AnyRouteMatch) => AnyRouteMatch,
1185
+ opts?: { remove?: boolean },
1186
+ ) => {
1187
+ let updated!: AnyRouteMatch
1188
+ const isPending = this.state.pendingMatches?.find((d) => d.id === id)
1189
+ const isMatched = this.state.matches.find((d) => d.id === id)
1171
1190
 
1172
1191
  const matchesKey = isPending
1173
1192
  ? 'pendingMatches'
@@ -1178,14 +1197,18 @@ export class Router<
1178
1197
  this.__store.setState((s) => ({
1179
1198
  ...s,
1180
1199
  [matchesKey]: opts?.remove
1181
- ? s[matchesKey]?.filter((d) => d.id !== match.id)
1182
- : s[matchesKey]?.map((d) => (d.id === match.id ? match : d)),
1200
+ ? s[matchesKey]?.filter((d) => d.id !== id)
1201
+ : s[matchesKey]?.map((d) =>
1202
+ d.id === id ? (updated = updater(d)) : d,
1203
+ ),
1183
1204
  }))
1205
+
1206
+ return updated
1184
1207
  }
1185
1208
 
1186
1209
  const handleMatchSpecialError = (match: AnyRouteMatch, err: any) => {
1187
- match = {
1188
- ...match,
1210
+ updateMatch(match.id, (prev) => ({
1211
+ ...prev,
1189
1212
  status: isRedirect(err)
1190
1213
  ? 'redirected'
1191
1214
  : isNotFound(err)
@@ -1193,9 +1216,7 @@ export class Router<
1193
1216
  : 'error',
1194
1217
  isFetching: false,
1195
1218
  error: err,
1196
- }
1197
-
1198
- updateMatch(match)
1219
+ }))
1199
1220
 
1200
1221
  if (!err.routeId) {
1201
1222
  err.routeId = match.routeId
@@ -1204,349 +1225,394 @@ export class Router<
1204
1225
  throw err
1205
1226
  }
1206
1227
 
1207
- // Check each match middleware to see if the route can be accessed
1208
- // eslint-disable-next-line prefer-const
1209
- for (let [index, match] of matches.entries()) {
1210
- const parentMatch = matches[index - 1]
1211
- const route = this.looseRoutesById[match.routeId]!
1212
- const abortController = new AbortController()
1213
-
1214
- const handleSerialError = (err: any, code: string) => {
1215
- err.routerCode = code
1216
- firstBadMatchIndex = firstBadMatchIndex ?? index
1217
-
1218
- if (isRedirect(err) || isNotFound(err)) {
1219
- handleMatchSpecialError(match, err)
1220
- }
1221
-
1222
- try {
1223
- route.options.onError?.(err)
1224
- } catch (errorHandlerErr) {
1225
- err = errorHandlerErr
1226
-
1227
- if (isRedirect(err) || isNotFound(err)) {
1228
- handleMatchSpecialError(match, errorHandlerErr)
1229
- }
1230
- }
1228
+ try {
1229
+ await new Promise<void>((resolveAll, rejectAll) => {
1230
+ ;(async () => {
1231
+ try {
1232
+ // Check each match middleware to see if the route can be accessed
1233
+ // eslint-disable-next-line prefer-const
1234
+ for (let [index, match] of matches.entries()) {
1235
+ const parentMatch = matches[index - 1]
1236
+ const route = this.looseRoutesById[match.routeId]!
1237
+ const abortController = new AbortController()
1238
+
1239
+ const handleSerialError = (err: any, code: string) => {
1240
+ err.routerCode = code
1241
+ firstBadMatchIndex = firstBadMatchIndex ?? index
1242
+
1243
+ if (isRedirect(err) || isNotFound(err)) {
1244
+ handleMatchSpecialError(match, err)
1245
+ }
1231
1246
 
1232
- matches[index] = match = {
1233
- ...match,
1234
- error: err,
1235
- status: 'error',
1236
- updatedAt: Date.now(),
1237
- abortController: new AbortController(),
1238
- }
1239
- }
1247
+ try {
1248
+ route.options.onError?.(err)
1249
+ } catch (errorHandlerErr) {
1250
+ err = errorHandlerErr
1240
1251
 
1241
- if (match.paramsError) {
1242
- handleSerialError(match.paramsError, 'PARSE_PARAMS')
1243
- }
1252
+ if (isRedirect(err) || isNotFound(err)) {
1253
+ handleMatchSpecialError(match, errorHandlerErr)
1254
+ }
1255
+ }
1244
1256
 
1245
- if (match.searchError) {
1246
- handleSerialError(match.searchError, 'VALIDATE_SEARCH')
1247
- }
1257
+ matches[index] = match = {
1258
+ ...match,
1259
+ error: err,
1260
+ status: 'error',
1261
+ updatedAt: Date.now(),
1262
+ abortController: new AbortController(),
1263
+ }
1264
+ }
1248
1265
 
1249
- // if (match.globalNotFound && !preload) {
1250
- // handleSerialError(notFound({ _global: true }), 'NOT_FOUND')
1251
- // }
1266
+ if (match.paramsError) {
1267
+ handleSerialError(match.paramsError, 'PARSE_PARAMS')
1268
+ }
1252
1269
 
1253
- try {
1254
- const parentContext = parentMatch?.context ?? this.options.context ?? {}
1255
-
1256
- const pendingMs =
1257
- route.options.pendingMs ?? this.options.defaultPendingMs
1258
- const pendingPromise =
1259
- typeof pendingMs === 'number' && pendingMs <= 0
1260
- ? Promise.resolve()
1261
- : new Promise<void>((r) => setTimeout(r, pendingMs))
1262
-
1263
- const beforeLoadContext =
1264
- (await route.options.beforeLoad?.({
1265
- search: match.search,
1266
- abortController,
1267
- params: match.params,
1268
- preload: !!preload,
1269
- context: parentContext,
1270
- location,
1271
- navigate: (opts: any) =>
1272
- this.navigate({ ...opts, from: match.pathname }),
1273
- buildLocation: this.buildLocation,
1274
- cause: preload ? 'preload' : match.cause,
1275
- })) ?? ({} as any)
1276
-
1277
- if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
1278
- handleSerialError(beforeLoadContext, 'BEFORE_LOAD')
1279
- }
1270
+ if (match.searchError) {
1271
+ handleSerialError(match.searchError, 'VALIDATE_SEARCH')
1272
+ }
1280
1273
 
1281
- const context = {
1282
- ...parentContext,
1283
- ...beforeLoadContext,
1284
- }
1274
+ // if (match.globalNotFound && !preload) {
1275
+ // handleSerialError(notFound({ _global: true }), 'NOT_FOUND')
1276
+ // }
1285
1277
 
1286
- matches[index] = match = {
1287
- ...match,
1288
- routeContext: replaceEqualDeep(match.routeContext, beforeLoadContext),
1289
- context: replaceEqualDeep(match.context, context),
1290
- abortController,
1291
- pendingPromise,
1292
- }
1293
- } catch (err) {
1294
- handleSerialError(err, 'BEFORE_LOAD')
1295
- break
1296
- }
1297
- }
1278
+ try {
1279
+ const parentContext =
1280
+ parentMatch?.context ?? this.options.context ?? {}
1281
+
1282
+ const pendingMs =
1283
+ route.options.pendingMs ?? this.options.defaultPendingMs
1284
+ const pendingPromise =
1285
+ typeof pendingMs !== 'number' || pendingMs <= 0
1286
+ ? Promise.resolve()
1287
+ : new Promise<void>((r) => {
1288
+ if (pendingMs !== Infinity) setTimeout(r, pendingMs)
1289
+ })
1290
+
1291
+ const shouldPending =
1292
+ !this.isServer &&
1293
+ !preload &&
1294
+ (route.options.loader || route.options.beforeLoad) &&
1295
+ typeof pendingMs === 'number' &&
1296
+ (route.options.pendingComponent ??
1297
+ this.options.defaultPendingComponent)
1298
+
1299
+ if (shouldPending) {
1300
+ // If we might show a pending component, we need to wait for the
1301
+ // pending promise to resolve before we start showing that state
1302
+ pendingPromise.then(async () => {
1303
+ if ((latestPromise = checkLatest())) return latestPromise
1304
+ // Update the match and prematurely resolve the loadMatches promise so that
1305
+ // the pending component can start rendering
1306
+ resolveAll()
1307
+ })
1308
+ }
1298
1309
 
1299
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
1300
- const matchPromises: Array<Promise<any>> = []
1310
+ const beforeLoadContext =
1311
+ (await route.options.beforeLoad?.({
1312
+ search: match.search,
1313
+ abortController,
1314
+ params: match.params,
1315
+ preload: !!preload,
1316
+ context: parentContext,
1317
+ location,
1318
+ navigate: (opts: any) =>
1319
+ this.navigate({ ...opts, from: match.pathname }),
1320
+ buildLocation: this.buildLocation,
1321
+ cause: preload ? 'preload' : match.cause,
1322
+ })) ?? ({} as any)
1323
+
1324
+ if (
1325
+ isRedirect(beforeLoadContext) ||
1326
+ isNotFound(beforeLoadContext)
1327
+ ) {
1328
+ handleSerialError(beforeLoadContext, 'BEFORE_LOAD')
1329
+ }
1301
1330
 
1302
- validResolvedMatches.forEach((match, index) => {
1303
- matchPromises.push(
1304
- // eslint-disable-next-line no-async-promise-executor
1305
- new Promise<void>(async (resolve, reject) => {
1306
- const parentMatchPromise = matchPromises[index - 1]
1307
- const route = this.looseRoutesById[match.routeId]!
1331
+ const context = {
1332
+ ...parentContext,
1333
+ ...beforeLoadContext,
1334
+ }
1308
1335
 
1309
- const handleError = (err: any) => {
1310
- if (isRedirect(err) || isNotFound(err)) {
1311
- handleMatchSpecialError(match, err)
1336
+ matches[index] = match = {
1337
+ ...match,
1338
+ routeContext: replaceEqualDeep(
1339
+ match.routeContext,
1340
+ beforeLoadContext,
1341
+ ),
1342
+ context: replaceEqualDeep(match.context, context),
1343
+ abortController,
1344
+ }
1345
+ } catch (err) {
1346
+ handleSerialError(err, 'BEFORE_LOAD')
1347
+ break
1348
+ }
1312
1349
  }
1313
- }
1314
-
1315
- let loadPromise: Promise<void> | undefined
1316
-
1317
- matches[index] = match = {
1318
- ...match,
1319
- showPending: false,
1320
- }
1321
1350
 
1322
- let didShowPending = false
1323
- const pendingMs =
1324
- route.options.pendingMs ?? this.options.defaultPendingMs
1325
- const pendingMinMs =
1326
- route.options.pendingMinMs ?? this.options.defaultPendingMinMs
1327
-
1328
- const loaderContext: LoaderFnContext = {
1329
- params: match.params,
1330
- deps: match.loaderDeps,
1331
- preload: !!preload,
1332
- parentMatchPromise,
1333
- abortController: match.abortController,
1334
- context: match.context,
1335
- location,
1336
- navigate: (opts) =>
1337
- this.navigate({ ...opts, from: match.pathname } as any),
1338
- cause: preload ? 'preload' : match.cause,
1339
- route,
1340
- }
1351
+ const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
1352
+ const matchPromises: Array<Promise<any>> = []
1341
1353
 
1342
- const fetch = async () => {
1343
- try {
1344
- if (match.isFetching) {
1345
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
1346
- } else {
1347
- // If the user doesn't want the route to reload, just
1348
- // resolve with the existing loader data
1354
+ await Promise.all(
1355
+ validResolvedMatches.map(async (match, index) => {
1356
+ const parentMatchPromise = matchPromises[index - 1]
1357
+ const route = this.looseRoutesById[match.routeId]!
1349
1358
 
1350
- // if (match.fetchCount && match.status === 'success') {
1351
- // resolve()
1352
- // }
1359
+ const handleError = (err: any) => {
1360
+ if (isRedirect(err) || isNotFound(err)) {
1361
+ handleMatchSpecialError(match, err)
1362
+ }
1363
+ }
1353
1364
 
1354
- // Otherwise, load the route
1355
- matches[index] = match = {
1356
- ...match,
1357
- isFetching: true,
1358
- fetchCount: match.fetchCount + 1,
1365
+ const loaderContext: LoaderFnContext = {
1366
+ params: match.params,
1367
+ deps: match.loaderDeps,
1368
+ preload: !!preload,
1369
+ parentMatchPromise,
1370
+ abortController: match.abortController,
1371
+ context: match.context,
1372
+ location,
1373
+ navigate: (opts) =>
1374
+ this.navigate({ ...opts, from: match.pathname } as any),
1375
+ cause: preload ? 'preload' : match.cause,
1376
+ route,
1359
1377
  }
1360
1378
 
1361
- const lazyPromise =
1362
- route.lazyFn?.().then((lazyRoute) => {
1363
- Object.assign(route.options, lazyRoute.options)
1364
- }) || Promise.resolve()
1365
-
1366
- // If for some reason lazy resolves more lazy components...
1367
- // We'll wait for that before pre attempt to preload any
1368
- // components themselves.
1369
- const componentsPromise = lazyPromise.then(() =>
1370
- Promise.all(
1371
- componentTypes.map(async (type) => {
1372
- const component = route.options[type]
1373
-
1374
- if ((component as any)?.preload) {
1375
- await (component as any).preload()
1376
- }
1377
- }),
1378
- ),
1379
- )
1379
+ const fetch = async () => {
1380
+ const existing = getRouteMatch(this.state, match.id)!
1381
+ let lazyPromise = Promise.resolve()
1382
+ let componentsPromise = Promise.resolve() as Promise<any>
1383
+ let loaderPromise = existing.loaderPromise
1384
+ let loadPromise = existing.loadPromise
1385
+
1386
+ // If the Matches component rendered
1387
+ // the pending component and needs to show it for
1388
+ // a minimum duration, we''ll wait for it to resolve
1389
+ // before committing to the match and resolving
1390
+ // the loadPromise
1391
+ const potentialPendingMinPromise = async () => {
1392
+ const latestMatch = getRouteMatch(this.state, match.id)
1393
+
1394
+ if (latestMatch?.minPendingPromise) {
1395
+ await latestMatch.minPendingPromise
1396
+
1397
+ if ((latestPromise = checkLatest()))
1398
+ return await latestPromise
1399
+
1400
+ updateMatch(latestMatch.id, (prev) => ({
1401
+ ...prev,
1402
+ minPendingPromise: undefined,
1403
+ }))
1404
+ }
1405
+ }
1380
1406
 
1381
- // Kick off the loader!
1382
- const loaderPromise = route.options.loader?.(loaderContext)
1407
+ try {
1408
+ if (!match.isFetching) {
1409
+ // If the user doesn't want the route to reload, just
1410
+ // resolve with the existing loader data
1383
1411
 
1384
- loadPromise = Promise.all([
1385
- componentsPromise,
1386
- loaderPromise,
1387
- lazyPromise,
1388
- ]).then((d) => d[1])
1389
- }
1412
+ // if (match.fetchCount && match.status === 'success') {
1413
+ // resolve()
1414
+ // }
1390
1415
 
1391
- matches[index] = match = {
1392
- ...match,
1393
- loadPromise,
1394
- }
1416
+ // Otherwise, load the route
1417
+ matches[index] = match = {
1418
+ ...match,
1419
+ isFetching: true,
1420
+ fetchCount: match.fetchCount + 1,
1421
+ }
1395
1422
 
1396
- updateMatch(match)
1423
+ lazyPromise =
1424
+ route.lazyFn?.().then((lazyRoute) => {
1425
+ Object.assign(route.options, lazyRoute.options)
1426
+ }) || Promise.resolve()
1427
+
1428
+ // If for some reason lazy resolves more lazy components...
1429
+ // We'll wait for that before pre attempt to preload any
1430
+ // components themselves.
1431
+ componentsPromise = lazyPromise.then(() =>
1432
+ Promise.all(
1433
+ componentTypes.map(async (type) => {
1434
+ const component = route.options[type]
1435
+
1436
+ if ((component as any)?.preload) {
1437
+ await (component as any).preload()
1438
+ }
1439
+ }),
1440
+ ),
1441
+ )
1442
+
1443
+ // Lazy option can modify the route options,
1444
+ // so we need to wait for it to resolve before
1445
+ // we can use the options
1446
+ await lazyPromise
1447
+
1448
+ if ((latestPromise = checkLatest()))
1449
+ return await latestPromise
1450
+
1451
+ // Kick off the loader!
1452
+ loaderPromise = route.options.loader?.(loaderContext)
1453
+
1454
+ const previousResolve = loadPromise.resolve
1455
+ // Create a new one
1456
+ loadPromise = createControlledPromise<void>(
1457
+ // Resolve the old when we we resolve the new one
1458
+ previousResolve,
1459
+ )
1460
+ }
1461
+
1462
+ matches[index] = match = updateMatch(match.id, (prev) => ({
1463
+ ...prev,
1464
+ loaderPromise,
1465
+ loadPromise,
1466
+ }))
1467
+
1468
+ const loaderData = await loaderPromise
1469
+ if ((latestPromise = checkLatest()))
1470
+ return await latestPromise
1471
+
1472
+ handleError(loaderData)
1473
+
1474
+ if ((latestPromise = checkLatest()))
1475
+ return await latestPromise
1476
+
1477
+ await potentialPendingMinPromise()
1478
+ if ((latestPromise = checkLatest()))
1479
+ return await latestPromise
1480
+
1481
+ const meta = route.options.meta?.({
1482
+ params: match.params,
1483
+ loaderData,
1484
+ })
1485
+
1486
+ const headers = route.options.headers?.({
1487
+ loaderData,
1488
+ })
1489
+
1490
+ matches[index] = match = updateMatch(match.id, (prev) => ({
1491
+ ...prev,
1492
+ error: undefined,
1493
+ status: 'success',
1494
+ isFetching: false,
1495
+ updatedAt: Date.now(),
1496
+ loaderData,
1497
+ meta,
1498
+ headers,
1499
+ }))
1500
+ } catch (e) {
1501
+ let error = e
1502
+ if ((latestPromise = checkLatest()))
1503
+ return await latestPromise
1504
+
1505
+ await potentialPendingMinPromise()
1506
+ if ((latestPromise = checkLatest()))
1507
+ return await latestPromise
1508
+
1509
+ handleError(e)
1510
+
1511
+ try {
1512
+ route.options.onError?.(e)
1513
+ } catch (onErrorError) {
1514
+ error = onErrorError
1515
+ handleError(onErrorError)
1516
+ }
1517
+
1518
+ matches[index] = match = updateMatch(match.id, (prev) => ({
1519
+ ...prev,
1520
+ error,
1521
+ status: 'error',
1522
+ isFetching: false,
1523
+ }))
1524
+ }
1397
1525
 
1398
- const loaderData = await loadPromise
1399
- if ((latestPromise = checkLatest())) return await latestPromise
1526
+ // Last but not least, wait for the the component
1527
+ // to be preloaded before we resolve the match
1528
+ await componentsPromise
1400
1529
 
1401
- handleError(loaderData)
1530
+ if ((latestPromise = checkLatest()))
1531
+ return await latestPromise
1402
1532
 
1403
- if (didShowPending && pendingMinMs) {
1404
- await new Promise((r) => setTimeout(r, pendingMinMs))
1405
- }
1533
+ loadPromise.resolve()
1534
+ }
1406
1535
 
1407
- if ((latestPromise = checkLatest())) return await latestPromise
1536
+ // This is where all of the stale-while-revalidate magic happens
1537
+ const age = Date.now() - match.updatedAt
1408
1538
 
1409
- const [meta, headers] = await Promise.all([
1410
- route.options.meta?.({
1411
- params: match.params,
1412
- loaderData,
1413
- }),
1414
- route.options.headers?.({
1415
- loaderData,
1416
- }),
1417
- ])
1418
-
1419
- matches[index] = match = {
1420
- ...match,
1421
- error: undefined,
1422
- status: 'success',
1423
- isFetching: false,
1424
- updatedAt: Date.now(),
1425
- loaderData,
1426
- loadPromise: undefined,
1427
- meta,
1428
- headers,
1429
- }
1430
- } catch (e) {
1431
- let error = e
1432
- if ((latestPromise = checkLatest())) return await latestPromise
1539
+ const staleAge = preload
1540
+ ? route.options.preloadStaleTime ??
1541
+ this.options.defaultPreloadStaleTime ??
1542
+ 30_000 // 30 seconds for preloads by default
1543
+ : route.options.staleTime ??
1544
+ this.options.defaultStaleTime ??
1545
+ 0
1433
1546
 
1434
- handleError(e)
1547
+ const shouldReloadOption = route.options.shouldReload
1435
1548
 
1436
- try {
1437
- route.options.onError?.(e)
1438
- } catch (onErrorError) {
1439
- error = onErrorError
1440
- handleError(onErrorError)
1441
- }
1549
+ // Default to reloading the route all the time
1550
+ // Allow shouldReload to get the last say,
1551
+ // if provided.
1552
+ const shouldReload =
1553
+ typeof shouldReloadOption === 'function'
1554
+ ? shouldReloadOption(loaderContext)
1555
+ : shouldReloadOption
1442
1556
 
1443
- matches[index] = match = {
1444
- ...match,
1445
- error,
1446
- status: 'error',
1447
- isFetching: false,
1448
- }
1449
- }
1557
+ matches[index] = match = {
1558
+ ...match,
1559
+ preload:
1560
+ !!preload &&
1561
+ !this.state.matches.find((d) => d.id === match.id),
1562
+ }
1450
1563
 
1451
- updateMatch(match)
1452
- }
1564
+ const fetchWithRedirect = async () => {
1565
+ try {
1566
+ await fetch()
1567
+ } catch (err) {
1568
+ if ((latestPromise = checkLatest()))
1569
+ return await latestPromise
1453
1570
 
1454
- // This is where all of the stale-while-revalidate magic happens
1455
- const age = Date.now() - match.updatedAt
1456
-
1457
- const staleAge = preload
1458
- ? route.options.preloadStaleTime ??
1459
- this.options.defaultPreloadStaleTime ??
1460
- 30_000 // 30 seconds for preloads by default
1461
- : route.options.staleTime ?? this.options.defaultStaleTime ?? 0
1462
-
1463
- const shouldReloadOption = route.options.shouldReload
1464
-
1465
- // Default to reloading the route all the time
1466
- // Allow shouldReload to get the last say,
1467
- // if provided.
1468
- const shouldReload =
1469
- typeof shouldReloadOption === 'function'
1470
- ? shouldReloadOption(loaderContext)
1471
- : shouldReloadOption
1472
-
1473
- matches[index] = match = {
1474
- ...match,
1475
- preload:
1476
- !!preload && !this.state.matches.find((d) => d.id === match.id),
1477
- }
1571
+ if (isRedirect(err)) {
1572
+ const redirect = this.resolveRedirect(err)
1478
1573
 
1479
- // If the route is successful and still fresh, just resolve
1480
- if (
1481
- match.status === 'success' &&
1482
- (match.invalid || (shouldReload ?? age > staleAge))
1483
- ) {
1484
- ;(async () => {
1485
- try {
1486
- await fetch()
1487
- } catch (err) {
1488
- console.info('Background Fetching Error', err)
1489
-
1490
- if (isRedirect(err)) {
1491
- const isActive = (
1492
- this.state.pendingMatches || this.state.matches
1493
- ).find((d) => d.id === match.id)
1494
-
1495
- // Redirects should not be persisted
1496
- handleError(err)
1497
-
1498
- // If the route is still active, redirect
1499
- // TODO: Do we really need this?
1500
- invariant(
1501
- false,
1502
- 'You need to redirect from a background fetch? This is not supported yet. File an issue.',
1503
- )
1504
- // if (isActive) {
1505
- // this.handleRedirect(err)
1506
- // }
1507
- }
1508
- }
1509
- })()
1574
+ if (!preload && !this.isServer) {
1575
+ this.navigate({ ...(redirect as any), replace: true })
1576
+ }
1510
1577
 
1511
- return resolve()
1512
- }
1578
+ throw redirect
1579
+ } else if (isNotFound(err)) {
1580
+ if (!preload) this.handleNotFound(matches, err)
1581
+ throw err
1582
+ }
1513
1583
 
1514
- const shouldPending =
1515
- !preload &&
1516
- route.options.loader &&
1517
- typeof pendingMs === 'number' &&
1518
- (route.options.pendingComponent ??
1519
- this.options.defaultPendingComponent)
1520
-
1521
- if (match.status !== 'success') {
1522
- try {
1523
- if (shouldPending) {
1524
- match.pendingPromise?.then(async () => {
1525
- if ((latestPromise = checkLatest())) return latestPromise
1526
-
1527
- didShowPending = true
1528
- matches[index] = match = {
1529
- ...match,
1530
- showPending: true,
1584
+ handleError(err)
1531
1585
  }
1586
+ }
1532
1587
 
1533
- updateMatch(match)
1534
- resolve()
1535
- })
1536
- }
1588
+ // If the route is successful and still fresh, just resolve
1589
+ if (
1590
+ match.status === 'success' &&
1591
+ (match.invalid || (shouldReload ?? age > staleAge))
1592
+ ) {
1593
+ fetchWithRedirect()
1594
+ return
1595
+ }
1537
1596
 
1538
- await fetch()
1539
- } catch (err) {
1540
- reject(err)
1541
- }
1542
- }
1597
+ if (match.status !== 'success') {
1598
+ await fetchWithRedirect()
1599
+ }
1600
+ }),
1601
+ )
1543
1602
 
1544
- resolve()
1545
- }),
1546
- )
1547
- })
1603
+ if ((latestPromise = checkLatest())) return await latestPromise
1548
1604
 
1549
- await Promise.all(matchPromises)
1605
+ resolveAll()
1606
+ } catch (err) {
1607
+ rejectAll(err)
1608
+ }
1609
+ })()
1610
+ })
1611
+ } catch (err) {
1612
+ if (isRedirect(err) || isNotFound(err)) {
1613
+ throw err
1614
+ }
1615
+ }
1550
1616
 
1551
1617
  return matches
1552
1618
  }
@@ -1569,68 +1635,74 @@ export class Router<
1569
1635
  }
1570
1636
 
1571
1637
  load = async (): Promise<void> => {
1572
- // eslint-disable-next-line no-async-promise-executor
1573
- const promise = new Promise<void>(async (resolve, reject) => {
1574
- const next = this.latestLocation
1575
- const prevLocation = this.state.resolvedLocation
1576
- const pathDidChange = prevLocation.href !== next.href
1577
- let latestPromise: Promise<void> | undefined | null
1578
-
1579
- // Cancel any pending matches
1580
- this.cancelMatches()
1581
-
1582
- this.emit({
1583
- type: 'onBeforeLoad',
1584
- fromLocation: prevLocation,
1585
- toLocation: next,
1586
- pathChanged: pathDidChange,
1587
- })
1638
+ let resolveLoad!: (value: void) => void
1639
+ let rejectLoad!: (reason: any) => void
1588
1640
 
1589
- let pendingMatches!: Array<RouteMatch<any, any>>
1590
- const previousMatches = this.state.matches
1641
+ const promise = new Promise<void>((resolve, reject) => {
1642
+ resolveLoad = resolve
1643
+ rejectLoad = reject
1644
+ })
1645
+
1646
+ this.latestLoadPromise = promise
1647
+
1648
+ let latestPromise: Promise<void> | undefined | null
1649
+ ;(async () => {
1650
+ try {
1651
+ const next = this.latestLocation
1652
+ const prevLocation = this.state.resolvedLocation
1653
+ const pathDidChange = prevLocation.href !== next.href
1591
1654
 
1592
- this.__store.batch(() => {
1593
- this.cleanCache()
1655
+ // Cancel any pending matches
1656
+ this.cancelMatches()
1594
1657
 
1595
- // Match the routes
1596
- pendingMatches = this.matchRoutes(next.pathname, next.search, {
1597
- debug: true,
1658
+ this.emit({
1659
+ type: 'onBeforeLoad',
1660
+ fromLocation: prevLocation,
1661
+ toLocation: next,
1662
+ pathChanged: pathDidChange,
1598
1663
  })
1599
1664
 
1600
- // Ingest the new matches
1601
- // If a cached moved to pendingMatches, remove it from cachedMatches
1602
- this.__store.setState((s) => ({
1603
- ...s,
1604
- isLoading: true,
1605
- location: next,
1606
- pendingMatches,
1607
- cachedMatches: s.cachedMatches.filter((d) => {
1608
- return !pendingMatches.find((e) => e.id === d.id)
1609
- }),
1610
- }))
1611
- })
1665
+ let pendingMatches!: Array<RouteMatch<any, any>>
1666
+ const previousMatches = this.state.matches
1667
+
1668
+ this.__store.batch(() => {
1669
+ this.cleanCache()
1670
+
1671
+ // Match the routes
1672
+ pendingMatches = this.matchRoutes(next.pathname, next.search)
1673
+
1674
+ // Ingest the new matches
1675
+ // If a cached moved to pendingMatches, remove it from cachedMatches
1676
+ this.__store.setState((s) => ({
1677
+ ...s,
1678
+ isLoading: true,
1679
+ location: next,
1680
+ pendingMatches,
1681
+ cachedMatches: s.cachedMatches.filter((d) => {
1682
+ return !pendingMatches.find((e) => e.id === d.id)
1683
+ }),
1684
+ }))
1685
+ })
1612
1686
 
1613
- try {
1614
1687
  let redirect: ResolvedRedirect | undefined
1615
1688
  let notFound: NotFoundError | undefined
1616
1689
 
1617
1690
  try {
1618
1691
  // Load the matches
1619
- await this.loadMatches({
1692
+ const loadMatchesPromise = this.loadMatches({
1620
1693
  matches: pendingMatches,
1621
1694
  location: next,
1622
1695
  checkLatest: () => this.checkLatest(promise),
1623
1696
  })
1697
+
1698
+ if (previousMatches.length || this.isServer) {
1699
+ await loadMatchesPromise
1700
+ }
1624
1701
  } catch (err) {
1625
1702
  if (isRedirect(err)) {
1626
- redirect = this.resolveRedirect(err)
1627
-
1628
- if (!isServer) {
1629
- this.navigate({ ...(redirect as any), replace: true })
1630
- }
1703
+ redirect = err as ResolvedRedirect
1631
1704
  } else if (isNotFound(err)) {
1632
1705
  notFound = err
1633
- this.handleNotFound(pendingMatches, err)
1634
1706
  }
1635
1707
 
1636
1708
  // Swallow all other errors that happen inside
@@ -1696,20 +1768,18 @@ export class Router<
1696
1768
  pathChanged: pathDidChange,
1697
1769
  })
1698
1770
 
1699
- resolve()
1771
+ resolveLoad()
1700
1772
  } catch (err) {
1701
1773
  // Only apply the latest transition
1702
1774
  if ((latestPromise = this.checkLatest(promise))) {
1703
1775
  return latestPromise
1704
1776
  }
1705
1777
 
1706
- console.log('Load Error', err)
1778
+ console.error('Load Error', err)
1707
1779
 
1708
- reject(err)
1780
+ rejectLoad(err)
1709
1781
  }
1710
- })
1711
-
1712
- this.latestLoadPromise = promise
1782
+ })()
1713
1783
 
1714
1784
  return this.latestLoadPromise
1715
1785
  }
@@ -1812,7 +1882,11 @@ export class Router<
1812
1882
  return matches
1813
1883
  } catch (err) {
1814
1884
  if (isRedirect(err)) {
1815
- return await this.preloadRoute(err as any)
1885
+ return await this.preloadRoute({
1886
+ _fromDest: next,
1887
+ from: next.pathname,
1888
+ ...(err as any),
1889
+ })
1816
1890
  }
1817
1891
  // Preload errors are not fatal, but we should still log them
1818
1892
  console.error(err)
@@ -2012,7 +2086,6 @@ export class Router<
2012
2086
  return {
2013
2087
  ...s,
2014
2088
  matches: matches as any,
2015
- lastUpdated: Date.now(),
2016
2089
  }
2017
2090
  })
2018
2091
  }
@@ -2099,7 +2172,6 @@ export function getInitialRouterState(
2099
2172
  matches: [],
2100
2173
  pendingMatches: [],
2101
2174
  cachedMatches: [],
2102
- lastUpdated: 0,
2103
2175
  statusCode: 200,
2104
2176
  }
2105
2177
  }