@tanstack/react-router 1.16.2 → 1.16.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/src/router.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  last,
39
39
  pick,
40
40
  Timeout,
41
+ isServer,
41
42
  } from './utils'
42
43
  import { RouteComponent } from './route'
43
44
  import { AnyRouteMatch, MatchRouteOptions, RouteMatch } from './Matches'
@@ -63,19 +64,12 @@ import {
63
64
  trimPathRight,
64
65
  } from './path'
65
66
  import invariant from 'tiny-invariant'
66
- import { isRedirect } from './redirects'
67
+ import { AnyRedirect, isRedirect } from './redirects'
67
68
  import { NotFoundError, isNotFound } from './not-found'
68
69
  import { ResolveRelativePath, ToOptions } from './link'
69
70
  import { NoInfer } from '@tanstack/react-store'
70
71
  import warning from 'tiny-warning'
71
- import {
72
- DeferredPromise,
73
- DeferredPromiseState,
74
- defaultDeserializeError,
75
- isDehydratedDeferred,
76
- isServerSideError,
77
- } from '.'
78
- // import warning from 'tiny-warning'
72
+ import { DeferredPromiseState } from '.'
79
73
 
80
74
  //
81
75
 
@@ -1135,108 +1129,96 @@ export class Router<
1135
1129
  }
1136
1130
 
1137
1131
  // Check each match middleware to see if the route can be accessed
1138
- try {
1139
- for (let [index, match] of matches.entries()) {
1140
- const parentMatch = matches[index - 1]
1141
- const route = this.looseRoutesById[match.routeId]!
1142
- const abortController = new AbortController()
1143
-
1144
- const handleErrorAndRedirect = (err: any, code: string) => {
1145
- err.routerCode = code
1146
- firstBadMatchIndex = firstBadMatchIndex ?? index
1132
+ for (let [index, match] of matches.entries()) {
1133
+ const parentMatch = matches[index - 1]
1134
+ const route = this.looseRoutesById[match.routeId]!
1135
+ const abortController = new AbortController()
1147
1136
 
1148
- if (isRedirect(err)) {
1149
- throw err
1150
- }
1137
+ const handleError = (err: any, code: string) => {
1138
+ err.routerCode = code
1139
+ firstBadMatchIndex = firstBadMatchIndex ?? index
1151
1140
 
1152
- if (isNotFound(err)) {
1153
- this.updateMatchesWithNotFound(matches, match, err)
1154
- }
1141
+ if (isRedirect(err)) {
1142
+ throw err
1143
+ }
1155
1144
 
1156
- try {
1157
- route.options.onError?.(err)
1158
- } catch (errorHandlerErr) {
1159
- err = errorHandlerErr
1145
+ if (isNotFound(err)) {
1146
+ err.routeId = match.routeId
1147
+ throw err
1148
+ }
1160
1149
 
1161
- if (isRedirect(errorHandlerErr)) {
1162
- throw errorHandlerErr
1163
- }
1164
- }
1150
+ try {
1151
+ route.options.onError?.(err)
1152
+ } catch (errorHandlerErr) {
1153
+ err = errorHandlerErr
1165
1154
 
1166
- matches[index] = match = {
1167
- ...match,
1168
- error: err,
1169
- status: 'error',
1170
- updatedAt: Date.now(),
1171
- abortController: new AbortController(),
1155
+ if (isRedirect(errorHandlerErr)) {
1156
+ throw errorHandlerErr
1172
1157
  }
1173
1158
  }
1174
1159
 
1175
- try {
1176
- if (match.paramsError) {
1177
- handleErrorAndRedirect(match.paramsError, 'PARSE_PARAMS')
1178
- }
1160
+ matches[index] = match = {
1161
+ ...match,
1162
+ error: err,
1163
+ status: 'error',
1164
+ updatedAt: Date.now(),
1165
+ abortController: new AbortController(),
1166
+ }
1167
+ }
1179
1168
 
1180
- if (match.searchError) {
1181
- handleErrorAndRedirect(match.searchError, 'VALIDATE_SEARCH')
1182
- }
1169
+ try {
1170
+ if (match.paramsError) {
1171
+ handleError(match.paramsError, 'PARSE_PARAMS')
1172
+ }
1183
1173
 
1184
- const parentContext =
1185
- parentMatch?.context ?? this.options.context ?? {}
1174
+ if (match.searchError) {
1175
+ handleError(match.searchError, 'VALIDATE_SEARCH')
1176
+ }
1186
1177
 
1187
- const pendingMs =
1188
- route.options.pendingMs ?? this.options.defaultPendingMs
1189
- const pendingPromise =
1190
- typeof pendingMs === 'number' && pendingMs <= 0
1191
- ? Promise.resolve()
1192
- : new Promise<void>((r) => setTimeout(r, pendingMs))
1193
-
1194
- const beforeLoadContext =
1195
- (await route.options.beforeLoad?.({
1196
- search: match.search,
1197
- abortController,
1198
- params: match.params,
1199
- preload: !!preload,
1200
- context: parentContext,
1201
- location: this.state.location,
1202
- // TOOD: just expose state and router, etc
1203
- navigate: (opts) =>
1204
- this.navigate({ ...opts, from: match.pathname } as any),
1205
- buildLocation: this.buildLocation,
1206
- cause: preload ? 'preload' : match.cause,
1207
- })) ?? ({} as any)
1208
-
1209
- if (isRedirect(beforeLoadContext)) {
1210
- throw beforeLoadContext
1211
- }
1178
+ const parentContext = parentMatch?.context ?? this.options.context ?? {}
1212
1179
 
1213
- const context = {
1214
- ...parentContext,
1215
- ...beforeLoadContext,
1216
- }
1180
+ const pendingMs =
1181
+ route.options.pendingMs ?? this.options.defaultPendingMs
1182
+ const pendingPromise =
1183
+ typeof pendingMs === 'number' && pendingMs <= 0
1184
+ ? Promise.resolve()
1185
+ : new Promise<void>((r) => setTimeout(r, pendingMs))
1217
1186
 
1218
- matches[index] = match = {
1219
- ...match,
1220
- routeContext: replaceEqualDeep(
1221
- match.routeContext,
1222
- beforeLoadContext,
1223
- ),
1224
- context: replaceEqualDeep(match.context, context),
1187
+ const beforeLoadContext =
1188
+ (await route.options.beforeLoad?.({
1189
+ search: match.search,
1225
1190
  abortController,
1226
- pendingPromise,
1227
- }
1228
- } catch (err) {
1229
- handleErrorAndRedirect(err, 'BEFORE_LOAD')
1230
- break
1191
+ params: match.params,
1192
+ preload: !!preload,
1193
+ context: parentContext,
1194
+ location: this.state.location,
1195
+ // TOOD: just expose state and router, etc
1196
+ navigate: (opts) =>
1197
+ this.navigate({ ...opts, from: match.pathname } as any),
1198
+ buildLocation: this.buildLocation,
1199
+ cause: preload ? 'preload' : match.cause,
1200
+ })) ?? ({} as any)
1201
+
1202
+ if (isRedirect(beforeLoadContext)) {
1203
+ throw beforeLoadContext
1231
1204
  }
1232
- }
1233
- } catch (err) {
1234
- if (isRedirect(err)) {
1235
- if (!preload) this.navigate(err as any)
1236
- return matches
1237
- }
1238
1205
 
1239
- throw err
1206
+ const context = {
1207
+ ...parentContext,
1208
+ ...beforeLoadContext,
1209
+ }
1210
+
1211
+ matches[index] = match = {
1212
+ ...match,
1213
+ routeContext: replaceEqualDeep(match.routeContext, beforeLoadContext),
1214
+ context: replaceEqualDeep(match.context, context),
1215
+ abortController,
1216
+ pendingPromise,
1217
+ }
1218
+ } catch (err) {
1219
+ handleError(err, 'BEFORE_LOAD')
1220
+ break
1221
+ }
1240
1222
  }
1241
1223
 
1242
1224
  const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
@@ -1244,26 +1226,19 @@ export class Router<
1244
1226
 
1245
1227
  validResolvedMatches.forEach((match, index) => {
1246
1228
  matchPromises.push(
1247
- new Promise<void>(async (resolve) => {
1229
+ new Promise<void>(async (resolve, reject) => {
1248
1230
  const parentMatchPromise = matchPromises[index - 1]
1249
1231
  const route = this.looseRoutesById[match.routeId]!
1250
1232
 
1251
- const handleErrorAndRedirect = (err: any) => {
1233
+ const handleError = (err: any) => {
1252
1234
  if (isRedirect(err)) {
1253
- if (!preload) {
1254
- this.navigate(err as any)
1255
- }
1256
- return true
1235
+ throw err
1257
1236
  }
1258
1237
 
1259
1238
  if (isNotFound(err)) {
1260
- if (!preload) {
1261
- this.updateMatchesWithNotFound(matches, match, err)
1262
- }
1263
- return true
1239
+ err.routeId = match.routeId
1240
+ throw err
1264
1241
  }
1265
-
1266
- return false
1267
1242
  }
1268
1243
 
1269
1244
  let loadPromise: Promise<void> | undefined
@@ -1360,9 +1335,7 @@ export class Router<
1360
1335
  const loaderData = await loadPromise
1361
1336
  if ((latestPromise = checkLatest())) return await latestPromise
1362
1337
 
1363
- if (isRedirect(loaderData) || isNotFound(loaderData)) {
1364
- if (handleErrorAndRedirect(loaderData)) return
1365
- }
1338
+ handleError(loaderData)
1366
1339
 
1367
1340
  if (didShowPending && pendingMinMs) {
1368
1341
  await new Promise((r) => setTimeout(r, pendingMinMs))
@@ -1372,6 +1345,7 @@ export class Router<
1372
1345
 
1373
1346
  const [meta, headers] = await Promise.all([
1374
1347
  route.options.meta?.({
1348
+ params: match.params,
1375
1349
  loaderData,
1376
1350
  }),
1377
1351
  route.options.headers?.({
@@ -1392,13 +1366,13 @@ export class Router<
1392
1366
  }
1393
1367
  } catch (error) {
1394
1368
  if ((latestPromise = checkLatest())) return await latestPromise
1395
- if (handleErrorAndRedirect(error)) return
1369
+ handleError(error)
1396
1370
 
1397
1371
  try {
1398
1372
  route.options.onError?.(error)
1399
1373
  } catch (onErrorError) {
1400
1374
  error = onErrorError
1401
- if (handleErrorAndRedirect(onErrorError)) return
1375
+ handleError(onErrorError)
1402
1376
  }
1403
1377
 
1404
1378
  matches[index] = match = {
@@ -1439,29 +1413,33 @@ export class Router<
1439
1413
  !!preload && !this.state.matches.find((d) => d.id === match.id),
1440
1414
  }
1441
1415
 
1442
- if (match.status !== 'success') {
1443
- // If we need to potentially show the pending component,
1444
- // start a timer to show it after the pendingMs
1445
- if (shouldPending) {
1446
- match.pendingPromise?.then(async () => {
1447
- if ((latestPromise = checkLatest())) return latestPromise
1448
-
1449
- didShowPending = true
1450
- matches[index] = match = {
1451
- ...match,
1452
- showPending: true,
1453
- }
1416
+ try {
1417
+ if (match.status !== 'success') {
1418
+ // If we need to potentially show the pending component,
1419
+ // start a timer to show it after the pendingMs
1420
+ if (shouldPending) {
1421
+ match.pendingPromise?.then(async () => {
1422
+ if ((latestPromise = checkLatest())) return latestPromise
1423
+
1424
+ didShowPending = true
1425
+ matches[index] = match = {
1426
+ ...match,
1427
+ showPending: true,
1428
+ }
1429
+
1430
+ updateMatch(match)
1431
+ resolve()
1432
+ })
1433
+ }
1454
1434
 
1455
- updateMatch(match)
1456
- resolve()
1457
- })
1435
+ // Critical Fetching, we need to await
1436
+ await fetch()
1437
+ } else if (match.invalid || (shouldReload ?? age > staleAge)) {
1438
+ // Background Fetching, no need to wait
1439
+ fetch()
1458
1440
  }
1459
-
1460
- // Critical Fetching, we need to await
1461
- await fetch()
1462
- } else if (match.invalid || (shouldReload ?? age > staleAge)) {
1463
- // Background Fetching, no need to wait
1464
- fetch()
1441
+ } catch (err) {
1442
+ reject(err)
1465
1443
  }
1466
1444
 
1467
1445
  resolve()
@@ -1470,6 +1448,7 @@ export class Router<
1470
1448
  })
1471
1449
 
1472
1450
  await Promise.all(matchPromises)
1451
+
1473
1452
  return matches
1474
1453
  }
1475
1454
 
@@ -1538,8 +1517,11 @@ export class Router<
1538
1517
  checkLatest: () => this.checkLatest(promise),
1539
1518
  })
1540
1519
  } catch (err) {
1541
- // swallow this error, since we'll display the
1542
- // errors on the route components
1520
+ if (isRedirect(err)) {
1521
+ this.handleRedirect(err)
1522
+ } else if (isNotFound(err)) {
1523
+ this.handleNotFound(pendingMatches, err)
1524
+ }
1543
1525
  }
1544
1526
 
1545
1527
  // Only apply the latest transition
@@ -1609,6 +1591,18 @@ export class Router<
1609
1591
  return this.latestLoadPromise
1610
1592
  }
1611
1593
 
1594
+ handleRedirect = (err: AnyRedirect) => {
1595
+ if (!err.href) {
1596
+ err.href = this.buildLocation(err as any).href
1597
+ }
1598
+
1599
+ if (isServer) {
1600
+ throw err
1601
+ }
1602
+
1603
+ this.navigate(err as any)
1604
+ }
1605
+
1612
1606
  cleanCache = () => {
1613
1607
  // This is where all of the garbage collection magic happens
1614
1608
  this.__store.setState((s) => {
@@ -1663,13 +1657,22 @@ export class Router<
1663
1657
  })
1664
1658
  })
1665
1659
 
1666
- matches = await this.loadMatches({
1667
- matches,
1668
- preload: true,
1669
- checkLatest: () => undefined,
1670
- })
1660
+ try {
1661
+ matches = await this.loadMatches({
1662
+ matches,
1663
+ preload: true,
1664
+ checkLatest: () => undefined,
1665
+ })
1671
1666
 
1672
- return matches
1667
+ return matches
1668
+ } catch (err) {
1669
+ // Preload errors are not fatal, but we should still log them
1670
+ if (!isRedirect(err) && !isNotFound(err)) {
1671
+ console.error(err)
1672
+ }
1673
+
1674
+ return undefined
1675
+ }
1673
1676
  }
1674
1677
 
1675
1678
  matchRoute = <
@@ -1856,6 +1859,7 @@ export class Router<
1856
1859
  ...match,
1857
1860
  ...dehydratedMatch,
1858
1861
  meta: route.options.meta?.({
1862
+ params: match.params,
1859
1863
  loaderData: dehydratedMatch.loaderData,
1860
1864
  }),
1861
1865
  links: route.options.links?.(),
@@ -1875,39 +1879,38 @@ export class Router<
1875
1879
  }
1876
1880
 
1877
1881
  // Finds a match that has a notFoundComponent
1878
- updateMatchesWithNotFound = (
1879
- matches: AnyRouteMatch[],
1880
- currentMatch: AnyRouteMatch,
1881
- err: NotFoundError,
1882
- ) => {
1882
+ handleNotFound = (matches: AnyRouteMatch[], err: NotFoundError) => {
1883
1883
  const matchesByRouteId = Object.fromEntries(
1884
1884
  matches.map((match) => [match.routeId, match]),
1885
1885
  ) as Record<string, AnyRouteMatch>
1886
1886
 
1887
- if (err.global) {
1888
- matchesByRouteId[rootRouteId]!.notFoundError = err
1889
- } else {
1887
+ if (!err.global && err.routeId) {
1890
1888
  // If the err contains a routeId, start searching up from that route
1891
- let currentRoute = (this.routesById as any)[
1892
- err.route ?? currentMatch.routeId
1893
- ] as AnyRoute
1889
+ let currentRoute = this.looseRoutesById[err.routeId]
1894
1890
 
1895
- // Go up the tree until we find a route with a notFoundComponent
1896
- while (!currentRoute.options.notFoundComponent) {
1897
- currentRoute = currentRoute?.parentRoute
1891
+ if (currentRoute) {
1892
+ // Go up the tree until we find a route with a notFoundComponent
1893
+ while (!currentRoute.options.notFoundComponent) {
1894
+ currentRoute = currentRoute?.parentRoute
1898
1895
 
1899
- invariant(
1900
- currentRoute,
1901
- 'Found invalid route tree while trying to find not-found handler.',
1902
- )
1896
+ invariant(
1897
+ currentRoute,
1898
+ 'Found invalid route tree while trying to find not-found handler.',
1899
+ )
1903
1900
 
1904
- if (currentRoute.id === rootRouteId) break
1905
- }
1901
+ if (currentRoute.id === rootRouteId) break
1902
+ }
1903
+
1904
+ const match = matchesByRouteId[currentRoute.id]
1905
+ invariant(match, 'Could not find match for route: ' + currentRoute.id)
1906
+ match.notFoundError = err
1906
1907
 
1907
- const match = matchesByRouteId[currentRoute.id]
1908
- invariant(match, 'Could not find match for route: ' + currentRoute.id)
1909
- match.notFoundError = err
1908
+ return
1909
+ }
1910
1910
  }
1911
+
1912
+ // Otherwise, just set the notFoundError on the root route
1913
+ matchesByRouteId[rootRouteId]!.notFoundError = err
1911
1914
  }
1912
1915
 
1913
1916
  hasNotFoundMatch = () => {