@tanstack/router-core 0.0.1-beta.35 → 0.0.1-beta.36

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.35",
4
+ "version": "0.0.1-beta.36",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
package/src/route.ts CHANGED
@@ -140,7 +140,6 @@ export function createRoute<
140
140
  actionState.status = 'success'
141
141
  return res
142
142
  } catch (err) {
143
- console.log('tanner')
144
143
  console.error(err)
145
144
  actionState.error = err
146
145
  actionState.status = 'error'
@@ -123,12 +123,17 @@ export type RouteOptions<
123
123
  // An asynchronous function made available to the route for performing asynchronous or mutative actions that
124
124
  // might invalidate the route's data.
125
125
  action?: ActionFn<TActionPayload, TActionResponse>
126
- // This async function is called before a route is loaded. If an error is thrown, the navigation is cancelled.
127
- // If you want to redirect instead, throw a call to the `router.navigate()` function
126
+ // This async function is called before a route is loaded.
127
+ // If an error is thrown here, the route's loader will not be called.
128
+ // If thrown during a navigation, the navigation will be cancelled and the error will be passed to the `onLoadError` function.
129
+ // If thrown during a preload event, the error will be logged to the console.
128
130
  beforeLoad?: (opts: {
129
131
  router: Router<any, any, unknown>
130
132
  match: RouteMatch
131
133
  }) => Promise<void> | void
134
+ // This function will be called if the route's loader throws an error **during an attempted navigation**.
135
+ // If you want to redirect due to an error, call `router.navigate()` from within this function.
136
+ onLoadError?: (err: any) => void
132
137
  // This function is called
133
138
  // when moving from an inactive state to an active one. Likewise, when moving from
134
139
  // an active to an inactive state, the return function (if provided) is called.
package/src/routeMatch.ts CHANGED
@@ -111,7 +111,7 @@ export function createRouteMatch<
111
111
  validate: () => {
112
112
  // Validate the search params and stabilize them
113
113
  const parentSearch =
114
- routeMatch.parentMatch?.search ?? router.__location.search
114
+ routeMatch.parentMatch?.search ?? router.state.currentLocation.search
115
115
 
116
116
  try {
117
117
  const prevSearch = routeMatch.routeSearch
@@ -180,7 +180,9 @@ export function createRouteMatch<
180
180
  if (loaderOpts?.preload && minMaxAge > 0) {
181
181
  // If the match is currently active, don't preload it
182
182
  if (
183
- router.state.matches.find((d) => d.matchId === routeMatch.matchId)
183
+ router.state.currentMatches.find(
184
+ (d) => d.matchId === routeMatch.matchId,
185
+ )
184
186
  ) {
185
187
  return
186
188
  }
package/src/router.ts CHANGED
@@ -201,21 +201,18 @@ export interface RouterState<
201
201
  TState extends LocationState = LocationState,
202
202
  > {
203
203
  status: 'idle' | 'loading'
204
- location: Location<TSearchObj, TState>
205
- matches: RouteMatch[]
204
+ latestLocation: Location<TSearchObj, TState>
205
+ currentMatches: RouteMatch[]
206
+ currentLocation: Location<TSearchObj, TState>
207
+ pendingMatches?: RouteMatch[]
208
+ pendingLocation?: Location<TSearchObj, TState>
206
209
  lastUpdated: number
207
210
  actions: Record<string, Action>
208
211
  loaders: Record<string, Loader>
209
- pending?: PendingState
210
212
  isFetching: boolean
211
213
  isPreloading: boolean
212
214
  }
213
215
 
214
- export interface PendingState {
215
- location: Location
216
- matches: RouteMatch[]
217
- }
218
-
219
216
  type Listener = (router: Router<any, any, any>) => void
220
217
 
221
218
  export type ListenerFn = () => void
@@ -259,13 +256,13 @@ type LinkCurrentTargetElement = {
259
256
  export interface DehydratedRouterState
260
257
  extends Pick<
261
258
  RouterState,
262
- 'status' | 'location' | 'lastUpdated' | 'location'
259
+ 'status' | 'latestLocation' | 'currentLocation' | 'lastUpdated'
263
260
  > {
264
- matches: DehydratedRouteMatch[]
261
+ currentMatches: DehydratedRouteMatch[]
265
262
  }
266
263
 
267
264
  export interface DehydratedRouter<TRouterContext = unknown> {
268
- location: Router['__location']
265
+ // location: Router['__location']
269
266
  state: DehydratedRouterState
270
267
  context: TRouterContext
271
268
  }
@@ -304,7 +301,7 @@ export interface Router<
304
301
  basepath: string
305
302
  // Internal:
306
303
  listeners: Listener[]
307
- __location: Location<TAllRouteInfo['fullSearchSchema']>
304
+ // __location: Location<TAllRouteInfo['fullSearchSchema']>
308
305
  navigateTimeout?: Timeout
309
306
  nextAction?: 'push' | 'replace'
310
307
  state: RouterState<TAllRouteInfo['fullSearchSchema']>
@@ -403,8 +400,9 @@ const createDefaultHistory = () =>
403
400
  function getInitialRouterState(): RouterState {
404
401
  return {
405
402
  status: 'idle',
406
- location: null!,
407
- matches: [],
403
+ latestLocation: null!,
404
+ currentLocation: null!,
405
+ currentMatches: [],
408
406
  actions: {},
409
407
  loaders: {},
410
408
  lastUpdated: Date.now(),
@@ -444,7 +442,6 @@ export function createRouter<
444
442
  basepath: '',
445
443
  routeTree: undefined!,
446
444
  routesById: {} as any,
447
- __location: undefined!,
448
445
  //
449
446
  resolveNavigation: () => {},
450
447
  matchCache: {},
@@ -466,12 +463,14 @@ export function createRouter<
466
463
  notify: (): void => {
467
464
  const isFetching =
468
465
  router.state.status === 'loading' ||
469
- router.state.matches.some((d) => d.isFetching)
466
+ router.state.currentMatches.some((d) => d.isFetching)
470
467
 
471
468
  const isPreloading = Object.values(router.matchCache).some(
472
469
  (d) =>
473
470
  d.match.isFetching &&
474
- !router.state.matches.find((dd) => dd.matchId === d.match.matchId),
471
+ !router.state.currentMatches.find(
472
+ (dd) => dd.matchId === d.match.matchId,
473
+ ),
475
474
  )
476
475
 
477
476
  if (
@@ -485,21 +484,20 @@ export function createRouter<
485
484
  }
486
485
  }
487
486
 
488
- cascadeLoaderData(router.state.matches)
487
+ cascadeLoaderData(router.state.currentMatches)
489
488
  router.listeners.forEach((listener) => listener(router))
490
489
  },
491
490
 
492
491
  dehydrate: () => {
493
492
  return {
494
- location: router.__location,
495
493
  state: {
496
494
  ...pick(router.state, [
495
+ 'latestLocation',
496
+ 'currentLocation',
497
497
  'status',
498
- 'location',
499
498
  'lastUpdated',
500
- 'location',
501
499
  ]),
502
- matches: router.state.matches.map((match) =>
500
+ currentMatches: router.state.currentMatches.map((match) =>
503
501
  pick(match, [
504
502
  'matchId',
505
503
  'status',
@@ -516,18 +514,22 @@ export function createRouter<
516
514
 
517
515
  hydrate: (dehydratedState) => {
518
516
  // Update the location
519
- router.__location = dehydratedState.location
517
+ router.state.latestLocation = dehydratedState.state.latestLocation
518
+ router.state.currentLocation = dehydratedState.state.currentLocation
520
519
 
521
520
  // Update the context
522
521
  router.options.context = dehydratedState.context
523
522
 
524
523
  // Match the routes
525
- const matches = router.matchRoutes(router.__location.pathname, {
526
- strictParseParams: true,
527
- })
524
+ const currentMatches = router.matchRoutes(
525
+ router.state.latestLocation.pathname,
526
+ {
527
+ strictParseParams: true,
528
+ },
529
+ )
528
530
 
529
- matches.forEach((match, index) => {
530
- const dehydratedMatch = dehydratedState.state.matches[index]
531
+ currentMatches.forEach((match, index) => {
532
+ const dehydratedMatch = dehydratedState.state.currentMatches[index]
531
533
  invariant(
532
534
  dehydratedMatch,
533
535
  'Oh no! Dehydrated route matches did not match the active state of the router 😬',
@@ -535,34 +537,24 @@ export function createRouter<
535
537
  Object.assign(match, dehydratedMatch)
536
538
  })
537
539
 
538
- matches.forEach((match) => match.__.validate())
540
+ currentMatches.forEach((match) => match.__.validate())
539
541
 
540
542
  router.state = {
541
543
  ...router.state,
542
544
  ...dehydratedState,
543
- matches,
545
+ currentMatches,
544
546
  }
545
547
  },
546
548
 
547
549
  mount: () => {
548
- const next = router.__.buildLocation({
549
- to: '.',
550
- search: true,
551
- hash: true,
552
- })
553
-
554
- // If the current location isn't updated, trigger a navigation
555
- // to the current location. Otherwise, load the current location.
556
- // if (next.href !== router.__location.href) {
557
- // router.__.commitLocation(next, true)
558
- // }
559
-
560
- if (!router.state.matches.length) {
550
+ if (!router.state.currentMatches.length) {
561
551
  router.load()
562
552
  }
563
553
 
564
554
  const unsub = router.history.listen((event) => {
565
- router.load(router.__.parseLocation(event.location, router.__location))
555
+ router.load(
556
+ router.__.parseLocation(event.location, router.state.latestLocation),
557
+ )
566
558
  })
567
559
 
568
560
  // addEventListener does not exist in React Native, but window does
@@ -589,12 +581,14 @@ export function createRouter<
589
581
 
590
582
  update: (opts) => {
591
583
  const newHistory = opts?.history !== router.history
592
- if (!router.__location || newHistory) {
584
+ if (!router.state.latestLocation || newHistory) {
593
585
  if (opts?.history) {
594
586
  router.history = opts.history
595
587
  }
596
- router.__location = router.__.parseLocation(router.history.location)
597
- router.state.location = router.__location
588
+ router.state.latestLocation = router.__.parseLocation(
589
+ router.history.location,
590
+ )
591
+ router.state.currentLocation = router.state.latestLocation
598
592
  }
599
593
 
600
594
  Object.assign(router.options, opts)
@@ -613,8 +607,8 @@ export function createRouter<
613
607
 
614
608
  cancelMatches: () => {
615
609
  ;[
616
- ...router.state.matches,
617
- ...(router.state.pending?.matches ?? []),
610
+ ...router.state.currentMatches,
611
+ ...(router.state.pendingMatches ?? []),
618
612
  ].forEach((match) => {
619
613
  match.cancel()
620
614
  })
@@ -626,61 +620,52 @@ export function createRouter<
626
620
 
627
621
  if (next) {
628
622
  // Ingest the new location
629
- router.__location = next
623
+ router.state.latestLocation = next
630
624
  }
631
625
 
632
626
  // Cancel any pending matches
633
627
  router.cancelMatches()
634
628
 
635
629
  // Match the routes
636
- const matches = router.matchRoutes(router.__location.pathname, {
630
+ const matches = router.matchRoutes(router.state.latestLocation.pathname, {
637
631
  strictParseParams: true,
638
632
  })
639
633
 
640
634
  if (typeof document !== 'undefined') {
641
635
  router.state = {
642
636
  ...router.state,
643
- pending: {
644
- matches: matches,
645
- location: router.__location,
646
- },
647
637
  status: 'loading',
638
+ pendingMatches: matches,
639
+ pendingLocation: router.state.latestLocation,
648
640
  }
649
641
  } else {
650
642
  router.state = {
651
643
  ...router.state,
652
- matches: matches,
653
- location: router.__location,
654
644
  status: 'loading',
645
+ currentMatches: matches,
646
+ currentLocation: router.state.latestLocation,
655
647
  }
656
648
  }
657
649
 
658
- // Check if each match middleware to see if the route can be accessed
659
- try {
660
- await Promise.all(
661
- matches.map((match) =>
662
- match.options.beforeLoad?.({
663
- router: router as any,
664
- match,
665
- }),
666
- ),
667
- )
668
- } catch (err: any) {
669
- console.info(err)
670
- invariant(false, `A route's beforeLoad middleware failed! 👆`)
671
- }
672
-
673
650
  router.notify()
674
651
 
675
652
  // Load the matches
676
- await router.loadMatches(matches)
653
+ try {
654
+ await router.loadMatches(matches)
655
+ } catch (err: any) {
656
+ console.log(err)
657
+ invariant(
658
+ false,
659
+ 'Matches failed to load due to error above ☝️. Navigation cancelled!',
660
+ )
661
+ }
677
662
 
678
663
  if (router.startedLoadingAt !== id) {
679
664
  // Ignore side-effects of match loading
680
665
  return router.navigationPromise
681
666
  }
682
667
 
683
- const previousMatches = router.state.matches
668
+ const previousMatches = router.state.currentMatches
684
669
 
685
670
  const exiting: RouteMatch[] = [],
686
671
  staying: RouteMatch[] = []
@@ -754,10 +739,11 @@ export function createRouter<
754
739
 
755
740
  router.state = {
756
741
  ...router.state,
757
- location: router.__location,
758
- matches,
759
- pending: undefined,
760
742
  status: 'idle',
743
+ currentLocation: router.state.latestLocation,
744
+ currentMatches: matches,
745
+ pendingLocation: undefined,
746
+ pendingMatches: undefined,
761
747
  }
762
748
 
763
749
  router.notify()
@@ -785,7 +771,7 @@ export function createRouter<
785
771
  })
786
772
  },
787
773
 
788
- loadRoute: async (navigateOpts = router.__location) => {
774
+ loadRoute: async (navigateOpts = router.state.latestLocation) => {
789
775
  const next = router.buildNext(navigateOpts)
790
776
  const matches = router.matchRoutes(next.pathname, {
791
777
  strictParseParams: true,
@@ -794,11 +780,15 @@ export function createRouter<
794
780
  return matches
795
781
  },
796
782
 
797
- preloadRoute: async (navigateOpts = router.__location, loaderOpts) => {
783
+ preloadRoute: async (
784
+ navigateOpts = router.state.latestLocation,
785
+ loaderOpts,
786
+ ) => {
798
787
  const next = router.buildNext(navigateOpts)
799
788
  const matches = router.matchRoutes(next.pathname, {
800
789
  strictParseParams: true,
801
790
  })
791
+
802
792
  await router.loadMatches(matches, {
803
793
  preload: true,
804
794
  maxAge:
@@ -825,8 +815,8 @@ export function createRouter<
825
815
  }
826
816
 
827
817
  const existingMatches = [
828
- ...router.state.matches,
829
- ...(router.state.pending?.matches ?? []),
818
+ ...router.state.currentMatches,
819
+ ...(router.state.pendingMatches ?? []),
830
820
  ]
831
821
 
832
822
  const recurse = async (routes: Route<any, any>[]): Promise<void> => {
@@ -857,13 +847,13 @@ export function createRouter<
857
847
  route.options.caseSensitive ?? router.options.caseSensitive,
858
848
  })
859
849
 
860
- console.log(
861
- router.basepath,
862
- route.fullPath,
863
- fuzzy,
864
- pathname,
865
- matchParams,
866
- )
850
+ // console.log(
851
+ // router.basepath,
852
+ // route.fullPath,
853
+ // fuzzy,
854
+ // pathname,
855
+ // matchParams,
856
+ // )
867
857
 
868
858
  if (matchParams) {
869
859
  let parsedParams
@@ -931,10 +921,30 @@ export function createRouter<
931
921
  },
932
922
 
933
923
  loadMatches: async (resolvedMatches, loaderOpts) => {
934
- const matchPromises = resolvedMatches.map(async (match) => {
924
+ resolvedMatches.forEach(async (match) => {
935
925
  // Validate the match (loads search params etc)
936
926
  match.__.validate()
927
+ })
937
928
 
929
+ // Check each match middleware to see if the route can be accessed
930
+ await Promise.all(
931
+ resolvedMatches.map(async (match) => {
932
+ try {
933
+ await match.options.beforeLoad?.({
934
+ router: router as any,
935
+ match,
936
+ })
937
+ } catch (err) {
938
+ if (!loaderOpts?.preload) {
939
+ match.options.onLoadError?.(err)
940
+ }
941
+
942
+ throw err
943
+ }
944
+ }),
945
+ )
946
+
947
+ const matchPromises = resolvedMatches.map(async (match) => {
938
948
  const search = match.search as { __data?: any }
939
949
 
940
950
  if (search.__data?.matchId && search.__data.matchId !== match.matchId) {
@@ -1004,8 +1014,8 @@ export function createRouter<
1004
1014
  .matchRoutes(next.pathname)
1005
1015
  .map((d) => d.matchId)
1006
1016
  ;[
1007
- ...router.state.matches,
1008
- ...(router.state.pending?.matches ?? []),
1017
+ ...router.state.currentMatches,
1018
+ ...(router.state.pendingMatches ?? []),
1009
1019
  ].forEach((match) => {
1010
1020
  if (unloadedMatchIds.includes(match.matchId)) {
1011
1021
  match.invalidate()
@@ -1037,13 +1047,13 @@ export function createRouter<
1037
1047
  const next = router.buildNext(location)
1038
1048
 
1039
1049
  if (opts?.pending) {
1040
- if (!router.state.pending?.location) {
1050
+ if (!router.state.pendingLocation) {
1041
1051
  return false
1042
1052
  }
1043
1053
 
1044
1054
  return !!matchPathname(
1045
1055
  router.basepath,
1046
- router.state.pending.location.pathname,
1056
+ router.state.pendingLocation.pathname,
1047
1057
  {
1048
1058
  ...opts,
1049
1059
  to: next.pathname,
@@ -1051,10 +1061,14 @@ export function createRouter<
1051
1061
  )
1052
1062
  }
1053
1063
 
1054
- return !!matchPathname(router.basepath, router.state.location.pathname, {
1055
- ...opts,
1056
- to: next.pathname,
1057
- })
1064
+ return !!matchPathname(
1065
+ router.basepath,
1066
+ router.state.currentLocation.pathname,
1067
+ {
1068
+ ...opts,
1069
+ to: next.pathname,
1070
+ },
1071
+ )
1058
1072
  },
1059
1073
 
1060
1074
  navigate: async ({ from, to = '.', search, hash, replace, params }) => {
@@ -1133,13 +1147,14 @@ export function createRouter<
1133
1147
  userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
1134
1148
 
1135
1149
  // Compare path/hash for matches
1136
- const pathIsEqual = router.state.location.pathname === next.pathname
1137
- const currentPathSplit = router.state.location.pathname.split('/')
1150
+ const pathIsEqual =
1151
+ router.state.currentLocation.pathname === next.pathname
1152
+ const currentPathSplit = router.state.currentLocation.pathname.split('/')
1138
1153
  const nextPathSplit = next.pathname.split('/')
1139
1154
  const pathIsFuzzyEqual = nextPathSplit.every(
1140
1155
  (d, i) => d === currentPathSplit[i],
1141
1156
  )
1142
- const hashIsEqual = router.state.location.hash === next.hash
1157
+ const hashIsEqual = router.state.currentLocation.hash === next.hash
1143
1158
  // Combine the matches based on user options
1144
1159
  const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
1145
1160
  const hashTest = activeOptions?.includeHash ? hashIsEqual : true
@@ -1169,10 +1184,15 @@ export function createRouter<
1169
1184
  // The click handler
1170
1185
  const handleFocus = (e: MouseEvent) => {
1171
1186
  if (preload) {
1172
- router.preloadRoute(nextOpts, {
1173
- maxAge: userPreloadMaxAge,
1174
- gcMaxAge: userPreloadGcMaxAge,
1175
- })
1187
+ router
1188
+ .preloadRoute(nextOpts, {
1189
+ maxAge: userPreloadMaxAge,
1190
+ gcMaxAge: userPreloadGcMaxAge,
1191
+ })
1192
+ .catch((err) => {
1193
+ console.log(err)
1194
+ console.warn('Error preloading route! ☝️')
1195
+ })
1176
1196
  }
1177
1197
  }
1178
1198
 
@@ -1186,10 +1206,15 @@ export function createRouter<
1186
1206
 
1187
1207
  target.preloadTimeout = setTimeout(() => {
1188
1208
  target.preloadTimeout = null
1189
- router.preloadRoute(nextOpts, {
1190
- maxAge: userPreloadMaxAge,
1191
- gcMaxAge: userPreloadGcMaxAge,
1192
- })
1209
+ router
1210
+ .preloadRoute(nextOpts, {
1211
+ maxAge: userPreloadMaxAge,
1212
+ gcMaxAge: userPreloadGcMaxAge,
1213
+ })
1214
+ .catch((err) => {
1215
+ console.log(err)
1216
+ console.warn('Error preloading route! ☝️')
1217
+ })
1193
1218
  }, preloadDelay)
1194
1219
  }
1195
1220
  }
@@ -1299,8 +1324,8 @@ export function createRouter<
1299
1324
 
1300
1325
  buildLocation: (dest: BuildNextOptions = {}): Location => {
1301
1326
  const fromPathname = dest.fromCurrent
1302
- ? router.__location.pathname
1303
- : dest.from ?? router.__location.pathname
1327
+ ? router.state.latestLocation.pathname
1328
+ : dest.from ?? router.state.latestLocation.pathname
1304
1329
 
1305
1330
  let pathname = resolvePath(
1306
1331
  router.basepath ?? '/',
@@ -1308,9 +1333,12 @@ export function createRouter<
1308
1333
  `${dest.to ?? '.'}`,
1309
1334
  )
1310
1335
 
1311
- const fromMatches = router.matchRoutes(router.__location.pathname, {
1312
- strictParseParams: true,
1313
- })
1336
+ const fromMatches = router.matchRoutes(
1337
+ router.state.latestLocation.pathname,
1338
+ {
1339
+ strictParseParams: true,
1340
+ },
1341
+ )
1314
1342
 
1315
1343
  const toMatches = router.matchRoutes(pathname)
1316
1344
 
@@ -1336,9 +1364,9 @@ export function createRouter<
1336
1364
  const preFilteredSearch = dest.__preSearchFilters?.length
1337
1365
  ? dest.__preSearchFilters.reduce(
1338
1366
  (prev, next) => next(prev),
1339
- router.__location.search,
1367
+ router.state.latestLocation.search,
1340
1368
  )
1341
- : router.__location.search
1369
+ : router.state.latestLocation.search
1342
1370
 
1343
1371
  // Then the link/navigate function
1344
1372
  const destSearch =
@@ -1359,22 +1387,22 @@ export function createRouter<
1359
1387
  : destSearch
1360
1388
 
1361
1389
  const search = replaceEqualDeep(
1362
- router.__location.search,
1390
+ router.state.latestLocation.search,
1363
1391
  postFilteredSearch,
1364
1392
  )
1365
1393
 
1366
1394
  const searchStr = router.options.stringifySearch(search)
1367
1395
  let hash =
1368
1396
  dest.hash === true
1369
- ? router.__location.hash
1370
- : functionalUpdate(dest.hash!, router.__location.hash)
1397
+ ? router.state.latestLocation.hash
1398
+ : functionalUpdate(dest.hash!, router.state.latestLocation.hash)
1371
1399
  hash = hash ? `#${hash}` : ''
1372
1400
 
1373
1401
  return {
1374
1402
  pathname,
1375
1403
  search,
1376
1404
  searchStr,
1377
- state: router.__location.state,
1405
+ state: router.state.latestLocation.state,
1378
1406
  hash,
1379
1407
  href: `${pathname}${searchStr}${hash}`,
1380
1408
  key: dest.key,