@tanstack/router-core 0.0.1-beta.173 → 0.0.1-beta.175

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/router-core",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.173",
4
+ "version": "0.0.1-beta.175",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -42,8 +42,8 @@
42
42
  "@babel/runtime": "^7.16.7",
43
43
  "tiny-invariant": "^1.3.1",
44
44
  "tiny-warning": "^1.0.3",
45
- "@gisatcz/cross-package-react-context": "^0.2.0",
46
- "@tanstack/react-store": "0.0.1-beta.173"
45
+ "@tanstack/store": "^0.0.1",
46
+ "@gisatcz/cross-package-react-context": "^0.2.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "rollup --config rollup.config.js",
package/src/route.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { ParsePathParams } from './link'
2
- import { AnyRouter, Router, RouteMatch, RegisteredRouter } from './router'
2
+ import {
3
+ AnyRouter,
4
+ Router,
5
+ RouteMatch,
6
+ RegisteredRouter,
7
+ AnyRouteMatch,
8
+ } from './router'
3
9
  import { IsAny, NoInfer, PickRequired, UnionToIntersection } from './utils'
4
10
  import invariant from 'tiny-invariant'
5
11
  import { joinPaths, trimPath } from './path'
@@ -386,21 +392,11 @@ export type UpdatableRouteOptions<
386
392
  >,
387
393
  ) => Promise<void> | void
388
394
  onError?: (err: any) => void
389
- // This function is called
390
- // when moving from an inactive state to an active one. Likewise, when moving from
391
- // an active to an inactive state, the return function (if provided) is called.
392
- onLoaded?: (matchContext: {
393
- params: TAllParams
394
- search: TFullSearchSchema
395
- }) =>
396
- | void
397
- | undefined
398
- | ((match: { params: TAllParams; search: TFullSearchSchema }) => void)
399
- // This function is called when the route remains active from one transition to the next.
400
- onTransition?: (match: {
401
- params: TAllParams
402
- search: TFullSearchSchema
403
- }) => void
395
+ // These functions are called as route matches are loaded, stick around and leave the active
396
+ // matches
397
+ onEnter?: (match: AnyRouteMatch) => void
398
+ onTransition?: (match: AnyRouteMatch) => void
399
+ onLeave?: (match: AnyRouteMatch) => void
404
400
  }
405
401
 
406
402
  export type ParseParamsOption<TPath extends string, TParams> = ParseParamsFn<
package/src/router.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Store } from '@tanstack/react-store'
1
+ import { Store } from '@tanstack/store'
2
2
  import invariant from 'tiny-invariant'
3
3
 
4
4
  //
@@ -37,6 +37,7 @@ import {
37
37
  FullSearchSchema,
38
38
  RouteById,
39
39
  RoutePaths,
40
+ RouteIds,
40
41
  } from './routeInfo'
41
42
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
42
43
  import {
@@ -108,13 +109,13 @@ export type HydrationCtx = {
108
109
 
109
110
  export interface RouteMatch<
110
111
  TRouteTree extends AnyRoute = AnyRoute,
111
- TRoute extends AnyRoute = AnyRoute,
112
+ TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
112
113
  > {
113
114
  id: string
114
115
  key?: string
115
- routeId: string
116
+ routeId: TRouteId
116
117
  pathname: string
117
- params: TRoute['types']['allParams']
118
+ params: RouteById<TRouteTree, TRouteId>['types']['allParams']
118
119
  status: 'pending' | 'success' | 'error'
119
120
  isFetching: boolean
120
121
  invalid: boolean
@@ -122,20 +123,21 @@ export interface RouteMatch<
122
123
  paramsError: unknown
123
124
  searchError: unknown
124
125
  updatedAt: number
125
- invalidAt: number
126
- preloadInvalidAt: number
127
- loaderData: TRoute['types']['loader']
126
+ maxAge: number
127
+ preloadMaxAge: number
128
+ loaderData: RouteById<TRouteTree, TRouteId>['types']['loader']
128
129
  loadPromise?: Promise<void>
129
130
  __resolveLoadPromise?: () => void
130
- routeContext: TRoute['types']['routeContext']
131
- context: TRoute['types']['context']
132
- routeSearch: TRoute['types']['searchSchema']
133
- search: FullSearchSchema<TRouteTree> & TRoute['types']['fullSearchSchema']
131
+ routeContext: RouteById<TRouteTree, TRouteId>['types']['routeContext']
132
+ context: RouteById<TRouteTree, TRouteId>['types']['context']
133
+ routeSearch: RouteById<TRouteTree, TRouteId>['types']['searchSchema']
134
+ search: FullSearchSchema<TRouteTree> &
135
+ RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
134
136
  fetchedAt: number
135
137
  abortController: AbortController
136
138
  }
137
139
 
138
- export type AnyRouteMatch = RouteMatch<AnyRoute, AnyRoute>
140
+ export type AnyRouteMatch = RouteMatch<any>
139
141
 
140
142
  export type RouterContextOptions<TRouteTree extends AnyRoute> =
141
143
  AnyContext extends TRouteTree['types']['routerContext']
@@ -155,6 +157,7 @@ export interface RouterOptions<
155
157
  parseSearch?: SearchParser
156
158
  defaultPreload?: false | 'intent'
157
159
  defaultPreloadDelay?: number
160
+ refetchOnWindowFocus?: boolean
158
161
  defaultComponent?: RegisteredRouteComponent<
159
162
  unknown,
160
163
  AnySearchSchema,
@@ -196,11 +199,11 @@ export interface RouterState<
196
199
  > {
197
200
  status: 'idle' | 'pending'
198
201
  isFetching: boolean
199
- matchesById: Record<string, RouteMatch<TRouteTree, ParseRoute<TRouteTree>>>
202
+ matchesById: Record<string, RouteMatch<TRouteTree>>
200
203
  matchIds: string[]
201
204
  pendingMatchIds: string[]
202
- matches: RouteMatch<TRouteTree, ParseRoute<TRouteTree>>[]
203
- pendingMatches: RouteMatch<TRouteTree, ParseRoute<TRouteTree>>[]
205
+ matches: RouteMatch<TRouteTree>[]
206
+ pendingMatches: RouteMatch<TRouteTree>[]
204
207
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
205
208
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
206
209
  lastUpdated: number
@@ -247,7 +250,8 @@ export type DehydratedRouteMatch = Pick<
247
250
  RouteMatch,
248
251
  | 'fetchedAt'
249
252
  | 'invalid'
250
- | 'invalidAt'
253
+ | 'maxAge'
254
+ | 'preloadMaxAge'
251
255
  | 'id'
252
256
  | 'loaderData'
253
257
  | 'status'
@@ -292,6 +296,9 @@ export type RouterListener<TRouterEvent extends RouterEvent> = {
292
296
  fn: ListenerFn<TRouterEvent>
293
297
  }
294
298
 
299
+ const visibilityChangeEvent = 'visibilitychange'
300
+ const focusEvent = 'focus'
301
+
295
302
  export class Router<
296
303
  TRouteTree extends AnyRoute = AnyRoute,
297
304
  TDehydrated extends Record<string, any> = Record<string, any>,
@@ -418,10 +425,28 @@ export class Router<
418
425
  }
419
426
 
420
427
  mount = () => {
421
- // If the router matches are empty, start loading the matches
422
- // if (!this.state.matches.length) {
428
+ // addEventListener does not exist in React Native, but window does
429
+ // In the future, we might need to invert control here for more adapters
430
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
431
+ if (typeof window !== 'undefined' && window.addEventListener) {
432
+ window.addEventListener(visibilityChangeEvent, this.#onFocus, false)
433
+ window.addEventListener(focusEvent, this.#onFocus, false)
434
+ }
435
+
423
436
  this.safeLoad()
424
- // }
437
+
438
+ return () => {
439
+ if (typeof window !== 'undefined' && window.removeEventListener) {
440
+ window.removeEventListener(visibilityChangeEvent, this.#onFocus)
441
+ window.removeEventListener(focusEvent, this.#onFocus)
442
+ }
443
+ }
444
+ }
445
+
446
+ #onFocus = () => {
447
+ if (this.options.refetchOnWindowFocus ?? true) {
448
+ this.reload()
449
+ }
425
450
  }
426
451
 
427
452
  update = (opts?: RouterOptions<any, any>): this => {
@@ -523,7 +548,6 @@ export class Router<
523
548
  }
524
549
 
525
550
  // Cancel any pending matches
526
- // this.cancelMatches()
527
551
 
528
552
  let pendingMatches!: RouteMatch<any, any>[]
529
553
 
@@ -575,6 +599,18 @@ export class Router<
575
599
  return latestPromise
576
600
  }
577
601
 
602
+ const exitingMatchIds = this.state.matchIds.filter(
603
+ (id) => !this.state.pendingMatchIds.includes(id),
604
+ )
605
+
606
+ const enteringMatchIds = this.state.pendingMatchIds.filter(
607
+ (id) => !this.state.matchIds.includes(id),
608
+ )
609
+
610
+ const stayingMatchIds = this.state.matchIds.filter((id) =>
611
+ this.state.pendingMatchIds.includes(id),
612
+ )
613
+
578
614
  this.__store.setState((s) => ({
579
615
  ...s,
580
616
  status: 'idle',
@@ -582,6 +618,19 @@ export class Router<
582
618
  matchIds: s.pendingMatchIds,
583
619
  pendingMatchIds: [],
584
620
  }))
621
+ ;(
622
+ [
623
+ [exitingMatchIds, 'onLeave'],
624
+ [enteringMatchIds, 'onEnter'],
625
+ [stayingMatchIds, 'onTransition'],
626
+ ] as const
627
+ ).forEach(([matchIds, hook]) => {
628
+ matchIds.forEach((id) => {
629
+ const match = this.getRouteMatch(id)!
630
+ const route = this.getRoute(match.routeId)
631
+ route.options[hook]?.(match)
632
+ })
633
+ })
585
634
 
586
635
  this.#emit({
587
636
  type: 'onLoad',
@@ -603,16 +652,17 @@ export class Router<
603
652
 
604
653
  this.latestLoadPromise = promise
605
654
 
655
+ this.latestLoadPromise.then(() => {
656
+ this.cleanMatches()
657
+ })
658
+
606
659
  return this.latestLoadPromise
607
660
  }
608
661
 
609
662
  #mergeMatches = (
610
- prevMatchesById: Record<
611
- string,
612
- RouteMatch<TRouteTree, ParseRoute<TRouteTree>>
613
- >,
663
+ prevMatchesById: Record<string, RouteMatch<TRouteTree>>,
614
664
  nextMatches: AnyRouteMatch[],
615
- ): Record<string, RouteMatch<TRouteTree, ParseRoute<TRouteTree>>> => {
665
+ ): Record<string, RouteMatch<TRouteTree>> => {
616
666
  const nextMatchesById: any = {
617
667
  ...prevMatchesById,
618
668
  }
@@ -672,10 +722,13 @@ export class Router<
672
722
  const outdatedMatchIds = Object.values(this.state.matchesById)
673
723
  .filter((match) => {
674
724
  const route = this.getRoute(match.routeId)
725
+
675
726
  return (
676
727
  !this.state.matchIds.includes(match.id) &&
677
728
  !this.state.pendingMatchIds.includes(match.id) &&
678
- match.preloadInvalidAt < now &&
729
+ (match.preloadMaxAge > -1
730
+ ? match.updatedAt + match.preloadMaxAge < now
731
+ : true) &&
679
732
  (route.options.gcMaxAge
680
733
  ? match.updatedAt + route.options.gcMaxAge < now
681
734
  : true)
@@ -701,7 +754,7 @@ export class Router<
701
754
  pathname: string,
702
755
  locationSearch: AnySearchSchema,
703
756
  opts?: { throwOnError?: boolean; debug?: boolean },
704
- ): RouteMatch<TRouteTree, ParseRoute<TRouteTree>>[] => {
757
+ ): RouteMatch<TRouteTree>[] => {
705
758
  let routeParams: AnyPathParams = {}
706
759
 
707
760
  let foundRoute = this.flatRoutes.find((route) => {
@@ -796,8 +849,8 @@ export class Router<
796
849
  params: routeParams,
797
850
  pathname: joinPaths([this.basepath, interpolatedPath]),
798
851
  updatedAt: Date.now(),
799
- invalidAt: Infinity,
800
- preloadInvalidAt: Infinity,
852
+ maxAge: -1,
853
+ preloadMaxAge: -1,
801
854
  routeSearch: {},
802
855
  search: {} as any,
803
856
  status: hasLoaders ? 'pending' : 'success',
@@ -918,13 +971,14 @@ export class Router<
918
971
  paramsError: match.paramsError,
919
972
  searchError: match.searchError,
920
973
  params: match.params,
921
- preloadInvalidAt: 0,
974
+ preloadMaxAge: 0,
922
975
  }))
923
976
  })
977
+ } else {
978
+ // If we're preloading, clean preload matches before we try and use them
979
+ this.cleanMatches()
924
980
  }
925
981
 
926
- this.cleanMatches()
927
-
928
982
  let firstBadMatchIndex: number | undefined
929
983
 
930
984
  // Check each match middleware to see if the route can be accessed
@@ -1003,7 +1057,9 @@ export class Router<
1003
1057
  if (
1004
1058
  match.isFetching ||
1005
1059
  (match.status === 'success' &&
1006
- !this.getIsInvalid({ matchId: match.id, preload: opts?.preload }))
1060
+ !isMatchInvalid(match, {
1061
+ preload: opts?.preload,
1062
+ }))
1007
1063
  ) {
1008
1064
  return this.getRouteMatch(match.id)?.loadPromise
1009
1065
  }
@@ -1099,8 +1155,6 @@ export class Router<
1099
1155
  })
1100
1156
 
1101
1157
  await Promise.all(matchPromises)
1102
-
1103
- this.cleanMatches()
1104
1158
  }
1105
1159
 
1106
1160
  reload = () => {
@@ -1351,7 +1405,8 @@ export class Router<
1351
1405
  pick(d, [
1352
1406
  'fetchedAt',
1353
1407
  'invalid',
1354
- 'invalidAt',
1408
+ 'preloadMaxAge',
1409
+ 'maxAge',
1355
1410
  'id',
1356
1411
  'loaderData',
1357
1412
  'status',
@@ -1710,22 +1765,17 @@ export class Router<
1710
1765
  })
1711
1766
 
1712
1767
  this.resetNextScroll = location.resetScroll ?? true
1713
- console.log('resetScroll', this.resetNextScroll)
1714
1768
 
1715
1769
  return this.latestLoadPromise
1716
1770
  }
1717
1771
 
1718
- getRouteMatch = (
1719
- id: string,
1720
- ): undefined | RouteMatch<TRouteTree, AnyRoute> => {
1772
+ getRouteMatch = (id: string): undefined | RouteMatch<TRouteTree> => {
1721
1773
  return this.state.matchesById[id]
1722
1774
  }
1723
1775
 
1724
1776
  setRouteMatch = (
1725
1777
  id: string,
1726
- updater: (
1727
- prev: RouteMatch<TRouteTree, AnyRoute>,
1728
- ) => RouteMatch<TRouteTree, AnyRoute>,
1778
+ updater: (prev: RouteMatch<TRouteTree>) => RouteMatch<TRouteTree>,
1729
1779
  ) => {
1730
1780
  this.__store.setState((prev) => {
1731
1781
  if (!prev.matchesById[id]) {
@@ -1757,29 +1807,24 @@ export class Router<
1757
1807
  const route = this.getRoute(match.routeId)
1758
1808
  const updatedAt = opts?.updatedAt ?? Date.now()
1759
1809
 
1760
- const preloadInvalidAt =
1761
- updatedAt +
1762
- (opts?.maxAge ??
1763
- route.options.preloadMaxAge ??
1764
- this.options.defaultPreloadMaxAge ??
1765
- 5000)
1810
+ const preloadMaxAge =
1811
+ opts?.maxAge ??
1812
+ route.options.preloadMaxAge ??
1813
+ this.options.defaultPreloadMaxAge ??
1814
+ 5000
1766
1815
 
1767
- const invalidAt =
1768
- updatedAt +
1769
- (opts?.maxAge ??
1770
- route.options.maxAge ??
1771
- this.options.defaultMaxAge ??
1772
- Infinity)
1816
+ const maxAge =
1817
+ opts?.maxAge ?? route.options.maxAge ?? this.options.defaultMaxAge ?? -1
1773
1818
 
1774
1819
  this.setRouteMatch(id, (s) => ({
1775
1820
  ...s,
1776
1821
  error: undefined,
1777
1822
  status: 'success',
1778
1823
  isFetching: false,
1779
- updatedAt: Date.now(),
1824
+ updatedAt: updatedAt,
1780
1825
  loaderData: functionalUpdate(updater, s.loaderData),
1781
- preloadInvalidAt,
1782
- invalidAt,
1826
+ preloadMaxAge,
1827
+ maxAge,
1783
1828
  }))
1784
1829
  }
1785
1830
 
@@ -1815,27 +1860,6 @@ export class Router<
1815
1860
  return this.reload()
1816
1861
  }
1817
1862
  }
1818
-
1819
- getIsInvalid = (opts?: { matchId: string; preload?: boolean }): boolean => {
1820
- if (!opts?.matchId) {
1821
- return !!this.state.matches.find((d) =>
1822
- this.getIsInvalid({ matchId: d.id, preload: opts?.preload }),
1823
- )
1824
- }
1825
-
1826
- const match = this.getRouteMatch(opts?.matchId)
1827
-
1828
- if (!match) {
1829
- return false
1830
- }
1831
-
1832
- const now = Date.now()
1833
-
1834
- return (
1835
- match.invalid ||
1836
- (opts?.preload ? match.preloadInvalidAt : match.invalidAt) < now
1837
- )
1838
- }
1839
1863
  }
1840
1864
 
1841
1865
  // Detect if we're in the DOM
@@ -1905,3 +1929,22 @@ export function lazyFn<
1905
1929
  return imported[key || 'default'](...args)
1906
1930
  }
1907
1931
  }
1932
+
1933
+ export function isMatchInvalid(
1934
+ match: AnyRouteMatch,
1935
+ opts?: { preload?: boolean },
1936
+ ) {
1937
+ const now = Date.now()
1938
+
1939
+ if (match.invalid) {
1940
+ return true
1941
+ }
1942
+
1943
+ if (opts?.preload) {
1944
+ return match.preloadMaxAge < 0
1945
+ ? false
1946
+ : match.updatedAt + match.preloadMaxAge < now
1947
+ }
1948
+
1949
+ return match.maxAge < 0 ? false : match.updatedAt + match.maxAge < now
1950
+ }