@tanstack/react-router 0.0.1-beta.13 → 0.0.1-beta.145
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/LICENSE +21 -0
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +1 -19
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
- package/build/cjs/index.js +649 -0
- package/build/cjs/index.js.map +1 -0
- package/build/esm/index.js +510 -2728
- package/build/esm/index.js.map +1 -1
- package/build/stats-html.html +59 -49
- package/build/stats-react.json +157 -46
- package/build/types/index.d.ts +127 -61
- package/build/umd/index.development.js +2085 -2367
- package/build/umd/index.development.js.map +1 -1
- package/build/umd/index.production.js +25 -4
- package/build/umd/index.production.js.map +1 -1
- package/package.json +11 -11
- package/src/index.tsx +934 -472
- package/build/cjs/react-router/src/index.js +0 -418
- package/build/cjs/react-router/src/index.js.map +0 -1
- package/build/cjs/router-core/build/esm/index.js +0 -2482
- package/build/cjs/router-core/build/esm/index.js.map +0 -1
package/src/index.tsx
CHANGED
|
@@ -1,158 +1,217 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
AnyRoute,
|
|
7
|
-
CheckId,
|
|
8
|
-
rootRouteId,
|
|
9
|
-
Router,
|
|
10
|
-
RouterState,
|
|
11
|
-
ToIdOption,
|
|
12
|
-
} from '@tanstack/router-core'
|
|
2
|
+
import { NoInfer, useStore } from '@tanstack/react-store'
|
|
3
|
+
import invariant from 'tiny-invariant'
|
|
4
|
+
import warning from 'tiny-warning'
|
|
13
5
|
import {
|
|
14
|
-
|
|
6
|
+
functionalUpdate,
|
|
7
|
+
last,
|
|
8
|
+
pick,
|
|
9
|
+
RegisteredRoutesInfo,
|
|
10
|
+
MatchRouteOptions,
|
|
11
|
+
RegisteredRouter,
|
|
15
12
|
RouterOptions,
|
|
13
|
+
Router,
|
|
16
14
|
RouteMatch,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
functionalUpdate,
|
|
23
|
-
createRouter,
|
|
24
|
-
AnyRouteInfo,
|
|
25
|
-
AllRouteInfo,
|
|
26
|
-
RouteInfo,
|
|
27
|
-
ValidFromPath,
|
|
15
|
+
RouteByPath,
|
|
16
|
+
AnyRoutesInfo,
|
|
17
|
+
DefaultRoutesInfo,
|
|
18
|
+
AnyRoute,
|
|
19
|
+
AnyRouteProps,
|
|
28
20
|
LinkOptions,
|
|
29
|
-
RouteInfoByPath,
|
|
30
|
-
ResolveRelativePath,
|
|
31
|
-
NoInfer,
|
|
32
21
|
ToOptions,
|
|
33
|
-
|
|
22
|
+
ResolveRelativePath,
|
|
23
|
+
NavigateOptions,
|
|
24
|
+
ResolveFullPath,
|
|
25
|
+
ResolveId,
|
|
26
|
+
AnySearchSchema,
|
|
27
|
+
ParsePathParams,
|
|
28
|
+
MergeParamsFromParent,
|
|
29
|
+
RouteContext,
|
|
30
|
+
AnyContext,
|
|
31
|
+
UseLoaderResult,
|
|
32
|
+
ResolveFullSearchSchema,
|
|
33
|
+
Route,
|
|
34
34
|
} from '@tanstack/router-core'
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export { lazyWithPreload as lazy } from 'react-lazy-with-preload/lib/index'
|
|
39
|
-
export type { PreloadableComponent as LazyComponent } from 'react-lazy-with-preload'
|
|
36
|
+
//
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
export
|
|
43
|
-
preload?: () => Promise<{
|
|
44
|
-
default: SyncRouteComponent
|
|
45
|
-
}>
|
|
46
|
-
}
|
|
38
|
+
export * from '@tanstack/router-core'
|
|
39
|
+
export { useStore }
|
|
47
40
|
|
|
48
41
|
declare module '@tanstack/router-core' {
|
|
49
|
-
interface
|
|
50
|
-
|
|
42
|
+
interface RegisterRouteComponent<TProps extends Record<string, any>> {
|
|
43
|
+
RouteComponent: RouteComponent<TProps>
|
|
51
44
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
TRouteConfig extends AnyRouteConfig = RouteConfig,
|
|
55
|
-
TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
|
|
56
|
-
> {
|
|
57
|
-
useState: () => RouterState
|
|
58
|
-
useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
|
|
59
|
-
routeId: TId,
|
|
60
|
-
) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
|
|
61
|
-
useMatch: <
|
|
62
|
-
TId extends keyof TAllRouteInfo['routeInfoById'],
|
|
63
|
-
TStrict extends true | false = true,
|
|
64
|
-
>(
|
|
65
|
-
routeId: TId,
|
|
66
|
-
opts?: { strict?: TStrict },
|
|
67
|
-
) => TStrict extends true
|
|
68
|
-
? RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
|
|
69
|
-
:
|
|
70
|
-
| RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
|
|
71
|
-
| undefined
|
|
72
|
-
linkProps: <TTo extends string = '.'>(
|
|
73
|
-
props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
|
|
74
|
-
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
75
|
-
) => React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
76
|
-
Link: <TTo extends string = '.'>(
|
|
77
|
-
props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
|
|
78
|
-
React.AnchorHTMLAttributes<HTMLAnchorElement> &
|
|
79
|
-
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
|
|
80
|
-
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
|
|
81
|
-
children?:
|
|
82
|
-
| React.ReactNode
|
|
83
|
-
| ((state: { isActive: boolean }) => React.ReactNode)
|
|
84
|
-
},
|
|
85
|
-
) => JSX.Element
|
|
86
|
-
MatchRoute: <TTo extends string = '.'>(
|
|
87
|
-
props: ToOptions<TAllRouteInfo, '/', TTo> &
|
|
88
|
-
MatchRouteOptions & {
|
|
89
|
-
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
|
|
90
|
-
children?:
|
|
91
|
-
| React.ReactNode
|
|
92
|
-
| ((
|
|
93
|
-
params: RouteInfoByPath<
|
|
94
|
-
TAllRouteInfo,
|
|
95
|
-
ResolveRelativePath<'/', NoInfer<TTo>>
|
|
96
|
-
>['allParams'],
|
|
97
|
-
) => React.ReactNode)
|
|
98
|
-
},
|
|
99
|
-
) => JSX.Element
|
|
45
|
+
interface RegisterRouteErrorComponent<TProps extends Record<string, any>> {
|
|
46
|
+
RouteComponent: RouteComponent<TProps>
|
|
100
47
|
}
|
|
101
|
-
|
|
48
|
+
// Extend the Route class to have some React-Specific methods
|
|
102
49
|
interface Route<
|
|
103
|
-
|
|
104
|
-
|
|
50
|
+
TParentRoute extends AnyRoute = AnyRoute,
|
|
51
|
+
TPath extends string = '/',
|
|
52
|
+
TFullPath extends ResolveFullPath<TParentRoute, TPath> = ResolveFullPath<
|
|
53
|
+
TParentRoute,
|
|
54
|
+
TPath
|
|
55
|
+
>,
|
|
56
|
+
TCustomId extends string = string,
|
|
57
|
+
TId extends ResolveId<TParentRoute, TCustomId, TPath> = ResolveId<
|
|
58
|
+
TParentRoute,
|
|
59
|
+
TCustomId,
|
|
60
|
+
TPath
|
|
61
|
+
>,
|
|
62
|
+
TLoader = unknown,
|
|
63
|
+
TSearchSchema extends AnySearchSchema = {},
|
|
64
|
+
TFullSearchSchema extends AnySearchSchema = ResolveFullSearchSchema<
|
|
65
|
+
TParentRoute,
|
|
66
|
+
TSearchSchema
|
|
67
|
+
>,
|
|
68
|
+
TParams extends Record<ParsePathParams<TPath>, any> = Record<
|
|
69
|
+
ParsePathParams<TPath>,
|
|
70
|
+
string
|
|
71
|
+
>,
|
|
72
|
+
TAllParams extends MergeParamsFromParent<
|
|
73
|
+
TParentRoute['__types']['allParams'],
|
|
74
|
+
TParams
|
|
75
|
+
> = MergeParamsFromParent<TParentRoute['__types']['allParams'], TParams>,
|
|
76
|
+
TParentContext extends TParentRoute['__types']['routeContext'] = TParentRoute['__types']['routeContext'],
|
|
77
|
+
TAllParentContext extends TParentRoute['__types']['context'] = TParentRoute['__types']['context'],
|
|
78
|
+
TRouteContext extends RouteContext = RouteContext,
|
|
79
|
+
TContext extends MergeParamsFromParent<
|
|
80
|
+
TParentRoute['__types']['context'],
|
|
81
|
+
TRouteContext
|
|
82
|
+
> = MergeParamsFromParent<
|
|
83
|
+
TParentRoute['__types']['context'],
|
|
84
|
+
TRouteContext
|
|
85
|
+
>,
|
|
86
|
+
TRouterContext extends AnyContext = AnyContext,
|
|
87
|
+
TChildren extends unknown = unknown,
|
|
88
|
+
TRoutesInfo extends DefaultRoutesInfo = DefaultRoutesInfo,
|
|
105
89
|
> {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
) =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
) =>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
children?:
|
|
139
|
-
| React.ReactNode
|
|
140
|
-
| ((
|
|
141
|
-
params: RouteInfoByPath<
|
|
142
|
-
TAllRouteInfo,
|
|
143
|
-
ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
|
|
144
|
-
>['allParams'],
|
|
145
|
-
) => React.ReactNode)
|
|
146
|
-
},
|
|
147
|
-
) => JSX.Element
|
|
90
|
+
useMatch: <TStrict extends boolean = true, TSelected = TContext>(opts?: {
|
|
91
|
+
strict?: TStrict
|
|
92
|
+
select?: (search: TContext) => TSelected
|
|
93
|
+
}) => TStrict extends true ? TSelected : TSelected | undefined
|
|
94
|
+
useLoader: <TStrict extends boolean = true, TSelected = TLoader>(opts?: {
|
|
95
|
+
strict?: TStrict
|
|
96
|
+
select?: (search: TLoader) => TSelected
|
|
97
|
+
}) => TStrict extends true
|
|
98
|
+
? UseLoaderResult<TSelected>
|
|
99
|
+
: UseLoaderResult<TSelected> | undefined
|
|
100
|
+
useContext: <TStrict extends boolean = true, TSelected = TContext>(opts?: {
|
|
101
|
+
strict?: TStrict
|
|
102
|
+
select?: (search: TContext) => TSelected
|
|
103
|
+
}) => TStrict extends true ? TSelected : TSelected | undefined
|
|
104
|
+
useRouteContext: <
|
|
105
|
+
TStrict extends boolean = true,
|
|
106
|
+
TSelected = TRouteContext,
|
|
107
|
+
>(opts?: {
|
|
108
|
+
strict?: TStrict
|
|
109
|
+
select?: (search: TRouteContext) => TSelected
|
|
110
|
+
}) => TStrict extends true ? TSelected : TSelected | undefined
|
|
111
|
+
useSearch: <
|
|
112
|
+
TStrict extends boolean = true,
|
|
113
|
+
TSelected = TFullSearchSchema,
|
|
114
|
+
>(opts?: {
|
|
115
|
+
strict?: TStrict
|
|
116
|
+
select?: (search: TFullSearchSchema) => TSelected
|
|
117
|
+
}) => TStrict extends true ? TSelected : TSelected | undefined
|
|
118
|
+
useParams: <TStrict extends boolean = true, TSelected = TAllParams>(opts?: {
|
|
119
|
+
strict?: TStrict
|
|
120
|
+
select?: (search: TAllParams) => TSelected
|
|
121
|
+
}) => TStrict extends true ? TSelected : TSelected | undefined
|
|
148
122
|
}
|
|
149
123
|
}
|
|
150
124
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
125
|
+
Route.__onInit = (route) => {
|
|
126
|
+
Object.assign(route, {
|
|
127
|
+
useMatch: (opts = {}) => {
|
|
128
|
+
return useMatch({ ...opts, from: route.id }) as any
|
|
129
|
+
},
|
|
130
|
+
useLoader: (opts = {}) => {
|
|
131
|
+
return useLoader({ ...opts, from: route.id }) as any
|
|
132
|
+
},
|
|
133
|
+
useContext: (opts: any = {}) => {
|
|
134
|
+
return useMatch({
|
|
135
|
+
...opts,
|
|
136
|
+
from: route.id,
|
|
137
|
+
select: (d: any) => opts?.select?.(d.context) ?? d.context,
|
|
138
|
+
} as any)
|
|
139
|
+
},
|
|
140
|
+
useRouteContext: (opts: any = {}) => {
|
|
141
|
+
return useMatch({
|
|
142
|
+
...opts,
|
|
143
|
+
from: route.id,
|
|
144
|
+
select: (d: any) => opts?.select?.(d.routeContext) ?? d.routeContext,
|
|
145
|
+
} as any)
|
|
146
|
+
},
|
|
147
|
+
useSearch: (opts = {}) => {
|
|
148
|
+
return useSearch({ ...opts, from: route.id } as any)
|
|
149
|
+
},
|
|
150
|
+
useParams: (opts = {}) => {
|
|
151
|
+
return useParams({ ...opts, from: route.id } as any)
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//
|
|
157
|
+
|
|
158
|
+
type ReactNode = any
|
|
159
|
+
|
|
160
|
+
export type SyncRouteComponent<TProps> =
|
|
161
|
+
| ((props: TProps) => ReactNode)
|
|
162
|
+
| React.LazyExoticComponent<(props: TProps) => ReactNode>
|
|
163
|
+
|
|
164
|
+
export type AsyncRouteComponent<TProps> = SyncRouteComponent<TProps> & {
|
|
165
|
+
preload?: () => Promise<void>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type RouteErrorComponent = AsyncRouteComponent<RouteErrorComponentProps>
|
|
169
|
+
|
|
170
|
+
export type RouteErrorComponentProps = {
|
|
171
|
+
error: Error
|
|
172
|
+
info: { componentStack: string }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type AnyRouteComponent = RouteComponent<AnyRouteProps>
|
|
176
|
+
|
|
177
|
+
export type RouteComponent<TProps> = AsyncRouteComponent<TProps>
|
|
178
|
+
|
|
179
|
+
export function lazyRouteComponent<
|
|
180
|
+
T extends Record<string, any>,
|
|
181
|
+
TKey extends keyof T = 'default',
|
|
182
|
+
>(
|
|
183
|
+
importer: () => Promise<T>,
|
|
184
|
+
exportName?: TKey,
|
|
185
|
+
): T[TKey] extends (props: infer TProps) => any
|
|
186
|
+
? AsyncRouteComponent<TProps>
|
|
187
|
+
: never {
|
|
188
|
+
let loadPromise: Promise<any>
|
|
189
|
+
|
|
190
|
+
const load = () => {
|
|
191
|
+
if (!loadPromise) {
|
|
192
|
+
loadPromise = importer()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return loadPromise
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const lazyComp = React.lazy(async () => {
|
|
199
|
+
const moduleExports = await load()
|
|
200
|
+
const comp = moduleExports[exportName ?? 'default']
|
|
201
|
+
return {
|
|
202
|
+
default: comp,
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
;(lazyComp as any).preload = load
|
|
207
|
+
|
|
208
|
+
return lazyComp as any
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type LinkPropsOptions<
|
|
212
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
213
|
+
TTo extends string = '',
|
|
214
|
+
> = LinkOptions<RegisteredRoutesInfo, TFrom, TTo> & {
|
|
156
215
|
// A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
|
|
157
216
|
activeProps?:
|
|
158
217
|
| React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
@@ -161,440 +220,843 @@ type LinkPropsOptions<
|
|
|
161
220
|
inactiveProps?:
|
|
162
221
|
| React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
163
222
|
| (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
|
|
223
|
+
// If set to `true`, the link's underlying navigate() call will be wrapped in a `React.startTransition` call. Defaults to `true`.
|
|
224
|
+
startTransition?: boolean
|
|
164
225
|
}
|
|
165
226
|
|
|
227
|
+
export type MakeUseMatchRouteOptions<
|
|
228
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
229
|
+
TTo extends string = '',
|
|
230
|
+
> = ToOptions<RegisteredRoutesInfo, TFrom, TTo> & MatchRouteOptions
|
|
231
|
+
|
|
232
|
+
export type MakeMatchRouteOptions<
|
|
233
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
234
|
+
TTo extends string = '',
|
|
235
|
+
> = ToOptions<RegisteredRoutesInfo, TFrom, TTo> &
|
|
236
|
+
MatchRouteOptions & {
|
|
237
|
+
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
|
|
238
|
+
children?:
|
|
239
|
+
| ((
|
|
240
|
+
params?: RouteByPath<
|
|
241
|
+
RegisteredRoutesInfo,
|
|
242
|
+
ResolveRelativePath<TFrom, NoInfer<TTo>>
|
|
243
|
+
>['__types']['allParams'],
|
|
244
|
+
) => ReactNode)
|
|
245
|
+
| React.ReactNode
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export type MakeLinkPropsOptions<
|
|
249
|
+
TFrom extends string = '/',
|
|
250
|
+
TTo extends string = '',
|
|
251
|
+
> = LinkPropsOptions<TFrom, TTo> & React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
252
|
+
|
|
253
|
+
export type MakeLinkOptions<
|
|
254
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
255
|
+
TTo extends string = '',
|
|
256
|
+
> = LinkPropsOptions<TFrom, TTo> &
|
|
257
|
+
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
|
|
258
|
+
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
|
|
259
|
+
children?:
|
|
260
|
+
| React.ReactNode
|
|
261
|
+
| ((state: { isActive: boolean }) => React.ReactNode)
|
|
262
|
+
}
|
|
263
|
+
|
|
166
264
|
export type PromptProps = {
|
|
167
265
|
message: string
|
|
168
|
-
|
|
169
|
-
children?:
|
|
266
|
+
condition?: boolean | any
|
|
267
|
+
children?: ReactNode
|
|
170
268
|
}
|
|
171
269
|
|
|
172
270
|
//
|
|
173
271
|
|
|
174
|
-
|
|
175
|
-
|
|
272
|
+
export function useLinkProps<
|
|
273
|
+
TFrom extends string = '/',
|
|
274
|
+
TTo extends string = '',
|
|
275
|
+
>(
|
|
276
|
+
options: MakeLinkPropsOptions<TFrom, TTo>,
|
|
277
|
+
): React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
278
|
+
const router = useRouter()
|
|
176
279
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
280
|
+
const {
|
|
281
|
+
// custom props
|
|
282
|
+
type,
|
|
283
|
+
children,
|
|
284
|
+
target,
|
|
285
|
+
activeProps = () => ({ className: 'active' }),
|
|
286
|
+
inactiveProps = () => ({}),
|
|
287
|
+
activeOptions,
|
|
288
|
+
disabled,
|
|
289
|
+
// fromCurrent,
|
|
290
|
+
hash,
|
|
291
|
+
search,
|
|
292
|
+
params,
|
|
293
|
+
to = '.',
|
|
294
|
+
preload,
|
|
295
|
+
preloadDelay,
|
|
296
|
+
replace,
|
|
297
|
+
// element props
|
|
298
|
+
style,
|
|
299
|
+
className,
|
|
300
|
+
onClick,
|
|
301
|
+
onFocus,
|
|
302
|
+
onMouseEnter,
|
|
303
|
+
onMouseLeave,
|
|
304
|
+
onTouchStart,
|
|
305
|
+
...rest
|
|
306
|
+
} = options
|
|
307
|
+
|
|
308
|
+
const linkInfo = router.buildLink(options as any)
|
|
309
|
+
|
|
310
|
+
if (linkInfo.type === 'external') {
|
|
311
|
+
const { href } = linkInfo
|
|
312
|
+
return { href }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const {
|
|
316
|
+
handleClick,
|
|
317
|
+
handleFocus,
|
|
318
|
+
handleEnter,
|
|
319
|
+
handleLeave,
|
|
320
|
+
handleTouchStart,
|
|
321
|
+
isActive,
|
|
322
|
+
next,
|
|
323
|
+
} = linkInfo
|
|
324
|
+
|
|
325
|
+
const handleReactClick = (e: Event) => {
|
|
326
|
+
if (options.startTransition ?? true) {
|
|
327
|
+
;(React.startTransition || ((d) => d))(() => {
|
|
328
|
+
handleClick(e)
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
}
|
|
183
332
|
|
|
184
|
-
const
|
|
333
|
+
const composeHandlers =
|
|
334
|
+
(handlers: (undefined | ((e: any) => void))[]) =>
|
|
335
|
+
(e: React.SyntheticEvent) => {
|
|
336
|
+
if (e.persist) e.persist()
|
|
337
|
+
handlers.filter(Boolean).forEach((handler) => {
|
|
338
|
+
if (e.defaultPrevented) return
|
|
339
|
+
handler!(e)
|
|
340
|
+
})
|
|
341
|
+
}
|
|
185
342
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
343
|
+
// Get the active props
|
|
344
|
+
const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
|
|
345
|
+
? functionalUpdate(activeProps as any, {}) ?? {}
|
|
346
|
+
: {}
|
|
347
|
+
|
|
348
|
+
// Get the inactive props
|
|
349
|
+
const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
|
|
350
|
+
isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...resolvedActiveProps,
|
|
354
|
+
...resolvedInactiveProps,
|
|
355
|
+
...rest,
|
|
356
|
+
href: disabled ? undefined : next.href,
|
|
357
|
+
onClick: composeHandlers([onClick, handleReactClick]),
|
|
358
|
+
onFocus: composeHandlers([onFocus, handleFocus]),
|
|
359
|
+
onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
|
|
360
|
+
onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
|
|
361
|
+
onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
|
|
362
|
+
target,
|
|
363
|
+
style: {
|
|
364
|
+
...style,
|
|
365
|
+
...resolvedActiveProps.style,
|
|
366
|
+
...resolvedInactiveProps.style,
|
|
367
|
+
},
|
|
368
|
+
className:
|
|
369
|
+
[
|
|
370
|
+
className,
|
|
371
|
+
resolvedActiveProps.className,
|
|
372
|
+
resolvedInactiveProps.className,
|
|
373
|
+
]
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.join(' ') || undefined,
|
|
376
|
+
...(disabled
|
|
377
|
+
? {
|
|
378
|
+
role: 'link',
|
|
379
|
+
'aria-disabled': true,
|
|
380
|
+
}
|
|
381
|
+
: undefined),
|
|
382
|
+
['data-status']: isActive ? 'active' : undefined,
|
|
383
|
+
}
|
|
189
384
|
}
|
|
190
385
|
|
|
191
|
-
export
|
|
192
|
-
|
|
386
|
+
export interface LinkFn<
|
|
387
|
+
TDefaultFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
388
|
+
TDefaultTo extends string = '',
|
|
389
|
+
> {
|
|
390
|
+
<
|
|
391
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = TDefaultFrom,
|
|
392
|
+
TTo extends string = TDefaultTo,
|
|
393
|
+
>(
|
|
394
|
+
props: MakeLinkOptions<TFrom, TTo> & React.RefAttributes<HTMLAnchorElement>,
|
|
395
|
+
): ReactNode
|
|
193
396
|
}
|
|
194
397
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
398
|
+
export const Link: LinkFn = React.forwardRef((props: any, ref) => {
|
|
399
|
+
const linkProps = useLinkProps(props)
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<a
|
|
403
|
+
{...{
|
|
404
|
+
ref: ref as any,
|
|
405
|
+
...linkProps,
|
|
406
|
+
children:
|
|
407
|
+
typeof props.children === 'function'
|
|
408
|
+
? props.children({
|
|
409
|
+
isActive: (linkProps as any)['data-status'] === 'active',
|
|
410
|
+
})
|
|
411
|
+
: props.children,
|
|
412
|
+
}}
|
|
413
|
+
/>
|
|
200
414
|
)
|
|
415
|
+
}) as any
|
|
416
|
+
|
|
417
|
+
export function Navigate<
|
|
418
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
419
|
+
TTo extends string = '',
|
|
420
|
+
>(props: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>): null {
|
|
421
|
+
const router = useRouter()
|
|
422
|
+
|
|
423
|
+
React.useLayoutEffect(() => {
|
|
424
|
+
router.navigate(props as any)
|
|
425
|
+
}, [])
|
|
426
|
+
|
|
427
|
+
return null
|
|
201
428
|
}
|
|
202
429
|
|
|
203
|
-
export
|
|
204
|
-
|
|
205
|
-
>(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
|
|
206
|
-
const makeRouteExt = (
|
|
207
|
-
route: AnyRoute,
|
|
208
|
-
router: Router<any, any>,
|
|
209
|
-
): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
|
|
210
|
-
return {
|
|
211
|
-
useRoute: (subRouteId = '.' as any) => {
|
|
212
|
-
const resolvedRouteId = router.resolvePath(
|
|
213
|
-
route.routeId,
|
|
214
|
-
subRouteId as string,
|
|
215
|
-
)
|
|
216
|
-
const resolvedRoute = router.getRoute(resolvedRouteId)
|
|
217
|
-
useRouterSubscription(router)
|
|
218
|
-
invariant(
|
|
219
|
-
resolvedRoute,
|
|
220
|
-
`Could not find a route for route "${
|
|
221
|
-
resolvedRouteId as string
|
|
222
|
-
}"! Did you forget to add it to your route config?`,
|
|
223
|
-
)
|
|
224
|
-
return resolvedRoute
|
|
225
|
-
},
|
|
226
|
-
linkProps: (options) => {
|
|
227
|
-
const {
|
|
228
|
-
// custom props
|
|
229
|
-
type,
|
|
230
|
-
children,
|
|
231
|
-
target,
|
|
232
|
-
activeProps = () => ({ className: 'active' }),
|
|
233
|
-
inactiveProps = () => ({}),
|
|
234
|
-
activeOptions,
|
|
235
|
-
disabled,
|
|
236
|
-
// fromCurrent,
|
|
237
|
-
hash,
|
|
238
|
-
search,
|
|
239
|
-
params,
|
|
240
|
-
to,
|
|
241
|
-
preload,
|
|
242
|
-
preloadDelay,
|
|
243
|
-
preloadMaxAge,
|
|
244
|
-
replace,
|
|
245
|
-
// element props
|
|
246
|
-
style,
|
|
247
|
-
className,
|
|
248
|
-
onClick,
|
|
249
|
-
onFocus,
|
|
250
|
-
onMouseEnter,
|
|
251
|
-
onMouseLeave,
|
|
252
|
-
onTouchStart,
|
|
253
|
-
onTouchEnd,
|
|
254
|
-
...rest
|
|
255
|
-
} = options
|
|
256
|
-
|
|
257
|
-
const linkInfo = route.buildLink(options)
|
|
258
|
-
|
|
259
|
-
if (linkInfo.type === 'external') {
|
|
260
|
-
const { href } = linkInfo
|
|
261
|
-
return { href }
|
|
262
|
-
}
|
|
430
|
+
export const matchIdsContext = React.createContext<string[]>(null!)
|
|
431
|
+
export const routerContext = React.createContext<RegisteredRouter>(null!)
|
|
263
432
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const reactHandleClick = (e: Event) => {
|
|
274
|
-
React.startTransition(() => {
|
|
275
|
-
handleClick(e)
|
|
276
|
-
})
|
|
277
|
-
}
|
|
433
|
+
export type RouterProps<
|
|
434
|
+
TRouteConfig extends AnyRoute = AnyRoute,
|
|
435
|
+
TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
|
|
436
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
437
|
+
> = Omit<RouterOptions<TRouteConfig, TDehydrated>, 'context'> & {
|
|
438
|
+
router: Router<TRouteConfig, TRoutesInfo>
|
|
439
|
+
context?: Partial<RouterOptions<TRouteConfig, TDehydrated>['context']>
|
|
440
|
+
}
|
|
278
441
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
})
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Get the active props
|
|
289
|
-
const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
|
|
290
|
-
isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
|
|
291
|
-
|
|
292
|
-
// Get the inactive props
|
|
293
|
-
const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
|
|
294
|
-
isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
...resolvedActiveProps,
|
|
298
|
-
...resolvedInactiveProps,
|
|
299
|
-
...rest,
|
|
300
|
-
href: disabled ? undefined : next.href,
|
|
301
|
-
onClick: composeHandlers([reactHandleClick, onClick]),
|
|
302
|
-
onFocus: composeHandlers([handleFocus, onFocus]),
|
|
303
|
-
onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
|
|
304
|
-
onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
|
|
305
|
-
target,
|
|
306
|
-
style: {
|
|
307
|
-
...style,
|
|
308
|
-
...resolvedActiveProps.style,
|
|
309
|
-
...resolvedInactiveProps.style,
|
|
310
|
-
},
|
|
311
|
-
className:
|
|
312
|
-
[
|
|
313
|
-
className,
|
|
314
|
-
resolvedActiveProps.className,
|
|
315
|
-
resolvedInactiveProps.className,
|
|
316
|
-
]
|
|
317
|
-
.filter(Boolean)
|
|
318
|
-
.join(' ') || undefined,
|
|
319
|
-
...(disabled
|
|
320
|
-
? {
|
|
321
|
-
role: 'link',
|
|
322
|
-
'aria-disabled': true,
|
|
323
|
-
}
|
|
324
|
-
: undefined),
|
|
325
|
-
['data-status']: isActive ? 'active' : undefined,
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
Link: React.forwardRef((props: any, ref) => {
|
|
329
|
-
const linkProps = route.linkProps(props)
|
|
330
|
-
|
|
331
|
-
useRouterSubscription(router)
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
<a
|
|
335
|
-
{...{
|
|
336
|
-
ref: ref as any,
|
|
337
|
-
...linkProps,
|
|
338
|
-
children:
|
|
339
|
-
typeof props.children === 'function'
|
|
340
|
-
? props.children({
|
|
341
|
-
isActive: (linkProps as any)['data-status'] === 'active',
|
|
342
|
-
})
|
|
343
|
-
: props.children,
|
|
344
|
-
}}
|
|
345
|
-
/>
|
|
346
|
-
)
|
|
347
|
-
}) as any,
|
|
348
|
-
MatchRoute: (opts) => {
|
|
349
|
-
const { pending, caseSensitive, children, ...rest } = opts
|
|
350
|
-
|
|
351
|
-
const params = route.matchRoute(rest as any, {
|
|
352
|
-
pending,
|
|
353
|
-
caseSensitive,
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
if (!params) {
|
|
357
|
-
return null
|
|
358
|
-
}
|
|
442
|
+
export function useRouterState<TSelected = RegisteredRouter['state']>(opts?: {
|
|
443
|
+
select: (state: RegisteredRouter['state']) => TSelected
|
|
444
|
+
}): TSelected {
|
|
445
|
+
const router = useRouter()
|
|
446
|
+
return useStore(router.__store, opts?.select)
|
|
447
|
+
}
|
|
359
448
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
449
|
+
export function RouterProvider<
|
|
450
|
+
TRouteConfig extends AnyRoute = AnyRoute,
|
|
451
|
+
TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
|
|
452
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
453
|
+
>({ router, ...rest }: RouterProps<TRouteConfig, TRoutesInfo, TDehydrated>) {
|
|
454
|
+
router.update(rest)
|
|
366
455
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
456
|
+
React.useEffect(() => {
|
|
457
|
+
let unsub
|
|
458
|
+
|
|
459
|
+
React.startTransition(() => {
|
|
460
|
+
unsub = router.mount()
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
return unsub
|
|
464
|
+
}, [router])
|
|
465
|
+
|
|
466
|
+
const Wrap = router.options.Wrap || React.Fragment
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<React.Suspense fallback={null}>
|
|
470
|
+
<Wrap>
|
|
471
|
+
<routerContext.Provider value={router as any}>
|
|
472
|
+
<Matches />
|
|
473
|
+
</routerContext.Provider>
|
|
474
|
+
</Wrap>
|
|
475
|
+
</React.Suspense>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function Matches() {
|
|
480
|
+
const router = useRouter()
|
|
382
481
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
runtimeMatch.routeId == match?.routeId,
|
|
394
|
-
`useMatch('${
|
|
395
|
-
match?.routeId as string
|
|
396
|
-
}') is being called in a component that is meant to render the '${
|
|
397
|
-
runtimeMatch.routeId
|
|
398
|
-
}' route. Did you mean to 'useMatch(${
|
|
399
|
-
match?.routeId as string
|
|
400
|
-
}, { strict: false })' or 'useRoute(${
|
|
401
|
-
match?.routeId as string
|
|
402
|
-
})' instead?`,
|
|
403
|
-
)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return match as any
|
|
407
|
-
},
|
|
482
|
+
const matchIds = useRouterState({
|
|
483
|
+
select: (state) => {
|
|
484
|
+
const hasPendingComponent = state.pendingMatches.some((d) => {
|
|
485
|
+
const route = router.getRoute(d.routeId as any)
|
|
486
|
+
return !!route?.options.pendingComponent
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
if (hasPendingComponent) {
|
|
490
|
+
console.log('hasPending')
|
|
491
|
+
return state.pendingMatchIds
|
|
408
492
|
}
|
|
409
493
|
|
|
410
|
-
|
|
494
|
+
return state.matchIds
|
|
495
|
+
},
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<matchIdsContext.Provider value={[undefined!, ...matchIds]}>
|
|
500
|
+
<CatchBoundary
|
|
501
|
+
errorComponent={ErrorComponent}
|
|
502
|
+
onCatch={() => {
|
|
503
|
+
warning(
|
|
504
|
+
false,
|
|
505
|
+
`Error in router! Consider setting an 'errorComponent' in your RootRoute! 👍`,
|
|
506
|
+
)
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
<Outlet />
|
|
510
|
+
</CatchBoundary>
|
|
511
|
+
</matchIdsContext.Provider>
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function useRouter(): RegisteredRouter {
|
|
516
|
+
const value = React.useContext(routerContext)
|
|
517
|
+
warning(value, 'useRouter must be used inside a <Router> component!')
|
|
518
|
+
return value
|
|
519
|
+
}
|
|
411
520
|
|
|
412
|
-
|
|
521
|
+
export function useMatches<T = RouteMatch[]>(opts?: {
|
|
522
|
+
select?: (matches: RouteMatch[]) => T
|
|
523
|
+
}): T {
|
|
524
|
+
const matchIds = React.useContext(matchIdsContext)
|
|
525
|
+
|
|
526
|
+
return useRouterState({
|
|
527
|
+
select: (state) => {
|
|
528
|
+
const matches = state.matches.slice(
|
|
529
|
+
state.matches.findIndex((d) => d.id === matchIds[0]),
|
|
530
|
+
)
|
|
531
|
+
return (opts?.select?.(matches) ?? matches) as T
|
|
413
532
|
},
|
|
414
|
-
|
|
415
|
-
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function useMatch<
|
|
537
|
+
TFrom extends keyof RegisteredRoutesInfo['routesById'],
|
|
538
|
+
TStrict extends boolean = true,
|
|
539
|
+
TRouteMatchState = RouteMatch<
|
|
540
|
+
RegisteredRoutesInfo,
|
|
541
|
+
RegisteredRoutesInfo['routesById'][TFrom]
|
|
542
|
+
>,
|
|
543
|
+
TSelected = TRouteMatchState,
|
|
544
|
+
>(opts?: {
|
|
545
|
+
from: TFrom
|
|
546
|
+
strict?: TStrict
|
|
547
|
+
select?: (match: TRouteMatchState) => TSelected
|
|
548
|
+
}): TStrict extends true ? TRouteMatchState : TRouteMatchState | undefined {
|
|
549
|
+
const router = useRouter()
|
|
550
|
+
const nearestMatchId = React.useContext(matchIdsContext)[0]!
|
|
551
|
+
const nearestMatchRouteId = router.getRouteMatch(nearestMatchId)?.routeId
|
|
552
|
+
|
|
553
|
+
const matchRouteId = useRouterState({
|
|
554
|
+
select: (state) => {
|
|
555
|
+
const matches = state.matches
|
|
556
|
+
const match = opts?.from
|
|
557
|
+
? matches.find((d) => d.routeId === opts?.from)
|
|
558
|
+
: matches.find((d) => d.id === nearestMatchId)
|
|
416
559
|
|
|
417
|
-
|
|
560
|
+
return match!.routeId
|
|
418
561
|
},
|
|
419
|
-
|
|
420
|
-
if (component.preload && typeof document !== 'undefined') {
|
|
421
|
-
component.preload()
|
|
422
|
-
// return await component.preload()
|
|
423
|
-
}
|
|
562
|
+
})
|
|
424
563
|
|
|
425
|
-
|
|
564
|
+
if (opts?.strict ?? true) {
|
|
565
|
+
invariant(
|
|
566
|
+
nearestMatchRouteId == matchRouteId,
|
|
567
|
+
`useMatch("${
|
|
568
|
+
matchRouteId as string
|
|
569
|
+
}") is being called in a component that is meant to render the '${nearestMatchRouteId}' route. Did you mean to 'useMatch("${
|
|
570
|
+
matchRouteId as string
|
|
571
|
+
}", { strict: false })' or 'useRoute("${
|
|
572
|
+
matchRouteId as string
|
|
573
|
+
}")' instead?`,
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const match = useRouterState({
|
|
578
|
+
select: (state) => {
|
|
579
|
+
const matches = state.matches
|
|
580
|
+
const match = opts?.from
|
|
581
|
+
? matches.find((d) => d.routeId === opts?.from)
|
|
582
|
+
: matches.find((d) => d.id === nearestMatchId)
|
|
583
|
+
|
|
584
|
+
invariant(
|
|
585
|
+
match,
|
|
586
|
+
`Could not find ${
|
|
587
|
+
opts?.from
|
|
588
|
+
? `an active match from "${opts.from}"`
|
|
589
|
+
: 'a nearest match!'
|
|
590
|
+
}`,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
return (opts?.select?.(match as any) ?? match) as TSelected
|
|
426
594
|
},
|
|
427
595
|
})
|
|
428
596
|
|
|
429
|
-
return
|
|
597
|
+
return match as any
|
|
430
598
|
}
|
|
431
599
|
|
|
432
|
-
export type
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
600
|
+
export type RouteFromIdOrRoute<T> = T extends RegisteredRoutesInfo['routeUnion']
|
|
601
|
+
? T
|
|
602
|
+
: T extends keyof RegisteredRoutesInfo['routesById']
|
|
603
|
+
? RegisteredRoutesInfo['routesById'][T]
|
|
604
|
+
: T extends string
|
|
605
|
+
? keyof RegisteredRoutesInfo['routesById']
|
|
606
|
+
: never
|
|
607
|
+
|
|
608
|
+
export function useLoader<
|
|
609
|
+
TFrom extends keyof RegisteredRoutesInfo['routesById'],
|
|
610
|
+
TStrict extends boolean = true,
|
|
611
|
+
TLoader = RegisteredRoutesInfo['routesById'][TFrom]['__types']['loader'],
|
|
612
|
+
TSelected = TLoader,
|
|
613
|
+
>(opts?: {
|
|
614
|
+
from: TFrom
|
|
615
|
+
strict?: TStrict
|
|
616
|
+
select?: (search: TLoader) => TSelected
|
|
617
|
+
}): TStrict extends true ? TSelected : TSelected | undefined {
|
|
618
|
+
return useMatch({
|
|
619
|
+
...(opts as any),
|
|
620
|
+
select: (match: RouteMatch) =>
|
|
621
|
+
(opts?.select?.(match.loaderData as TLoader) ??
|
|
622
|
+
match.loaderData) as TSelected,
|
|
623
|
+
})
|
|
439
624
|
}
|
|
440
625
|
|
|
441
|
-
export function
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
626
|
+
export function useRouterContext<
|
|
627
|
+
TFrom extends keyof RegisteredRoutesInfo['routesById'],
|
|
628
|
+
TStrict extends boolean = true,
|
|
629
|
+
TContext = RegisteredRoutesInfo['routesById'][TFrom]['__types']['context'],
|
|
630
|
+
TSelected = TContext,
|
|
631
|
+
>(opts?: {
|
|
632
|
+
from: TFrom
|
|
633
|
+
strict?: TStrict
|
|
634
|
+
select?: (search: TContext) => TSelected
|
|
635
|
+
}): TStrict extends true ? TSelected : TSelected | undefined {
|
|
636
|
+
return useMatch({
|
|
637
|
+
...(opts as any),
|
|
638
|
+
select: (match: RouteMatch) =>
|
|
639
|
+
(opts?.select?.(match.context as TContext) ?? match.context) as TSelected,
|
|
640
|
+
})
|
|
641
|
+
}
|
|
446
642
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
643
|
+
export function useRouteContext<
|
|
644
|
+
TFrom extends keyof RegisteredRoutesInfo['routesById'],
|
|
645
|
+
TStrict extends boolean = true,
|
|
646
|
+
TRouteContext = RegisteredRoutesInfo['routesById'][TFrom]['__types']['routeContext'],
|
|
647
|
+
TSelected = TRouteContext,
|
|
648
|
+
>(opts?: {
|
|
649
|
+
from: TFrom
|
|
650
|
+
strict?: TStrict
|
|
651
|
+
select?: (search: TRouteContext) => TSelected
|
|
652
|
+
}): TStrict extends true ? TSelected : TSelected | undefined {
|
|
653
|
+
return useMatch({
|
|
654
|
+
...(opts as any),
|
|
655
|
+
select: (match: RouteMatch) =>
|
|
656
|
+
(opts?.select?.(match.routeContext as TRouteContext) ??
|
|
657
|
+
match.routeContext) as TSelected,
|
|
658
|
+
})
|
|
659
|
+
}
|
|
453
660
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
661
|
+
export function useSearch<
|
|
662
|
+
TFrom extends keyof RegisteredRoutesInfo['routesById'],
|
|
663
|
+
TStrict extends boolean = true,
|
|
664
|
+
TSearch = RegisteredRoutesInfo['routesById'][TFrom]['__types']['fullSearchSchema'],
|
|
665
|
+
TSelected = TSearch,
|
|
666
|
+
>(opts?: {
|
|
667
|
+
from: TFrom
|
|
668
|
+
strict?: TStrict
|
|
669
|
+
select?: (search: TSearch) => TSelected
|
|
670
|
+
}): TStrict extends true ? TSelected : TSelected | undefined {
|
|
671
|
+
return useMatch({
|
|
672
|
+
...(opts as any),
|
|
673
|
+
select: (match: RouteMatch) => {
|
|
674
|
+
return (opts?.select?.(match.search as TSearch) ??
|
|
675
|
+
match.search) as TSelected
|
|
676
|
+
},
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function useParams<
|
|
681
|
+
TFrom extends keyof RegisteredRoutesInfo['routesById'] = '/',
|
|
682
|
+
TDefaultSelected = RegisteredRoutesInfo['allParams'] &
|
|
683
|
+
RegisteredRoutesInfo['routesById'][TFrom]['__types']['allParams'],
|
|
684
|
+
TSelected = TDefaultSelected,
|
|
685
|
+
>(opts?: {
|
|
686
|
+
from: TFrom
|
|
687
|
+
select?: (search: TDefaultSelected) => TSelected
|
|
688
|
+
}): TSelected {
|
|
689
|
+
return useRouterState({
|
|
690
|
+
select: (state: any) => {
|
|
691
|
+
const params = (last(state.matches) as any)?.params
|
|
692
|
+
return (opts?.select?.(params) ?? params) as TSelected
|
|
693
|
+
},
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function useNavigate<
|
|
698
|
+
TDefaultFrom extends RegisteredRoutesInfo['routePaths'] = '/',
|
|
699
|
+
>(defaultOpts?: { from?: TDefaultFrom }) {
|
|
700
|
+
const router = useRouter()
|
|
701
|
+
return React.useCallback(
|
|
702
|
+
<
|
|
703
|
+
TFrom extends RegisteredRoutesInfo['routePaths'] = TDefaultFrom,
|
|
704
|
+
TTo extends string = '',
|
|
705
|
+
>(
|
|
706
|
+
opts?: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>,
|
|
707
|
+
) => {
|
|
708
|
+
return router.navigate({ ...defaultOpts, ...(opts as any) })
|
|
709
|
+
},
|
|
710
|
+
[],
|
|
460
711
|
)
|
|
461
712
|
}
|
|
462
713
|
|
|
463
|
-
export function
|
|
464
|
-
const
|
|
465
|
-
warning(!value, 'useRouter must be used inside a <Router> component!')
|
|
714
|
+
export function useMatchRoute() {
|
|
715
|
+
const router = useRouter()
|
|
466
716
|
|
|
467
|
-
|
|
717
|
+
return React.useCallback(
|
|
718
|
+
<TFrom extends string = '/', TTo extends string = ''>(
|
|
719
|
+
opts: MakeUseMatchRouteOptions<TFrom, TTo>,
|
|
720
|
+
) => {
|
|
721
|
+
const { pending, caseSensitive, ...rest } = opts
|
|
468
722
|
|
|
469
|
-
|
|
723
|
+
return router.matchRoute(rest as any, {
|
|
724
|
+
pending,
|
|
725
|
+
caseSensitive,
|
|
726
|
+
})
|
|
727
|
+
},
|
|
728
|
+
[],
|
|
729
|
+
)
|
|
470
730
|
}
|
|
471
731
|
|
|
472
|
-
export function
|
|
473
|
-
|
|
732
|
+
export function MatchRoute<TFrom extends string = '/', TTo extends string = ''>(
|
|
733
|
+
props: MakeMatchRouteOptions<TFrom, TTo>,
|
|
734
|
+
): any {
|
|
735
|
+
const matchRoute = useMatchRoute()
|
|
736
|
+
const params = matchRoute(props)
|
|
737
|
+
|
|
738
|
+
if (typeof props.children === 'function') {
|
|
739
|
+
return (props.children as any)(params)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return !!params ? props.children : null
|
|
474
743
|
}
|
|
475
744
|
|
|
476
745
|
export function Outlet() {
|
|
477
|
-
const
|
|
478
|
-
const matches = useMatches().slice(1)
|
|
479
|
-
const match = matches[0]
|
|
746
|
+
const matchIds = React.useContext(matchIdsContext).slice(1)
|
|
480
747
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (!match) {
|
|
748
|
+
if (!matchIds[0]) {
|
|
484
749
|
return null
|
|
485
750
|
}
|
|
486
751
|
|
|
487
|
-
|
|
752
|
+
return <Match matchIds={matchIds} />
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const defaultPending = () => null
|
|
756
|
+
|
|
757
|
+
function Match({ matchIds }: { matchIds: string[] }) {
|
|
758
|
+
const router = useRouter()
|
|
759
|
+
const matchId = matchIds[0]!
|
|
760
|
+
const routeId = router.getRouteMatch(matchId)!.routeId
|
|
761
|
+
const route = router.getRoute(routeId)
|
|
762
|
+
|
|
763
|
+
const PendingComponent = (route.options.pendingComponent ??
|
|
488
764
|
router.options.defaultPendingComponent ??
|
|
489
765
|
defaultPending) as any
|
|
490
766
|
|
|
491
767
|
const errorComponent =
|
|
492
|
-
|
|
768
|
+
route.options.errorComponent ??
|
|
769
|
+
router.options.defaultErrorComponent ??
|
|
770
|
+
ErrorComponent
|
|
771
|
+
|
|
772
|
+
const ResolvedSuspenseBoundary =
|
|
773
|
+
route.options.wrapInSuspense ?? !route.isRoot
|
|
774
|
+
? React.Suspense
|
|
775
|
+
: SafeFragment
|
|
776
|
+
|
|
777
|
+
const ResolvedCatchBoundary = !!errorComponent ? CatchBoundary : SafeFragment
|
|
493
778
|
|
|
494
779
|
return (
|
|
495
|
-
<
|
|
496
|
-
<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
invariant(false, 'This should never happen!')
|
|
518
|
-
})() as JSX.Element
|
|
519
|
-
}
|
|
520
|
-
</CatchBoundary>
|
|
521
|
-
</React.Suspense>
|
|
522
|
-
</MatchesProvider>
|
|
780
|
+
<matchIdsContext.Provider value={matchIds}>
|
|
781
|
+
<ResolvedSuspenseBoundary
|
|
782
|
+
fallback={React.createElement(PendingComponent, {
|
|
783
|
+
useLoader: route.useLoader,
|
|
784
|
+
useMatch: route.useMatch,
|
|
785
|
+
useContext: route.useContext,
|
|
786
|
+
useRouteContext: route.useRouteContext,
|
|
787
|
+
useSearch: route.useSearch,
|
|
788
|
+
useParams: route.useParams,
|
|
789
|
+
})}
|
|
790
|
+
>
|
|
791
|
+
<ResolvedCatchBoundary
|
|
792
|
+
key={route.id}
|
|
793
|
+
errorComponent={errorComponent}
|
|
794
|
+
onCatch={() => {
|
|
795
|
+
warning(false, `Error in route match: ${matchId}`)
|
|
796
|
+
}}
|
|
797
|
+
>
|
|
798
|
+
<MatchInner matchId={matchId} PendingComponent={PendingComponent} />
|
|
799
|
+
</ResolvedCatchBoundary>
|
|
800
|
+
</ResolvedSuspenseBoundary>
|
|
801
|
+
</matchIdsContext.Provider>
|
|
523
802
|
)
|
|
524
803
|
}
|
|
525
804
|
|
|
805
|
+
function MatchInner({
|
|
806
|
+
matchId,
|
|
807
|
+
PendingComponent,
|
|
808
|
+
}: {
|
|
809
|
+
matchId: string
|
|
810
|
+
PendingComponent: any
|
|
811
|
+
}): any {
|
|
812
|
+
const router = useRouter()
|
|
813
|
+
|
|
814
|
+
const match = useRouterState({
|
|
815
|
+
select: (d) => {
|
|
816
|
+
const match = d.matchesById[matchId]
|
|
817
|
+
return pick(match!, ['status', 'loadPromise', 'routeId', 'error'])
|
|
818
|
+
},
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
const route = router.getRoute(match.routeId)
|
|
822
|
+
|
|
823
|
+
if (match.status === 'error') {
|
|
824
|
+
throw match.error
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (match.status === 'pending') {
|
|
828
|
+
return React.createElement(PendingComponent, {
|
|
829
|
+
useLoader: route.useLoader,
|
|
830
|
+
useMatch: route.useMatch,
|
|
831
|
+
useContext: route.useContext,
|
|
832
|
+
useRouteContext: route.useRouteContext,
|
|
833
|
+
useSearch: route.useSearch,
|
|
834
|
+
useParams: route.useParams,
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (match.status === 'success') {
|
|
839
|
+
let comp = route.options.component ?? router.options.defaultComponent
|
|
840
|
+
|
|
841
|
+
if (comp) {
|
|
842
|
+
return React.createElement(comp, {
|
|
843
|
+
useLoader: route.useLoader,
|
|
844
|
+
useMatch: route.useMatch,
|
|
845
|
+
useContext: route.useContext,
|
|
846
|
+
useRouteContext: route.useRouteContext,
|
|
847
|
+
useSearch: route.useSearch,
|
|
848
|
+
useParams: route.useParams,
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return <Outlet />
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
invariant(
|
|
856
|
+
false,
|
|
857
|
+
'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function SafeFragment(props: any) {
|
|
862
|
+
return <>{props.children}</>
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export function useInjectHtml() {
|
|
866
|
+
const router = useRouter()
|
|
867
|
+
|
|
868
|
+
return React.useCallback(
|
|
869
|
+
(html: string | (() => Promise<string> | string)) => {
|
|
870
|
+
router.injectHtml(html)
|
|
871
|
+
},
|
|
872
|
+
[],
|
|
873
|
+
)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function useDehydrate() {
|
|
877
|
+
const router = useRouter()
|
|
878
|
+
|
|
879
|
+
return React.useCallback(function dehydrate<T>(
|
|
880
|
+
key: any,
|
|
881
|
+
data: T | (() => Promise<T> | T),
|
|
882
|
+
) {
|
|
883
|
+
return router.dehydrateData(key, data)
|
|
884
|
+
},
|
|
885
|
+
[])
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function useHydrate() {
|
|
889
|
+
const router = useRouter()
|
|
890
|
+
|
|
891
|
+
return function hydrate<T = unknown>(key: any) {
|
|
892
|
+
return router.hydrateData(key) as T
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// This is the messiest thing ever... I'm either seriously tired (likely) or
|
|
897
|
+
// there has to be a better way to reset error boundaries when the
|
|
898
|
+
// router's location key changes.
|
|
899
|
+
|
|
526
900
|
class CatchBoundary extends React.Component<{
|
|
527
901
|
children: any
|
|
528
902
|
errorComponent: any
|
|
903
|
+
onCatch: (error: any, info: any) => void
|
|
529
904
|
}> {
|
|
530
905
|
state = {
|
|
531
906
|
error: false,
|
|
907
|
+
info: undefined,
|
|
532
908
|
}
|
|
533
909
|
componentDidCatch(error: any, info: any) {
|
|
534
|
-
|
|
535
|
-
|
|
910
|
+
this.props.onCatch(error, info)
|
|
536
911
|
this.setState({
|
|
537
912
|
error,
|
|
538
913
|
info,
|
|
539
914
|
})
|
|
540
915
|
}
|
|
541
916
|
render() {
|
|
542
|
-
|
|
917
|
+
return (
|
|
918
|
+
<CatchBoundaryInner
|
|
919
|
+
{...this.props}
|
|
920
|
+
errorState={this.state}
|
|
921
|
+
reset={() => this.setState({})}
|
|
922
|
+
/>
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function CatchBoundaryInner(props: {
|
|
928
|
+
children: any
|
|
929
|
+
errorComponent: any
|
|
930
|
+
errorState: { error: unknown; info: any }
|
|
931
|
+
reset: () => void
|
|
932
|
+
}) {
|
|
933
|
+
const locationKey = useRouterState({
|
|
934
|
+
select: (d) => d.resolvedLocation.key,
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
const [activeErrorState, setActiveErrorState] = React.useState(
|
|
938
|
+
props.errorState,
|
|
939
|
+
)
|
|
940
|
+
const errorComponent = props.errorComponent ?? ErrorComponent
|
|
941
|
+
const prevKeyRef = React.useRef('' as any)
|
|
543
942
|
|
|
544
|
-
|
|
545
|
-
|
|
943
|
+
React.useEffect(() => {
|
|
944
|
+
if (activeErrorState) {
|
|
945
|
+
if (locationKey !== prevKeyRef.current) {
|
|
946
|
+
setActiveErrorState({} as any)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
prevKeyRef.current = locationKey
|
|
951
|
+
}, [activeErrorState, locationKey])
|
|
952
|
+
|
|
953
|
+
React.useEffect(() => {
|
|
954
|
+
if (props.errorState.error) {
|
|
955
|
+
setActiveErrorState(props.errorState)
|
|
546
956
|
}
|
|
957
|
+
// props.reset()
|
|
958
|
+
}, [props.errorState.error])
|
|
547
959
|
|
|
548
|
-
|
|
960
|
+
if (props.errorState.error && activeErrorState.error) {
|
|
961
|
+
return React.createElement(errorComponent, activeErrorState)
|
|
549
962
|
}
|
|
963
|
+
|
|
964
|
+
return props.children
|
|
550
965
|
}
|
|
551
966
|
|
|
552
|
-
export function
|
|
967
|
+
export function ErrorComponent({ error }: { error: any }) {
|
|
968
|
+
const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
|
|
969
|
+
|
|
553
970
|
return (
|
|
554
971
|
<div style={{ padding: '.5rem', maxWidth: '100%' }}>
|
|
555
|
-
<
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
</code>
|
|
571
|
-
) : null}
|
|
572
|
-
</pre>
|
|
972
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
|
|
973
|
+
<strong style={{ fontSize: '1rem' }}>Something went wrong!</strong>
|
|
974
|
+
<button
|
|
975
|
+
style={{
|
|
976
|
+
appearance: 'none',
|
|
977
|
+
fontSize: '.6em',
|
|
978
|
+
border: '1px solid currentColor',
|
|
979
|
+
padding: '.1rem .2rem',
|
|
980
|
+
fontWeight: 'bold',
|
|
981
|
+
borderRadius: '.25rem',
|
|
982
|
+
}}
|
|
983
|
+
onClick={() => setShow((d) => !d)}
|
|
984
|
+
>
|
|
985
|
+
{show ? 'Hide Error' : 'Show Error'}
|
|
986
|
+
</button>
|
|
573
987
|
</div>
|
|
988
|
+
<div style={{ height: '.25rem' }} />
|
|
989
|
+
{show ? (
|
|
990
|
+
<div>
|
|
991
|
+
<pre
|
|
992
|
+
style={{
|
|
993
|
+
fontSize: '.7em',
|
|
994
|
+
border: '1px solid red',
|
|
995
|
+
borderRadius: '.25rem',
|
|
996
|
+
padding: '.3rem',
|
|
997
|
+
color: 'red',
|
|
998
|
+
overflow: 'auto',
|
|
999
|
+
}}
|
|
1000
|
+
>
|
|
1001
|
+
{error.message ? <code>{error.message}</code> : null}
|
|
1002
|
+
</pre>
|
|
1003
|
+
</div>
|
|
1004
|
+
) : null}
|
|
574
1005
|
</div>
|
|
575
1006
|
)
|
|
576
1007
|
}
|
|
577
1008
|
|
|
578
|
-
export function
|
|
1009
|
+
export function useBlocker(
|
|
1010
|
+
message: string,
|
|
1011
|
+
condition: boolean | any = true,
|
|
1012
|
+
): void {
|
|
579
1013
|
const router = useRouter()
|
|
580
1014
|
|
|
581
1015
|
React.useEffect(() => {
|
|
582
|
-
if (!
|
|
1016
|
+
if (!condition) return
|
|
583
1017
|
|
|
584
|
-
let unblock = router.history.block((
|
|
1018
|
+
let unblock = router.history.block((retry, cancel) => {
|
|
585
1019
|
if (window.confirm(message)) {
|
|
586
1020
|
unblock()
|
|
587
|
-
|
|
588
|
-
} else {
|
|
589
|
-
router.location.pathname = window.location.pathname
|
|
1021
|
+
retry()
|
|
590
1022
|
}
|
|
591
1023
|
})
|
|
592
1024
|
|
|
593
1025
|
return unblock
|
|
594
|
-
}
|
|
1026
|
+
})
|
|
595
1027
|
}
|
|
596
1028
|
|
|
597
|
-
export function
|
|
598
|
-
|
|
599
|
-
return (children ?? null) as
|
|
1029
|
+
export function Block({ message, condition, children }: PromptProps) {
|
|
1030
|
+
useBlocker(message, condition)
|
|
1031
|
+
return (children ?? null) as ReactNode
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export function shallow<T>(objA: T, objB: T) {
|
|
1035
|
+
if (Object.is(objA, objB)) {
|
|
1036
|
+
return true
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (
|
|
1040
|
+
typeof objA !== 'object' ||
|
|
1041
|
+
objA === null ||
|
|
1042
|
+
typeof objB !== 'object' ||
|
|
1043
|
+
objB === null
|
|
1044
|
+
) {
|
|
1045
|
+
return false
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const keysA = Object.keys(objA)
|
|
1049
|
+
if (keysA.length !== Object.keys(objB).length) {
|
|
1050
|
+
return false
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
1054
|
+
if (
|
|
1055
|
+
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
|
|
1056
|
+
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
|
|
1057
|
+
) {
|
|
1058
|
+
return false
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return true
|
|
600
1062
|
}
|