@tanstack/react-router 0.0.1-beta.226 → 0.0.1-beta.228

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.226",
4
+ "version": "0.0.1-beta.228",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -42,7 +42,7 @@
42
42
  "@babel/runtime": "^7.16.7",
43
43
  "tiny-invariant": "^1.3.1",
44
44
  "tiny-warning": "^1.0.3",
45
- "@tanstack/history": "0.0.1-beta.226"
45
+ "@tanstack/history": "0.0.1-beta.228"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "rollup --config rollup.config.js"
package/src/Matches.tsx CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  RoutePaths,
15
15
  } from './routeInfo'
16
16
  import { RegisteredRouter } from './router'
17
- import { NoInfer, StrictOrFrom } from './utils'
17
+ import { NoInfer, StrictOrFrom, pick } from './utils'
18
18
 
19
19
  export interface RouteMatch<
20
20
  TRouteTree extends AnyRoute = AnyRoute,
@@ -35,7 +35,6 @@ export interface RouteMatch<
35
35
  loaderData?: RouteById<TRouteTree, TRouteId>['types']['loaderData']
36
36
  __resolveLoadPromise?: () => void
37
37
  context: RouteById<TRouteTree, TRouteId>['types']['allContext']
38
- routeSearch: RouteById<TRouteTree, TRouteId>['types']['searchSchema']
39
38
  search: FullSearchSchema<TRouteTree> &
40
39
  RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
41
40
  fetchedAt: number
@@ -85,7 +84,6 @@ export function Matches() {
85
84
  )
86
85
  }
87
86
 
88
- const defaultPending = () => null
89
87
  function SafeFragment(props: any) {
90
88
  return <>{props.children}</>
91
89
  }
@@ -98,17 +96,17 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
98
96
  const locationKey = useRouterState().location.state?.key
99
97
 
100
98
  const PendingComponent = (route.options.pendingComponent ??
101
- options.defaultPendingComponent ??
102
- defaultPending) as any
99
+ options.defaultPendingComponent) as any
103
100
 
104
101
  const routeErrorComponent =
105
102
  route.options.errorComponent ??
106
103
  options.defaultErrorComponent ??
107
104
  ErrorComponent
108
105
 
109
- const ResolvedSuspenseBoundary = route.options.wrapInSuspense
110
- ? React.Suspense
111
- : SafeFragment
106
+ const ResolvedSuspenseBoundary =
107
+ route.options.wrapInSuspense ?? PendingComponent
108
+ ? React.Suspense
109
+ : SafeFragment
112
110
 
113
111
  const errorComponent = routeErrorComponent
114
112
  ? React.useCallback(
@@ -125,6 +123,8 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
125
123
  )
126
124
  : undefined
127
125
 
126
+ const ResolvedCatchBoundary = errorComponent ? CatchBoundary : SafeFragment
127
+
128
128
  return (
129
129
  <matchesContext.Provider value={matches}>
130
130
  <ResolvedSuspenseBoundary
@@ -135,21 +135,15 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
135
135
  useParams: route.useParams,
136
136
  })}
137
137
  >
138
- {errorComponent ? (
139
- <CatchBoundary
140
- resetKey={locationKey}
141
- errorComponent={errorComponent}
142
- onCatch={() => {
143
- warning(false, `Error in route match: ${match.id}`)
144
- }}
145
- >
146
- <MatchInner match={match} />
147
- </CatchBoundary>
148
- ) : (
149
- <SafeFragment>
150
- <MatchInner match={match} />
151
- </SafeFragment>
152
- )}
138
+ <ResolvedCatchBoundary
139
+ resetKey={locationKey}
140
+ errorComponent={errorComponent}
141
+ onCatch={() => {
142
+ warning(false, `Error in route match: ${match.id}`)
143
+ }}
144
+ >
145
+ <MatchInner match={match} />
146
+ </ResolvedCatchBoundary>
153
147
  </ResolvedSuspenseBoundary>
154
148
  </matchesContext.Provider>
155
149
  )
@@ -1,11 +1,4 @@
1
- import {
2
- HistoryLocation,
3
- HistoryState,
4
- RouterHistory,
5
- createBrowserHistory,
6
- } from '@tanstack/history'
7
1
  import * as React from 'react'
8
- import invariant from 'tiny-invariant'
9
2
  import warning from 'tiny-warning'
10
3
  import { Matches } from './Matches'
11
4
  import {
@@ -16,53 +9,18 @@ import {
16
9
  ToOptions,
17
10
  } from './link'
18
11
  import { ParsedLocation } from './location'
19
- import {
20
- cleanPath,
21
- interpolatePath,
22
- joinPaths,
23
- matchPathname,
24
- parsePathname,
25
- resolvePath,
26
- trimPath,
27
- trimPathRight,
28
- } from './path'
29
- import { isRedirect } from './redirects'
30
- import {
31
- AnyPathParams,
32
- AnyRoute,
33
- AnySearchSchema,
34
- LoaderFnContext,
35
- Route,
36
- } from './route'
37
- import {
38
- FullSearchSchema,
39
- RouteById,
40
- RoutePaths,
41
- RoutesById,
42
- RoutesByPath,
43
- } from './routeInfo'
12
+ import { AnyRoute } from './route'
13
+ import { RouteById, RoutePaths } from './routeInfo'
44
14
  import {
45
15
  BuildNextOptions,
46
- DehydratedRouteMatch,
47
16
  RegisteredRouter,
48
17
  Router,
49
18
  RouterOptions,
50
19
  RouterState,
51
- componentTypes,
52
20
  } from './router'
53
- import {
54
- NoInfer,
55
- PickAsRequired,
56
- functionalUpdate,
57
- last,
58
- deepEqual,
59
- pick,
60
- replaceEqualDeep,
61
- useStableCallback,
62
- escapeJSON,
63
- } from './utils'
21
+ import { NoInfer, PickAsRequired } from './utils'
64
22
  import { MatchRouteOptions } from './Matches'
65
- import { AnyRouteMatch, RouteMatch } from './Matches'
23
+ import { RouteMatch } from './Matches'
66
24
 
67
25
  export interface CommitLocationOptions {
68
26
  replace?: boolean
@@ -108,31 +66,6 @@ export type BuildLocationFn<TRouteTree extends AnyRoute> = (
108
66
 
109
67
  export type InjectedHtmlEntry = string | (() => Promise<string> | string)
110
68
 
111
- // export type RouterContext<
112
- // TRouteTree extends AnyRoute,
113
- // // TDehydrated extends Record<string, any>,
114
- // > = {
115
- // buildLink: BuildLinkFn<TRouteTree>
116
- // state: RouterState<TRouteTree>
117
- // navigate: NavigateFn<TRouteTree>
118
- // matchRoute: MatchRouteFn<TRouteTree>
119
- // routeTree: TRouteTree
120
- // routesById: RoutesById<TRouteTree>
121
- // options: RouterOptions<TRouteTree>
122
- // history: RouterHistory
123
- // load: LoadFn
124
- // buildLocation: BuildLocationFn<TRouteTree>
125
- // subscribe: Router<TRouteTree>['subscribe']
126
- // resetNextScrollRef: React.MutableRefObject<boolean>
127
- // injectedHtmlRef: React.MutableRefObject<InjectedHtmlEntry[]>
128
- // injectHtml: (entry: InjectedHtmlEntry) => void
129
- // dehydrateData: <T>(
130
- // key: any,
131
- // getData: T | (() => Promise<T> | T),
132
- // ) => () => void
133
- // hydrateData: <T>(key: any) => T | undefined
134
- // }
135
-
136
69
  export const routerContext = React.createContext<Router<any>>(null!)
137
70
 
138
71
  if (typeof document !== 'undefined') {
package/src/fileRoute.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  TrimPathLeft,
15
15
  RouteConstraints,
16
16
  } from './route'
17
- import { Assign, AssignAll, Expand, IsAny } from './utils'
17
+ import { Assign, Expand, IsAny } from './utils'
18
18
 
19
19
  export interface FileRoutesByPath {
20
20
  // '/': {
@@ -86,13 +86,14 @@ export class FileRoute<
86
86
 
87
87
  createRoute = <
88
88
  TSearchSchema extends RouteConstraints['TSearchSchema'] = {},
89
- TFullSearchSchema extends RouteConstraints['TFullSearchSchema'] = ResolveFullSearchSchema<
89
+ TFullSearchSchema extends
90
+ RouteConstraints['TFullSearchSchema'] = ResolveFullSearchSchema<
90
91
  TParentRoute,
91
92
  TSearchSchema
92
93
  >,
93
- TParams extends RouteConstraints['TParams'] = ParsePathParams<TPath> extends never
94
- ? AnyPathParams
95
- : Record<ParsePathParams<TPath>, RouteConstraints['TPath']>,
94
+ TParams extends RouteConstraints['TParams'] = Expand<
95
+ Record<ParsePathParams<TPath>, string>
96
+ >,
96
97
  TAllParams extends RouteConstraints['TAllParams'] = MergeFromFromParent<
97
98
  TParentRoute['types']['allParams'],
98
99
  TParams
@@ -112,7 +113,7 @@ export class FileRoute<
112
113
  RouteOptions<
113
114
  TParentRoute,
114
115
  string,
115
- string,
116
+ TPath,
116
117
  TSearchSchema,
117
118
  TFullSearchSchema,
118
119
  TParams,
package/src/route.ts CHANGED
@@ -137,7 +137,8 @@ export type BaseRouteOptions<
137
137
  TRouteContext
138
138
  >
139
139
  }) & {
140
- loader?: RouteLoadFn<
140
+ key?: (opts: { search: TFullSearchSchema; location: ParsedLocation }) => any
141
+ loader?: RouteLoaderFn<
141
142
  TAllParams,
142
143
  TFullSearchSchema,
143
144
  NoInfer<TAllContext>,
@@ -265,7 +266,7 @@ export type ParentParams<TParentParams> = AnyPathParams extends TParentParams
265
266
  [Key in keyof TParentParams]?: DefinedPathParamWarning
266
267
  }
267
268
 
268
- export type RouteLoadFn<
269
+ export type RouteLoaderFn<
269
270
  TAllParams = {},
270
271
  TFullSearchSchema extends Record<string, any> = {},
271
272
  TAllContext extends Record<string, any> = AnyContext,
package/src/router.ts CHANGED
@@ -553,67 +553,21 @@ export class Router<
553
553
  return
554
554
  })
555
555
 
556
- const matches = matchedRoutes.map((route, index) => {
557
- const interpolatedPath = interpolatePath(route.path, routeParams)
558
- const matchId = interpolatePath(route.id, routeParams, true)
556
+ const matches: AnyRouteMatch[] = []
559
557
 
560
- // Waste not, want not. If we already have a match for this route,
561
- // reuse it. This is important for layout routes, which might stick
562
- // around between navigation actions that only change leaf routes.
563
- const existingMatch = getRouteMatch(this.state, matchId)
558
+ matchedRoutes.forEach((route, index) => {
559
+ // Take each matched route and resolve + validate its search params
560
+ // This has to happen serially because each route's search params
561
+ // can depend on the parent route's search params
562
+ // It must also happen before we create the match so that we can
563
+ // pass the search params to the route's potential key function
564
+ // which is used to uniquely identify the route match in state
564
565
 
565
- const cause = this.state.matches.find((d) => d.id === matchId)
566
- ? 'stay'
567
- : 'enter'
568
-
569
- if (existingMatch) {
570
- return { ...existingMatch, cause }
571
- }
566
+ const parentMatch = matches[index - 1]
572
567
 
573
- // Create a fresh route match
574
- const hasLoaders = !!(
575
- route.options.loader ||
576
- componentTypes.some((d) => (route.options[d] as any)?.preload)
577
- )
578
-
579
- const routeMatch: AnyRouteMatch = {
580
- id: matchId,
581
- routeId: route.id,
582
- params: routeParams,
583
- pathname: joinPaths([this.basepath, interpolatedPath]),
584
- updatedAt: Date.now(),
585
- routeSearch: {},
586
- search: {} as any,
587
- status: hasLoaders ? 'pending' : 'success',
588
- isFetching: false,
589
- invalid: false,
590
- error: undefined,
591
- paramsError: parseErrors[index],
592
- searchError: undefined,
593
- loadPromise: Promise.resolve(),
594
- context: undefined!,
595
- abortController: new AbortController(),
596
- shouldReloadDeps: undefined,
597
- fetchedAt: 0,
598
- cause,
599
- }
600
-
601
- return routeMatch
602
- })
603
-
604
- // Take each match and resolve its search params and context
605
- // This has to happen after the matches are created or found
606
- // so that we can use the parent match's search params and context
607
- matches.forEach((match, i): any => {
608
- const parentMatch = matches[i - 1]
609
- const route = this.looseRoutesById[match.routeId]!
610
-
611
- const searchInfo = (() => {
568
+ const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
612
569
  // Validate the search params and stabilize them
613
- const parentSearchInfo = {
614
- search: parentMatch?.search ?? locationSearch,
615
- routeSearch: parentMatch?.routeSearch ?? locationSearch,
616
- }
570
+ const parentSearch = parentMatch?.search ?? locationSearch
617
571
 
618
572
  try {
619
573
  const validator =
@@ -621,35 +575,81 @@ export class Router<
621
575
  ? route.options.validateSearch.parse
622
576
  : route.options.validateSearch
623
577
 
624
- let routeSearch = validator?.(parentSearchInfo.search) ?? {}
625
-
626
- let search = {
627
- ...parentSearchInfo.search,
628
- ...routeSearch,
629
- }
630
-
631
- routeSearch = replaceEqualDeep(match.routeSearch, routeSearch)
632
- search = replaceEqualDeep(match.search, search)
578
+ let search = validator?.(parentSearch) ?? {}
633
579
 
634
- return {
635
- routeSearch,
636
- search,
637
- searchDidChange: match.routeSearch !== routeSearch,
638
- }
580
+ return [
581
+ {
582
+ ...parentSearch,
583
+ ...search,
584
+ },
585
+ undefined,
586
+ ]
639
587
  } catch (err: any) {
640
- match.searchError = new SearchParamError(err.message, {
588
+ const searchError = new SearchParamError(err.message, {
641
589
  cause: err,
642
590
  })
643
591
 
644
592
  if (opts?.throwOnError) {
645
- throw match.searchError
593
+ throw searchError
646
594
  }
647
595
 
648
- return parentSearchInfo
596
+ return [parentSearch, searchError]
649
597
  }
650
598
  })()
651
599
 
652
- Object.assign(match, searchInfo)
600
+ const interpolatedPath = interpolatePath(route.path, routeParams)
601
+ const matchId =
602
+ interpolatePath(route.id, routeParams, true) +
603
+ (route.options.key?.({
604
+ search: preMatchSearch,
605
+ location: this.state.location,
606
+ }) ?? '')
607
+
608
+ // Waste not, want not. If we already have a match for this route,
609
+ // reuse it. This is important for layout routes, which might stick
610
+ // around between navigation actions that only change leaf routes.
611
+ const existingMatch = getRouteMatch(this.state, matchId)
612
+
613
+ const cause = this.state.matches.find((d) => d.id === matchId)
614
+ ? 'stay'
615
+ : 'enter'
616
+
617
+ // Create a fresh route match
618
+ const hasLoaders = !!(
619
+ route.options.loader ||
620
+ componentTypes.some((d) => (route.options[d] as any)?.preload)
621
+ )
622
+
623
+ const match: AnyRouteMatch = existingMatch
624
+ ? { ...existingMatch, cause }
625
+ : {
626
+ id: matchId,
627
+ routeId: route.id,
628
+ params: routeParams,
629
+ pathname: joinPaths([this.basepath, interpolatedPath]),
630
+ updatedAt: Date.now(),
631
+ search: {} as any,
632
+ searchError: undefined,
633
+ status: hasLoaders ? 'pending' : 'success',
634
+ isFetching: false,
635
+ invalid: false,
636
+ error: undefined,
637
+ paramsError: parseErrors[index],
638
+ loadPromise: Promise.resolve(),
639
+ context: undefined!,
640
+ abortController: new AbortController(),
641
+ shouldReloadDeps: undefined,
642
+ fetchedAt: 0,
643
+ cause,
644
+ }
645
+
646
+ // Regardless of whether we're reusing an existing match or creating
647
+ // a new one, we need to update the match's search params
648
+ match.search = replaceEqualDeep(match.search, preMatchSearch)
649
+ // And also update the searchError if there is one
650
+ match.searchError = searchError
651
+
652
+ matches.push(match)
653
653
  })
654
654
 
655
655
  return matches as any