@tanstack/react-router 0.0.1-beta.279 → 0.0.1-beta.280

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.279",
4
+ "version": "0.0.1-beta.280",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -44,7 +44,7 @@
44
44
  "@tanstack/store": "^0.1.3",
45
45
  "tiny-invariant": "^1.3.1",
46
46
  "tiny-warning": "^1.0.3",
47
- "@tanstack/history": "0.0.1-beta.279"
47
+ "@tanstack/history": "0.0.1-beta.280"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "rollup --config rollup.config.js"
package/src/Matches.tsx CHANGED
@@ -40,7 +40,6 @@ export interface RouteMatch<
40
40
  search: FullSearchSchema<TRouteTree> &
41
41
  RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
42
42
  fetchCount: number
43
- shouldReloadDeps: any
44
43
  abortController: AbortController
45
44
  cause: 'preload' | 'enter' | 'stay'
46
45
  loaderDeps: RouteById<TRouteTree, TRouteId>['types']['loaderDeps']
@@ -187,7 +187,6 @@ function Transitioner() {
187
187
  }
188
188
  }
189
189
  }
190
- router.pendingMatches = []
191
190
 
192
191
  router.__store.setState((s) => ({
193
192
  ...s,
@@ -217,7 +216,7 @@ export function getRouteMatch<TRouteTree extends AnyRoute>(
217
216
  id: string,
218
217
  ): undefined | RouteMatch<TRouteTree> {
219
218
  return [
220
- ...state.preloadMatches,
219
+ ...state.cachedMatches,
221
220
  ...(state.pendingMatches ?? []),
222
221
  ...state.matches,
223
222
  ].find((d) => d.id === id)
package/src/link.tsx CHANGED
@@ -433,7 +433,7 @@ export function useLinkProps<
433
433
  : true
434
434
  const searchTest =
435
435
  activeOptions?.includeSearch ?? true
436
- ? deepEqual(s.location.search, next.search, true)
436
+ ? deepEqual(s.location.search, next.search, !activeOptions?.exact)
437
437
  : true
438
438
 
439
439
  // The final "active" test
package/src/route.ts CHANGED
@@ -99,16 +99,6 @@ export type BaseRouteOptions<
99
99
  > = RoutePathOptions<TCustomId, TPath> & {
100
100
  getParentRoute: () => TParentRoute
101
101
  validateSearch?: SearchSchemaValidator<TSearchSchema>
102
- shouldReload?:
103
- | boolean
104
- | ((
105
- match: LoaderFnContext<
106
- TAllParams,
107
- TFullSearchSchema,
108
- TAllContext,
109
- TRouteContext
110
- >,
111
- ) => any)
112
102
  } & (keyof PickRequired<RouteContext> extends never
113
103
  ? // This async function is called before a route is loaded.
114
104
  // If an error is thrown here, the route's loader will not be called.
@@ -187,6 +177,10 @@ export type UpdatableRouteOptions<
187
177
  pendingComponent?: RouteComponent
188
178
  pendingMs?: number
189
179
  pendingMinMs?: number
180
+ staleTime?: number
181
+ gcTime?: number
182
+ preloadStaleTime?: number
183
+ preloadGcTime?: number
190
184
  // Filter functions that can manipulate search params *before* they are passed to links and navigate
191
185
  // calls that match this route.
192
186
  preSearchFilters?: SearchFilter<TFullSearchSchema>[]
package/src/router.ts CHANGED
@@ -111,7 +111,10 @@ export interface RouterOptions<
111
111
  defaultPendingComponent?: RouteComponent
112
112
  defaultPendingMs?: number
113
113
  defaultPendingMinMs?: number
114
- defaultPreloadMaxAge?: number
114
+ defaultStaleTime?: number
115
+ defaultPreloadStaleTime?: number
116
+ defaultPreloadGcTime?: number
117
+ defaultGcTime?: number
115
118
  caseSensitive?: boolean
116
119
  routeTree?: TRouteTree
117
120
  basepath?: string
@@ -131,7 +134,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
131
134
  isTransitioning: boolean
132
135
  matches: RouteMatch<TRouteTree>[]
133
136
  pendingMatches?: RouteMatch<TRouteTree>[]
134
- preloadMatches: RouteMatch<TRouteTree>[]
137
+ cachedMatches: RouteMatch<TRouteTree>[]
135
138
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
136
139
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
137
140
  lastUpdated: number
@@ -221,7 +224,6 @@ export class Router<
221
224
  navigateTimeout: Timeout | null = null
222
225
  latestLoadPromise: Promise<void> = Promise.resolve()
223
226
  subscribers = new Set<RouterListener<RouterEvent>>()
224
- pendingMatches: AnyRouteMatch[] = []
225
227
  injectedHtml: InjectedHtmlEntry[] = []
226
228
  dehydratedData?: TDehydrated
227
229
 
@@ -663,7 +665,6 @@ export class Router<
663
665
  routeContext: undefined!,
664
666
  context: undefined!,
665
667
  abortController: new AbortController(),
666
- shouldReloadDeps: undefined,
667
668
  fetchCount: 0,
668
669
  cause,
669
670
  loaderDeps,
@@ -984,15 +985,18 @@ export class Router<
984
985
  let firstBadMatchIndex: number | undefined
985
986
 
986
987
  const updateMatch = (match: AnyRouteMatch) => {
987
- // const isPreload = this.state.preloadMatches.find((d) => d.id === match.id)
988
+ // const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
988
989
  const isPending = this.state.pendingMatches?.find(
989
990
  (d) => d.id === match.id,
990
991
  )
991
- const matchesKey = preload
992
- ? 'preloadMatches'
993
- : isPending
994
- ? 'pendingMatches'
995
- : 'matches'
992
+
993
+ const isMatched = this.state.matches.find((d) => d.id === match.id)
994
+
995
+ const matchesKey = isPending
996
+ ? 'pendingMatches'
997
+ : isMatched
998
+ ? 'matches'
999
+ : 'cachedMatches'
996
1000
 
997
1001
  this.__store.setState((s) => ({
998
1002
  ...s,
@@ -1149,89 +1153,33 @@ export class Router<
1149
1153
  cause: preload ? 'preload' : match.cause,
1150
1154
  }
1151
1155
 
1152
- // Default to reloading the route all the time
1153
- let shouldLoad = true
1154
-
1155
- const shouldReloadFn = route.options.shouldReload
1156
-
1157
- let shouldReloadDeps =
1158
- typeof shouldReloadFn === 'function'
1159
- ? shouldReloadFn(loaderContext)
1160
- : !!(shouldReloadFn ?? true)
1161
-
1162
- const compareDeps = () => {
1163
- if (typeof shouldReloadDeps === 'object') {
1164
- // compare the deps to see if they've changed
1165
- shouldLoad = !deepEqual(
1166
- shouldReloadDeps,
1167
- match.shouldReloadDeps,
1168
- )
1169
- } else {
1170
- shouldLoad = !!shouldReloadDeps
1171
- }
1172
- }
1173
-
1174
- // If it's the first preload, or the route is entering, or we're
1175
- // invalidating, we definitely need to load the route
1176
- if (invalidate) {
1177
- // Change nothing, we need to load the route
1178
- } else if (preload) {
1179
- if (!match.fetchCount) {
1180
- // Change nothing, we need to preload the route
1181
- } else {
1182
- compareDeps()
1183
- }
1184
- } else if (match.cause === 'enter') {
1185
- if (!match.fetchCount) {
1186
- // Change nothing, we 100% need to load the route
1187
- } else {
1188
- compareDeps()
1189
- }
1190
- } else {
1191
- compareDeps()
1156
+ if (match.fetchCount && match.status === 'success') {
1157
+ resolve()
1192
1158
  }
1193
1159
 
1194
- if (typeof shouldReloadDeps === 'object') {
1195
- matches[index] = match = {
1196
- ...match,
1197
- shouldReloadDeps,
1198
- }
1160
+ // Otherwise, load the route
1161
+ matches[index] = match = {
1162
+ ...match,
1163
+ isFetching: true,
1164
+ fetchCount: match.fetchCount + 1,
1199
1165
  }
1200
1166
 
1201
- // If the user doesn't want the route to reload, just
1202
- // resolve with the existing loader data
1203
-
1204
- if (!shouldLoad) {
1205
- loadPromise = Promise.resolve(match.loaderData)
1206
- } else {
1207
- if (match.fetchCount && match.status === 'success') {
1208
- resolve()
1209
- }
1210
-
1211
- // Otherwise, load the route
1212
- matches[index] = match = {
1213
- ...match,
1214
- isFetching: true,
1215
- fetchCount: match.fetchCount + 1,
1216
- }
1217
-
1218
- const componentsPromise = Promise.all(
1219
- componentTypes.map(async (type) => {
1220
- const component = route.options[type]
1167
+ const componentsPromise = Promise.all(
1168
+ componentTypes.map(async (type) => {
1169
+ const component = route.options[type]
1221
1170
 
1222
- if ((component as any)?.preload) {
1223
- await (component as any).preload()
1224
- }
1225
- }),
1226
- )
1171
+ if ((component as any)?.preload) {
1172
+ await (component as any).preload()
1173
+ }
1174
+ }),
1175
+ )
1227
1176
 
1228
- const loaderPromise = route.options.loader?.(loaderContext)
1177
+ const loaderPromise = route.options.loader?.(loaderContext)
1229
1178
 
1230
- loadPromise = Promise.all([
1231
- componentsPromise,
1232
- loaderPromise,
1233
- ]).then((d) => d[1])
1234
- }
1179
+ loadPromise = Promise.all([
1180
+ componentsPromise,
1181
+ loaderPromise,
1182
+ ]).then((d) => d[1])
1235
1183
  }
1236
1184
 
1237
1185
  matches[index] = match = {
@@ -1280,25 +1228,28 @@ export class Router<
1280
1228
  error,
1281
1229
  status: 'error',
1282
1230
  isFetching: false,
1283
- updatedAt: Date.now(),
1284
- }
1285
- } finally {
1286
- // If we showed the pending component, that means
1287
- // we already moved the pendingMatches to the matches
1288
- // state, so we need to update that specific match
1289
- if (didShowPending && pendingMinMs && match.showPending) {
1290
- updateMatch(match)
1291
1231
  }
1292
1232
  }
1293
1233
 
1294
1234
  updateMatch(match)
1295
1235
  }
1296
1236
 
1297
- if (match.fetchCount && match.status === 'success') {
1298
- // Background Fetching
1299
- fetch()
1237
+ // This is where all of the stale-while-revalidate magic happens
1238
+ const age = Date.now() - match.updatedAt
1239
+
1240
+ let staleAge = preload
1241
+ ? route.options.preloadStaleTime ??
1242
+ this.options.defaultPreloadStaleTime ??
1243
+ 30_000 // 30 seconds for preloads by default
1244
+ : route.options.staleTime ?? this.options.defaultStaleTime ?? 0
1245
+
1246
+ if (match.status === 'success') {
1247
+ // Background Fetching, no need to wait
1248
+ if (age > staleAge) {
1249
+ fetch()
1250
+ }
1300
1251
  } else {
1301
- // Critical Fetching
1252
+ // Critical Fetching, we need to await
1302
1253
 
1303
1254
  // If we need to potentially show the pending component,
1304
1255
  // start a timer to show it after the pendingMs
@@ -1320,9 +1271,6 @@ export class Router<
1320
1271
  await fetch()
1321
1272
  }
1322
1273
 
1323
- resolve()
1324
- // No Fetching
1325
-
1326
1274
  resolve()
1327
1275
  }),
1328
1276
  )
@@ -1358,15 +1306,23 @@ export class Router<
1358
1306
  const previousMatches = this.state.matches
1359
1307
 
1360
1308
  this.__store.batch(() => {
1361
- this.__store.setState((s) => ({
1362
- ...s,
1363
- preloadMatches: s.preloadMatches.filter((d) => {
1364
- return (
1365
- Date.now() - d.updatedAt <
1366
- (this.options.defaultPreloadMaxAge ?? 3000)
1367
- )
1368
- }),
1369
- }))
1309
+ // This is where all of the garbage collection magic happens
1310
+ this.__store.setState((s) => {
1311
+ return {
1312
+ ...s,
1313
+ cachedMatches: s.cachedMatches.filter((d) => {
1314
+ const route = this.looseRoutesById[d.routeId]!
1315
+
1316
+ return (
1317
+ d.status !== 'error' &&
1318
+ Date.now() - d.updatedAt <
1319
+ (route.options.gcTime ??
1320
+ this.options.defaultGcTime ??
1321
+ 5 * 60 * 1000)
1322
+ )
1323
+ }),
1324
+ }
1325
+ })
1370
1326
 
1371
1327
  // Match the routes
1372
1328
  pendingMatches = this.matchRoutes(next.pathname, next.search, {
@@ -1374,12 +1330,13 @@ export class Router<
1374
1330
  })
1375
1331
 
1376
1332
  // Ingest the new matches
1333
+ // If a cached moved to pendingMatches, remove it from cachedMatches
1377
1334
  this.__store.setState((s) => ({
1378
1335
  ...s,
1379
1336
  isLoading: true,
1380
1337
  location: next,
1381
1338
  pendingMatches,
1382
- preloadMatches: s.preloadMatches.filter((d) => {
1339
+ cachedMatches: s.cachedMatches.filter((d) => {
1383
1340
  return !pendingMatches.find((e) => e.id === d.id)
1384
1341
  }),
1385
1342
  }))
@@ -1403,29 +1360,35 @@ export class Router<
1403
1360
  return latestPromise
1404
1361
  }
1405
1362
 
1406
- const exitingMatchIds = previousMatches.filter(
1407
- (id) => !this.pendingMatches.includes(id),
1363
+ const exitingMatches = previousMatches.filter(
1364
+ (match) => !pendingMatches.find((d) => d.id === match.id),
1408
1365
  )
1409
- const enteringMatchIds = this.pendingMatches.filter(
1410
- (id) => !previousMatches.includes(id),
1366
+ const enteringMatches = pendingMatches.filter(
1367
+ (match) => !previousMatches.find((d) => d.id === match.id),
1411
1368
  )
1412
- const stayingMatchIds = previousMatches.filter((id) =>
1413
- this.pendingMatches.includes(id),
1369
+ const stayingMatches = previousMatches.filter((match) =>
1370
+ pendingMatches.find((d) => d.id === match.id),
1414
1371
  )
1415
1372
 
1373
+ // Commit the pending matches. If a previous match was
1374
+ // removed, place it in the cachedMatches
1416
1375
  this.__store.setState((s) => ({
1417
1376
  ...s,
1418
1377
  isLoading: false,
1419
1378
  matches: pendingMatches,
1420
1379
  pendingMatches: undefined,
1380
+ cachedMatches: [
1381
+ ...s.cachedMatches,
1382
+ ...exitingMatches.filter((d) => d.status !== 'error'),
1383
+ ],
1421
1384
  }))
1422
1385
 
1423
1386
  //
1424
1387
  ;(
1425
1388
  [
1426
- [exitingMatchIds, 'onLeave'],
1427
- [enteringMatchIds, 'onEnter'],
1428
- [stayingMatchIds, 'onStay'],
1389
+ [exitingMatches, 'onLeave'],
1390
+ [enteringMatches, 'onEnter'],
1391
+ [stayingMatches, 'onStay'],
1429
1392
  ] as const
1430
1393
  ).forEach(([matches, hook]) => {
1431
1394
  matches.forEach((match) => {
@@ -1469,7 +1432,7 @@ export class Router<
1469
1432
  [
1470
1433
  ...this.state.matches,
1471
1434
  ...(this.state.pendingMatches ?? []),
1472
- ...this.state.preloadMatches,
1435
+ ...this.state.cachedMatches,
1473
1436
  ]?.map((d) => [d.id, true]),
1474
1437
  )
1475
1438
 
@@ -1478,7 +1441,7 @@ export class Router<
1478
1441
  if (!loadedMatchIds[match.id]) {
1479
1442
  this.__store.setState((s) => ({
1480
1443
  ...s,
1481
- preloadMatches: [...(s.preloadMatches as any), match],
1444
+ cachedMatches: [...(s.cachedMatches as any), match],
1482
1445
  }))
1483
1446
  }
1484
1447
  })
@@ -1661,7 +1624,7 @@ export function getInitialRouterState(
1661
1624
  location,
1662
1625
  matches: [],
1663
1626
  pendingMatches: [],
1664
- preloadMatches: [],
1627
+ cachedMatches: [],
1665
1628
  lastUpdated: Date.now(),
1666
1629
  }
1667
1630
  }