@tanstack/router-core 1.167.5 → 1.168.0

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 (59) hide show
  1. package/dist/cjs/index.cjs +3 -0
  2. package/dist/cjs/index.d.cts +2 -0
  3. package/dist/cjs/load-matches.cjs +14 -9
  4. package/dist/cjs/load-matches.cjs.map +1 -1
  5. package/dist/cjs/router.cjs +135 -151
  6. package/dist/cjs/router.cjs.map +1 -1
  7. package/dist/cjs/router.d.cts +16 -10
  8. package/dist/cjs/scroll-restoration.cjs +5 -4
  9. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  10. package/dist/cjs/ssr/createRequestHandler.cjs +2 -2
  11. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  12. package/dist/cjs/ssr/ssr-client.cjs +14 -17
  13. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  14. package/dist/cjs/ssr/ssr-server.cjs +1 -1
  15. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  16. package/dist/cjs/stores.cjs +148 -0
  17. package/dist/cjs/stores.cjs.map +1 -0
  18. package/dist/cjs/stores.d.cts +70 -0
  19. package/dist/cjs/utils.cjs +7 -0
  20. package/dist/cjs/utils.cjs.map +1 -1
  21. package/dist/cjs/utils.d.cts +1 -0
  22. package/dist/esm/index.d.ts +2 -0
  23. package/dist/esm/index.js +2 -1
  24. package/dist/esm/load-matches.js +14 -9
  25. package/dist/esm/load-matches.js.map +1 -1
  26. package/dist/esm/router.d.ts +16 -10
  27. package/dist/esm/router.js +135 -151
  28. package/dist/esm/router.js.map +1 -1
  29. package/dist/esm/scroll-restoration.js +5 -4
  30. package/dist/esm/scroll-restoration.js.map +1 -1
  31. package/dist/esm/ssr/createRequestHandler.js +2 -2
  32. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  33. package/dist/esm/ssr/ssr-client.js +14 -17
  34. package/dist/esm/ssr/ssr-client.js.map +1 -1
  35. package/dist/esm/ssr/ssr-server.js +1 -1
  36. package/dist/esm/ssr/ssr-server.js.map +1 -1
  37. package/dist/esm/stores.d.ts +70 -0
  38. package/dist/esm/stores.js +146 -0
  39. package/dist/esm/stores.js.map +1 -0
  40. package/dist/esm/utils.d.ts +1 -0
  41. package/dist/esm/utils.js +7 -1
  42. package/dist/esm/utils.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/index.ts +11 -0
  45. package/src/load-matches.ts +23 -11
  46. package/src/router.ts +238 -252
  47. package/src/scroll-restoration.ts +6 -5
  48. package/src/ssr/createRequestHandler.ts +5 -4
  49. package/src/ssr/ssr-client.ts +17 -18
  50. package/src/ssr/ssr-server.ts +1 -1
  51. package/src/stores.ts +342 -0
  52. package/src/utils.ts +9 -0
  53. package/dist/cjs/utils/batch.cjs +0 -16
  54. package/dist/cjs/utils/batch.cjs.map +0 -1
  55. package/dist/cjs/utils/batch.d.cts +0 -1
  56. package/dist/esm/utils/batch.d.ts +0 -1
  57. package/dist/esm/utils/batch.js +0 -15
  58. package/dist/esm/utils/batch.js.map +0 -1
  59. package/src/utils/batch.ts +0 -18
package/src/router.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { createStore } from '@tanstack/store'
2
1
  import { createBrowserHistory, parseHref } from '@tanstack/history'
3
2
  import { isServer } from '@tanstack/router-core/isServer'
4
- import { batch } from './utils/batch'
5
3
  import {
6
4
  DEFAULT_PROTOCOL_ALLOWLIST,
7
5
  createControlledPromise,
@@ -43,7 +41,7 @@ import {
43
41
  executeRewriteOutput,
44
42
  rewriteBasepath,
45
43
  } from './rewrite'
46
- import type { Store } from '@tanstack/store'
44
+ import { createRouterStores } from './stores'
47
45
  import type { LRUCache } from './lru-cache'
48
46
  import type {
49
47
  ProcessRouteTreeResult,
@@ -105,7 +103,7 @@ import type {
105
103
  AnySerializationAdapter,
106
104
  ValidateSerializableInput,
107
105
  } from './ssr/serializer/transformer'
108
- // import type { AnyRouterConfig } from './config'
106
+ import type { GetStoreConfig, RouterStores } from './stores'
109
107
 
110
108
  export type ControllablePromise<T = any> = Promise<T> & {
111
109
  resolve: (value: T) => void
@@ -543,8 +541,6 @@ export interface RouterState<
543
541
  isLoading: boolean
544
542
  isTransitioning: boolean
545
543
  matches: Array<TRouteMatch>
546
- pendingMatches?: Array<TRouteMatch>
547
- cachedMatches: Array<TRouteMatch>
548
544
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
549
545
  resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
550
546
  statusCode: number
@@ -871,27 +867,20 @@ export type TrailingSlashOption =
871
867
 
872
868
  /**
873
869
  * Compute whether path, href or hash changed between previous and current
874
- * resolved locations in router state.
870
+ * resolved locations.
875
871
  */
876
- export function getLocationChangeInfo(routerState: {
877
- resolvedLocation?: ParsedLocation
878
- location: ParsedLocation
879
- }) {
880
- const fromLocation = routerState.resolvedLocation
881
- const toLocation = routerState.location
872
+ export function getLocationChangeInfo(
873
+ location: ParsedLocation,
874
+ resolvedLocation?: ParsedLocation,
875
+ ) {
876
+ const fromLocation = resolvedLocation
877
+ const toLocation = location
882
878
  const pathChanged = fromLocation?.pathname !== toLocation.pathname
883
879
  const hrefChanged = fromLocation?.href !== toLocation.href
884
880
  const hashChanged = fromLocation?.hash !== toLocation.hash
885
881
  return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
886
882
  }
887
883
 
888
- function filterRedirectedCachedMatches<T extends { status: string }>(
889
- matches: Array<T>,
890
- ): Array<T> {
891
- const filtered = matches.filter((d) => d.status !== 'redirected')
892
- return filtered.length === matches.length ? matches : filtered
893
- }
894
-
895
884
  export type CreateRouterFn = <
896
885
  TRouteTree extends AnyRoute,
897
886
  TTrailingSlashOption extends TrailingSlashOption = 'never',
@@ -936,24 +925,6 @@ declare global {
936
925
  *
937
926
  * @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType
938
927
  */
939
- type RouterStateStore<TState> = {
940
- state: TState
941
- setState: (updater: (prev: TState) => TState) => void
942
- }
943
-
944
- function createServerStore<TState>(
945
- initialState: TState,
946
- ): RouterStateStore<TState> {
947
- const store = {
948
- state: initialState,
949
- setState: (updater: (prev: TState) => TState) => {
950
- store.state = updater(store.state)
951
- },
952
- } as RouterStateStore<TState>
953
-
954
- return store
955
- }
956
-
957
928
  export class RouterCore<
958
929
  in out TRouteTree extends AnyRoute,
959
930
  in out TTrailingSlashOption extends TrailingSlashOption,
@@ -974,7 +945,10 @@ export class RouterCore<
974
945
  isScrollRestorationSetup = false
975
946
 
976
947
  // Must build in constructor
977
- __store!: Store<RouterState<TRouteTree>>
948
+ stores!: RouterStores<TRouteTree>
949
+ private getStoreConfig!: GetStoreConfig
950
+ batch!: (fn: () => void) => void
951
+
978
952
  options!: PickAsRequired<
979
953
  RouterOptions<
980
954
  TRouteTree,
@@ -1011,7 +985,10 @@ export class RouterCore<
1011
985
  TRouterHistory,
1012
986
  TDehydrated
1013
987
  >,
988
+ getStoreConfig: GetStoreConfig,
1014
989
  ) {
990
+ this.getStoreConfig = getStoreConfig
991
+
1015
992
  this.update({
1016
993
  defaultPreloadDelay: 50,
1017
994
  defaultPendingMs: 1000,
@@ -1140,14 +1117,15 @@ export class RouterCore<
1140
1117
  this.setRoutes(processRouteTreeResult)
1141
1118
  }
1142
1119
 
1143
- if (!this.__store && this.latestLocation) {
1144
- if (isServer ?? this.isServer) {
1145
- this.__store = createServerStore(
1146
- getInitialRouterState(this.latestLocation),
1147
- ) as unknown as Store<any>
1148
- } else {
1149
- this.__store = createStore(getInitialRouterState(this.latestLocation))
1120
+ if (!this.stores && this.latestLocation) {
1121
+ const config = this.getStoreConfig(this)
1122
+ this.batch = config.batch
1123
+ this.stores = createRouterStores(
1124
+ getInitialRouterState(this.latestLocation),
1125
+ config,
1126
+ )
1150
1127
 
1128
+ if (!(isServer ?? this.isServer)) {
1151
1129
  setupScrollRestoration(this)
1152
1130
  }
1153
1131
  }
@@ -1188,11 +1166,8 @@ export class RouterCore<
1188
1166
  needsLocationUpdate = true
1189
1167
  }
1190
1168
 
1191
- if (needsLocationUpdate && this.__store) {
1192
- this.__store.setState((s) => ({
1193
- ...s,
1194
- location: this.latestLocation,
1195
- }))
1169
+ if (needsLocationUpdate && this.stores) {
1170
+ this.stores.location.setState(() => this.latestLocation)
1196
1171
  }
1197
1172
 
1198
1173
  if (
@@ -1207,7 +1182,7 @@ export class RouterCore<
1207
1182
  }
1208
1183
 
1209
1184
  get state(): RouterState<TRouteTree> {
1210
- return this.__store.state
1185
+ return this.stores.__store.state
1211
1186
  }
1212
1187
 
1213
1188
  updateLatestLocation = () => {
@@ -1440,10 +1415,14 @@ export class RouterCore<
1440
1415
  : undefined
1441
1416
 
1442
1417
  const matches = new Array<AnyRouteMatch>(matchedRoutes.length)
1443
-
1444
- const previousMatchesByRouteId = new Map(
1445
- this.state.matches.map((match) => [match.routeId, match]),
1446
- )
1418
+ // Snapshot of active match state keyed by routeId, used to stabilise
1419
+ // params/search across navigations.
1420
+ const previousActiveMatchesByRouteId = new Map<string, AnyRouteMatch>()
1421
+ for (const store of this.stores.activeMatchStoresById.values()) {
1422
+ if (store.routeId) {
1423
+ previousActiveMatchesByRouteId.set(store.routeId, store.state)
1424
+ }
1425
+ }
1447
1426
 
1448
1427
  for (let index = 0; index < matchedRoutes.length; index++) {
1449
1428
  const route = matchedRoutes[index]!
@@ -1528,7 +1507,7 @@ export class RouterCore<
1528
1507
 
1529
1508
  const existingMatch = this.getMatch(matchId)
1530
1509
 
1531
- const previousMatch = previousMatchesByRouteId.get(route.id)
1510
+ const previousMatch = previousActiveMatchesByRouteId.get(route.id)
1532
1511
 
1533
1512
  const strictParams = existingMatch?._strictParams ?? usedParams
1534
1513
 
@@ -1644,7 +1623,7 @@ export class RouterCore<
1644
1623
  const existingMatch = this.getMatch(match.id)
1645
1624
 
1646
1625
  // Update the match's params
1647
- const previousMatch = previousMatchesByRouteId.get(match.routeId)
1626
+ const previousMatch = previousActiveMatchesByRouteId.get(match.routeId)
1648
1627
  match.params = previousMatch
1649
1628
  ? nullReplaceEqualDeep(previousMatch.params, routeParams)
1650
1629
  : routeParams
@@ -1734,11 +1713,14 @@ export class RouterCore<
1734
1713
  }
1735
1714
 
1736
1715
  // Determine params: reuse from state if possible, otherwise parse
1737
- const lastStateMatch = last(this.state.matches)
1716
+ const lastStateMatchId = last(this.stores.matchesId.state)
1717
+ const lastStateMatch =
1718
+ lastStateMatchId &&
1719
+ this.stores.activeMatchStoresById.get(lastStateMatchId)?.state
1738
1720
  const canReuseParams =
1739
1721
  lastStateMatch &&
1740
1722
  lastStateMatch.routeId === lastRoute.id &&
1741
- location.pathname === this.state.location.pathname
1723
+ lastStateMatch.pathname === location.pathname
1742
1724
 
1743
1725
  let params: Record<string, unknown>
1744
1726
  if (canReuseParams) {
@@ -1783,19 +1765,23 @@ export class RouterCore<
1783
1765
  }
1784
1766
 
1785
1767
  cancelMatches = () => {
1786
- const currentPendingMatches = this.state.matches.filter(
1787
- (match) => match.status === 'pending',
1788
- )
1789
- const currentLoadingMatches = this.state.matches.filter(
1790
- (match) => match.isFetching === 'loader',
1791
- )
1792
- const matchesToCancelArray = new Set([
1793
- ...(this.state.pendingMatches ?? []),
1794
- ...currentPendingMatches,
1795
- ...currentLoadingMatches,
1796
- ])
1797
- matchesToCancelArray.forEach((match) => {
1798
- this.cancelMatch(match.id)
1768
+ this.stores.pendingMatchesId.state.forEach((matchId) => {
1769
+ this.cancelMatch(matchId)
1770
+ })
1771
+
1772
+ this.stores.matchesId.state.forEach((matchId) => {
1773
+ if (this.stores.pendingMatchStoresById.has(matchId)) {
1774
+ return
1775
+ }
1776
+
1777
+ const match = this.stores.activeMatchStoresById.get(matchId)?.state
1778
+ if (!match) {
1779
+ return
1780
+ }
1781
+
1782
+ if (match.status === 'pending' || match.isFetching === 'loader') {
1783
+ this.cancelMatch(matchId)
1784
+ }
1799
1785
  })
1800
1786
  }
1801
1787
 
@@ -2368,26 +2354,28 @@ export class RouterCore<
2368
2354
  // Match the routes
2369
2355
  const pendingMatches = this.matchRoutes(this.latestLocation)
2370
2356
 
2357
+ const nextCachedMatches = this.stores.cachedMatchesSnapshot.state.filter(
2358
+ (d) => !pendingMatches.some((e) => e.id === d.id),
2359
+ )
2360
+
2371
2361
  // Ingest the new matches
2372
- this.__store.setState((s) => ({
2373
- ...s,
2374
- status: 'pending',
2375
- statusCode: 200,
2376
- isLoading: true,
2377
- location: this.latestLocation,
2378
- pendingMatches,
2379
- // If a cached moved to pendingMatches, remove it from cachedMatches
2380
- cachedMatches: s.cachedMatches.filter(
2381
- (d) => !pendingMatches.some((e) => e.id === d.id),
2382
- ),
2383
- }))
2362
+ this.batch(() => {
2363
+ this.stores.status.setState(() => 'pending')
2364
+ this.stores.statusCode.setState(() => 200)
2365
+ this.stores.isLoading.setState(() => true)
2366
+ this.stores.location.setState(() => this.latestLocation)
2367
+ this.stores.setPendingMatches(pendingMatches)
2368
+ // If a cached match moved to pending matches, remove it from cached matches
2369
+ this.stores.setCachedMatches(nextCachedMatches)
2370
+ })
2384
2371
  }
2385
2372
 
2386
2373
  load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
2387
2374
  let redirect: AnyRedirect | undefined
2388
2375
  let notFound: NotFoundError | undefined
2389
2376
  let loadPromise: Promise<void>
2390
- const previousLocation = this.state.resolvedLocation ?? this.state.location
2377
+ const previousLocation =
2378
+ this.stores.resolvedLocation.state ?? this.stores.location.state
2391
2379
 
2392
2380
  // eslint-disable-next-line prefer-const
2393
2381
  loadPromise = new Promise<void>((resolve) => {
@@ -2395,31 +2383,26 @@ export class RouterCore<
2395
2383
  try {
2396
2384
  this.beforeLoad()
2397
2385
  const next = this.latestLocation
2398
- const prevLocation = this.state.resolvedLocation
2386
+ const prevLocation = this.stores.resolvedLocation.state
2387
+ const locationChangeInfo = getLocationChangeInfo(next, prevLocation)
2399
2388
 
2400
- if (!this.state.redirect) {
2389
+ if (!this.stores.redirect.state) {
2401
2390
  this.emit({
2402
2391
  type: 'onBeforeNavigate',
2403
- ...getLocationChangeInfo({
2404
- resolvedLocation: prevLocation,
2405
- location: next,
2406
- }),
2392
+ ...locationChangeInfo,
2407
2393
  })
2408
2394
  }
2409
2395
 
2410
2396
  this.emit({
2411
2397
  type: 'onBeforeLoad',
2412
- ...getLocationChangeInfo({
2413
- resolvedLocation: prevLocation,
2414
- location: next,
2415
- }),
2398
+ ...locationChangeInfo,
2416
2399
  })
2417
2400
 
2418
2401
  await loadMatches({
2419
2402
  router: this,
2420
2403
  sync: opts?.sync,
2421
2404
  forceStaleReload: previousLocation.href === next.href,
2422
- matches: this.state.pendingMatches as Array<AnyRouteMatch>,
2405
+ matches: this.stores.pendingMatchesSnapshot.state,
2423
2406
  location: next,
2424
2407
  updateMatch: this.updateMatch,
2425
2408
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -2434,80 +2417,92 @@ export class RouterCore<
2434
2417
  //
2435
2418
  // exitingMatches uses match.id (routeId + params + loaderDeps) so
2436
2419
  // navigating /foo?page=1 → /foo?page=2 correctly caches the page=1 entry.
2437
- let exitingMatches: Array<AnyRouteMatch> = []
2420
+ let exitingMatches: Array<AnyRouteMatch> | null = null
2438
2421
 
2439
2422
  // Lifecycle-hook identity uses routeId only so that navigating between
2440
2423
  // different params/deps of the same route fires onStay (not onLeave+onEnter).
2441
- let hookExitingMatches: Array<AnyRouteMatch> = []
2442
- let hookEnteringMatches: Array<AnyRouteMatch> = []
2443
- let hookStayingMatches: Array<AnyRouteMatch> = []
2444
-
2445
- batch(() => {
2446
- this.__store.setState((s) => {
2447
- const previousMatches = s.matches
2448
- const newMatches = s.pendingMatches || s.matches
2449
-
2450
- exitingMatches = previousMatches.filter(
2451
- (match) => !newMatches.some((d) => d.id === match.id),
2452
- )
2453
-
2454
- // Lifecycle-hook identity: routeId only (route presence in tree)
2455
- hookExitingMatches = previousMatches.filter(
2456
- (match) =>
2457
- !newMatches.some((d) => d.routeId === match.routeId),
2458
- )
2459
- hookEnteringMatches = newMatches.filter(
2460
- (match) =>
2461
- !previousMatches.some(
2462
- (d) => d.routeId === match.routeId,
2463
- ),
2464
- )
2465
- hookStayingMatches = newMatches.filter((match) =>
2466
- previousMatches.some(
2467
- (d) => d.routeId === match.routeId,
2424
+ let hookExitingMatches: Array<AnyRouteMatch> | null = null
2425
+ let hookEnteringMatches: Array<AnyRouteMatch> | null = null
2426
+ let hookStayingMatches: Array<AnyRouteMatch> | null = null
2427
+
2428
+ this.batch(() => {
2429
+ const pendingMatches =
2430
+ this.stores.pendingMatchesSnapshot.state
2431
+ const mountPending = pendingMatches.length
2432
+ const currentMatches =
2433
+ this.stores.activeMatchesSnapshot.state
2434
+
2435
+ exitingMatches = mountPending
2436
+ ? currentMatches.filter(
2437
+ (match) =>
2438
+ !this.stores.pendingMatchStoresById.has(match.id),
2439
+ )
2440
+ : null
2441
+
2442
+ // Lifecycle-hook identity: routeId only (route presence in tree)
2443
+ // Build routeId sets from pools to avoid derived stores.
2444
+ const pendingRouteIds = new Set<string>()
2445
+ for (const s of this.stores.pendingMatchStoresById.values()) {
2446
+ if (s.routeId) pendingRouteIds.add(s.routeId)
2447
+ }
2448
+ const activeRouteIds = new Set<string>()
2449
+ for (const s of this.stores.activeMatchStoresById.values()) {
2450
+ if (s.routeId) activeRouteIds.add(s.routeId)
2451
+ }
2452
+
2453
+ hookExitingMatches = mountPending
2454
+ ? currentMatches.filter(
2455
+ (match) => !pendingRouteIds.has(match.routeId),
2456
+ )
2457
+ : null
2458
+ hookEnteringMatches = mountPending
2459
+ ? pendingMatches.filter(
2460
+ (match) => !activeRouteIds.has(match.routeId),
2461
+ )
2462
+ : null
2463
+ hookStayingMatches = mountPending
2464
+ ? pendingMatches.filter((match) =>
2465
+ activeRouteIds.has(match.routeId),
2466
+ )
2467
+ : currentMatches
2468
+
2469
+ this.stores.isLoading.setState(() => false)
2470
+ this.stores.loadedAt.setState(() => Date.now())
2471
+ /**
2472
+ * When committing new matches, cache any exiting matches that are still usable.
2473
+ * Routes that resolved with `status: 'error'` or `status: 'notFound'` are
2474
+ * deliberately excluded from `cachedMatches` so that subsequent invalidations
2475
+ * or reloads re-run their loaders instead of reusing the failed/not-found data.
2476
+ */
2477
+ if (mountPending) {
2478
+ this.stores.setActiveMatches(pendingMatches)
2479
+ this.stores.setPendingMatches([])
2480
+ this.stores.setCachedMatches([
2481
+ ...this.stores.cachedMatchesSnapshot.state,
2482
+ ...exitingMatches!.filter(
2483
+ (d) =>
2484
+ d.status !== 'error' &&
2485
+ d.status !== 'notFound' &&
2486
+ d.status !== 'redirected',
2468
2487
  ),
2469
- )
2470
-
2471
- return {
2472
- ...s,
2473
- isLoading: false,
2474
- loadedAt: Date.now(),
2475
- matches: newMatches,
2476
- pendingMatches: undefined,
2477
- /**
2478
- * When committing new matches, cache any exiting matches that are still usable.
2479
- * Routes that resolved with `status: 'error'` or `status: 'notFound'` are
2480
- * deliberately excluded from `cachedMatches` so that subsequent invalidations
2481
- * or reloads re-run their loaders instead of reusing the failed/not-found data.
2482
- */
2483
- cachedMatches: [
2484
- ...s.cachedMatches,
2485
- ...exitingMatches.filter(
2486
- (d) =>
2487
- d.status !== 'error' &&
2488
- d.status !== 'notFound' &&
2489
- d.status !== 'redirected',
2490
- ),
2491
- ],
2492
- }
2493
- })
2494
- this.clearExpiredCache()
2488
+ ])
2489
+ this.clearExpiredCache()
2490
+ }
2495
2491
  })
2496
2492
 
2497
2493
  //
2498
- ;(
2499
- [
2500
- [hookExitingMatches, 'onLeave'],
2501
- [hookEnteringMatches, 'onEnter'],
2502
- [hookStayingMatches, 'onStay'],
2503
- ] as const
2504
- ).forEach(([matches, hook]) => {
2505
- matches.forEach((match) => {
2494
+ for (const [matches, hook] of [
2495
+ [hookExitingMatches, 'onLeave'],
2496
+ [hookEnteringMatches, 'onEnter'],
2497
+ [hookStayingMatches, 'onStay'],
2498
+ ] as const) {
2499
+ if (!matches) continue
2500
+ for (const match of matches as Array<AnyRouteMatch>) {
2506
2501
  this.looseRoutesById[match.routeId]!.options[hook]?.(
2507
2502
  match,
2508
2503
  )
2509
- })
2510
- })
2504
+ }
2505
+ }
2511
2506
  })
2512
2507
  })
2513
2508
  },
@@ -2526,17 +2521,20 @@ export class RouterCore<
2526
2521
  notFound = err
2527
2522
  }
2528
2523
 
2529
- this.__store.setState((s) => ({
2530
- ...s,
2531
- statusCode: redirect
2532
- ? redirect.status
2533
- : notFound
2534
- ? 404
2535
- : s.matches.some((d) => d.status === 'error')
2536
- ? 500
2537
- : 200,
2538
- redirect,
2539
- }))
2524
+ const nextStatusCode = redirect
2525
+ ? redirect.status
2526
+ : notFound
2527
+ ? 404
2528
+ : this.stores.activeMatchesSnapshot.state.some(
2529
+ (d) => d.status === 'error',
2530
+ )
2531
+ ? 500
2532
+ : 200
2533
+
2534
+ this.batch(() => {
2535
+ this.stores.statusCode.setState(() => nextStatusCode)
2536
+ this.stores.redirect.setState(() => redirect)
2537
+ })
2540
2538
  }
2541
2539
 
2542
2540
  if (this.latestLoadPromise === loadPromise) {
@@ -2563,14 +2561,13 @@ export class RouterCore<
2563
2561
  let newStatusCode: number | undefined = undefined
2564
2562
  if (this.hasNotFoundMatch()) {
2565
2563
  newStatusCode = 404
2566
- } else if (this.__store.state.matches.some((d) => d.status === 'error')) {
2564
+ } else if (
2565
+ this.stores.activeMatchesSnapshot.state.some((d) => d.status === 'error')
2566
+ ) {
2567
2567
  newStatusCode = 500
2568
2568
  }
2569
2569
  if (newStatusCode !== undefined) {
2570
- this.__store.setState((s) => ({
2571
- ...s,
2572
- statusCode: newStatusCode,
2573
- }))
2570
+ this.stores.statusCode.setState(() => newStatusCode)
2574
2571
  }
2575
2572
  }
2576
2573
 
@@ -2599,15 +2596,12 @@ export class RouterCore<
2599
2596
  this.isViewTransitionTypesSupported
2600
2597
  ) {
2601
2598
  const next = this.latestLocation
2602
- const prevLocation = this.state.resolvedLocation
2599
+ const prevLocation = this.stores.resolvedLocation.state
2603
2600
 
2604
2601
  const resolvedViewTransitionTypes =
2605
2602
  typeof shouldViewTransition.types === 'function'
2606
2603
  ? shouldViewTransition.types(
2607
- getLocationChangeInfo({
2608
- resolvedLocation: prevLocation,
2609
- location: next,
2610
- }),
2604
+ getLocationChangeInfo(next, prevLocation),
2611
2605
  )
2612
2606
  : shouldViewTransition.types
2613
2607
 
@@ -2632,40 +2626,40 @@ export class RouterCore<
2632
2626
 
2633
2627
  updateMatch: UpdateMatchFn = (id, updater) => {
2634
2628
  this.startTransition(() => {
2635
- const matchesKey = this.state.pendingMatches?.some((d) => d.id === id)
2636
- ? 'pendingMatches'
2637
- : this.state.matches.some((d) => d.id === id)
2638
- ? 'matches'
2639
- : this.state.cachedMatches.some((d) => d.id === id)
2640
- ? 'cachedMatches'
2641
- : ''
2642
-
2643
- if (matchesKey) {
2644
- if (matchesKey === 'cachedMatches') {
2645
- this.__store.setState((s) => ({
2646
- ...s,
2647
- cachedMatches: filterRedirectedCachedMatches(
2648
- s.cachedMatches.map((d) => (d.id === id ? updater(d) : d)),
2649
- ),
2650
- }))
2629
+ const pendingMatch = this.stores.pendingMatchStoresById.get(id)
2630
+ if (pendingMatch) {
2631
+ pendingMatch.setState(updater)
2632
+ return
2633
+ }
2634
+
2635
+ const activeMatch = this.stores.activeMatchStoresById.get(id)
2636
+ if (activeMatch) {
2637
+ activeMatch.setState(updater)
2638
+ return
2639
+ }
2640
+
2641
+ const cachedMatch = this.stores.cachedMatchStoresById.get(id)
2642
+ if (cachedMatch) {
2643
+ const next = updater(cachedMatch.state)
2644
+ if (next.status === 'redirected') {
2645
+ const deleted = this.stores.cachedMatchStoresById.delete(id)
2646
+ if (deleted) {
2647
+ this.stores.cachedMatchesId.setState((prev) =>
2648
+ prev.filter((matchId) => matchId !== id),
2649
+ )
2650
+ }
2651
2651
  } else {
2652
- this.__store.setState((s) => ({
2653
- ...s,
2654
- [matchesKey]: s[matchesKey]?.map((d) =>
2655
- d.id === id ? updater(d) : d,
2656
- ),
2657
- }))
2652
+ cachedMatch.setState(() => next)
2658
2653
  }
2659
2654
  }
2660
2655
  })
2661
2656
  }
2662
2657
 
2663
2658
  getMatch: GetMatchFn = (matchId: string): AnyRouteMatch | undefined => {
2664
- const findFn = (d: { id: string }) => d.id === matchId
2665
2659
  return (
2666
- this.state.cachedMatches.find(findFn) ??
2667
- this.state.pendingMatches?.find(findFn) ??
2668
- this.state.matches.find(findFn)
2660
+ this.stores.cachedMatchStoresById.get(matchId)?.state ??
2661
+ this.stores.pendingMatchStoresById.get(matchId)?.state ??
2662
+ this.stores.activeMatchStoresById.get(matchId)?.state
2669
2663
  )
2670
2664
  }
2671
2665
 
@@ -2701,12 +2695,17 @@ export class RouterCore<
2701
2695
  return d
2702
2696
  }
2703
2697
 
2704
- this.__store.setState((s) => ({
2705
- ...s,
2706
- matches: s.matches.map(invalidate),
2707
- cachedMatches: s.cachedMatches.map(invalidate),
2708
- pendingMatches: s.pendingMatches?.map(invalidate),
2709
- }))
2698
+ this.batch(() => {
2699
+ this.stores.setActiveMatches(
2700
+ this.stores.activeMatchesSnapshot.state.map(invalidate),
2701
+ )
2702
+ this.stores.setCachedMatches(
2703
+ this.stores.cachedMatchesSnapshot.state.map(invalidate),
2704
+ )
2705
+ this.stores.setPendingMatches(
2706
+ this.stores.pendingMatchesSnapshot.state.map(invalidate),
2707
+ )
2708
+ })
2710
2709
 
2711
2710
  this.shouldViewTransition = false
2712
2711
  return this.load({ sync: opts?.sync })
@@ -2763,25 +2762,18 @@ export class RouterCore<
2763
2762
  clearCache: ClearCacheFn<this> = (opts) => {
2764
2763
  const filter = opts?.filter
2765
2764
  if (filter !== undefined) {
2766
- this.__store.setState((s) => {
2767
- return {
2768
- ...s,
2769
- cachedMatches: s.cachedMatches.filter(
2770
- (m) => !filter(m as MakeRouteMatchUnion<this>),
2771
- ),
2772
- }
2773
- })
2765
+ this.stores.setCachedMatches(
2766
+ this.stores.cachedMatchesSnapshot.state.filter(
2767
+ (m) => !filter(m as MakeRouteMatchUnion<this>),
2768
+ ),
2769
+ )
2774
2770
  } else {
2775
- this.__store.setState((s) => {
2776
- return {
2777
- ...s,
2778
- cachedMatches: [],
2779
- }
2780
- })
2771
+ this.stores.setCachedMatches([])
2781
2772
  }
2782
2773
  }
2783
2774
 
2784
2775
  clearExpiredCache = () => {
2776
+ const now = Date.now()
2785
2777
  // This is where all of the garbage collection magic happens
2786
2778
  const filter = (d: MakeRouteMatch<TRouteTree>) => {
2787
2779
  const route = this.looseRoutesById[d.routeId]!
@@ -2801,7 +2793,7 @@ export class RouterCore<
2801
2793
  const isError = d.status === 'error'
2802
2794
  if (isError) return true
2803
2795
 
2804
- const gcEligible = Date.now() - d.updatedAt >= gcTime
2796
+ const gcEligible = now - d.updatedAt >= gcTime
2805
2797
  return gcEligible
2806
2798
  }
2807
2799
  this.clearCache({ filter })
@@ -2823,28 +2815,24 @@ export class RouterCore<
2823
2815
  dest: opts,
2824
2816
  })
2825
2817
 
2826
- const activeMatchIds = new Set(
2827
- [...this.state.matches, ...(this.state.pendingMatches ?? [])].map(
2828
- (d) => d.id,
2829
- ),
2830
- )
2818
+ const activeMatchIds = new Set([
2819
+ ...this.stores.matchesId.state,
2820
+ ...this.stores.pendingMatchesId.state,
2821
+ ])
2831
2822
 
2832
2823
  const loadedMatchIds = new Set([
2833
2824
  ...activeMatchIds,
2834
- ...this.state.cachedMatches.map((d) => d.id),
2825
+ ...this.stores.cachedMatchesId.state,
2835
2826
  ])
2836
2827
 
2837
- // If the matches are already loaded, we need to add them to the cachedMatches
2838
- batch(() => {
2839
- matches.forEach((match) => {
2840
- if (!loadedMatchIds.has(match.id)) {
2841
- this.__store.setState((s) => ({
2842
- ...s,
2843
- cachedMatches: [...(s.cachedMatches as any), match],
2844
- }))
2845
- }
2846
- })
2847
- })
2828
+ // If the matches are already loaded, we need to add them to the cached matches.
2829
+ const matchesToCache = matches.filter(
2830
+ (match) => !loadedMatchIds.has(match.id),
2831
+ )
2832
+ if (matchesToCache.length) {
2833
+ const cachedMatches = this.stores.cachedMatchesSnapshot.state
2834
+ this.stores.setCachedMatches([...cachedMatches, ...matchesToCache])
2835
+ }
2848
2836
 
2849
2837
  try {
2850
2838
  matches = await loadMatches({
@@ -2898,16 +2886,16 @@ export class RouterCore<
2898
2886
  }
2899
2887
  const next = this.buildLocation(matchLocation as any)
2900
2888
 
2901
- if (opts?.pending && this.state.status !== 'pending') {
2889
+ if (opts?.pending && this.stores.status.state !== 'pending') {
2902
2890
  return false
2903
2891
  }
2904
2892
 
2905
2893
  const pending =
2906
- opts?.pending === undefined ? !this.state.isLoading : opts.pending
2894
+ opts?.pending === undefined ? !this.stores.isLoading.state : opts.pending
2907
2895
 
2908
2896
  const baseLocation = pending
2909
2897
  ? this.latestLocation
2910
- : this.state.resolvedLocation || this.state.location
2898
+ : this.stores.resolvedLocation.state || this.stores.location.state
2911
2899
 
2912
2900
  const match = findSingleMatch(
2913
2901
  next.pathname,
@@ -2943,7 +2931,7 @@ export class RouterCore<
2943
2931
  serverSsr?: ServerSsr
2944
2932
 
2945
2933
  hasNotFoundMatch = () => {
2946
- return this.__store.state.matches.some(
2934
+ return this.stores.activeMatchesSnapshot.state.some(
2947
2935
  (d) => d.status === 'notFound' || d.globalNotFound,
2948
2936
  )
2949
2937
  }
@@ -2989,8 +2977,6 @@ export function getInitialRouterState(
2989
2977
  resolvedLocation: undefined,
2990
2978
  location,
2991
2979
  matches: [],
2992
- pendingMatches: [],
2993
- cachedMatches: [],
2994
2980
  statusCode: 200,
2995
2981
  }
2996
2982
  }