@tanstack/react-router 0.0.1-beta.14 → 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 -2732
- 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 +2086 -2372
- 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 -470
- package/build/cjs/react-router/src/index.js +0 -413
- package/build/cjs/react-router/src/index.js.map +0 -1
- package/build/cjs/router-core/build/esm/index.js +0 -2491
- 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,438 +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()
|
|
481
|
+
|
|
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
|
+
})
|
|
382
488
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (opts?.strict ?? true) {
|
|
387
|
-
invariant(
|
|
388
|
-
match,
|
|
389
|
-
`Could not find an active match for "${routeId as string}"!`,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
invariant(
|
|
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
|
-
},
|
|
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
|
|
416
552
|
|
|
417
|
-
|
|
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)
|
|
559
|
+
|
|
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
|
-
|
|
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
|
+
}
|
|
451
660
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
[],
|
|
458
711
|
)
|
|
459
712
|
}
|
|
460
713
|
|
|
461
|
-
export function
|
|
462
|
-
const
|
|
463
|
-
warning(!value, 'useRouter must be used inside a <Router> component!')
|
|
714
|
+
export function useMatchRoute() {
|
|
715
|
+
const router = useRouter()
|
|
464
716
|
|
|
465
|
-
|
|
717
|
+
return React.useCallback(
|
|
718
|
+
<TFrom extends string = '/', TTo extends string = ''>(
|
|
719
|
+
opts: MakeUseMatchRouteOptions<TFrom, TTo>,
|
|
720
|
+
) => {
|
|
721
|
+
const { pending, caseSensitive, ...rest } = opts
|
|
466
722
|
|
|
467
|
-
|
|
723
|
+
return router.matchRoute(rest as any, {
|
|
724
|
+
pending,
|
|
725
|
+
caseSensitive,
|
|
726
|
+
})
|
|
727
|
+
},
|
|
728
|
+
[],
|
|
729
|
+
)
|
|
468
730
|
}
|
|
469
731
|
|
|
470
|
-
export function
|
|
471
|
-
|
|
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
|
|
472
743
|
}
|
|
473
744
|
|
|
474
745
|
export function Outlet() {
|
|
475
|
-
const
|
|
476
|
-
const matches = useMatches().slice(1)
|
|
477
|
-
const match = matches[0]
|
|
746
|
+
const matchIds = React.useContext(matchIdsContext).slice(1)
|
|
478
747
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (!match) {
|
|
748
|
+
if (!matchIds[0]) {
|
|
482
749
|
return null
|
|
483
750
|
}
|
|
484
751
|
|
|
485
|
-
|
|
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 ??
|
|
486
764
|
router.options.defaultPendingComponent ??
|
|
487
765
|
defaultPending) as any
|
|
488
766
|
|
|
489
767
|
const errorComponent =
|
|
490
|
-
|
|
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
|
|
491
778
|
|
|
492
779
|
return (
|
|
493
|
-
<
|
|
494
|
-
<
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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>
|
|
802
|
+
)
|
|
803
|
+
}
|
|
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
|
+
[],
|
|
521
873
|
)
|
|
522
874
|
}
|
|
523
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
|
+
|
|
524
900
|
class CatchBoundary extends React.Component<{
|
|
525
901
|
children: any
|
|
526
902
|
errorComponent: any
|
|
903
|
+
onCatch: (error: any, info: any) => void
|
|
527
904
|
}> {
|
|
528
905
|
state = {
|
|
529
906
|
error: false,
|
|
907
|
+
info: undefined,
|
|
530
908
|
}
|
|
531
909
|
componentDidCatch(error: any, info: any) {
|
|
532
|
-
|
|
533
|
-
|
|
910
|
+
this.props.onCatch(error, info)
|
|
534
911
|
this.setState({
|
|
535
912
|
error,
|
|
536
913
|
info,
|
|
537
914
|
})
|
|
538
915
|
}
|
|
539
916
|
render() {
|
|
540
|
-
|
|
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)
|
|
541
942
|
|
|
542
|
-
|
|
543
|
-
|
|
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)
|
|
544
956
|
}
|
|
957
|
+
// props.reset()
|
|
958
|
+
}, [props.errorState.error])
|
|
545
959
|
|
|
546
|
-
|
|
960
|
+
if (props.errorState.error && activeErrorState.error) {
|
|
961
|
+
return React.createElement(errorComponent, activeErrorState)
|
|
547
962
|
}
|
|
963
|
+
|
|
964
|
+
return props.children
|
|
548
965
|
}
|
|
549
966
|
|
|
550
|
-
export function
|
|
967
|
+
export function ErrorComponent({ error }: { error: any }) {
|
|
968
|
+
const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
|
|
969
|
+
|
|
551
970
|
return (
|
|
552
971
|
<div style={{ padding: '.5rem', maxWidth: '100%' }}>
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
</code>
|
|
569
|
-
) : null}
|
|
570
|
-
</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>
|
|
571
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}
|
|
572
1005
|
</div>
|
|
573
1006
|
)
|
|
574
1007
|
}
|
|
575
1008
|
|
|
576
|
-
export function
|
|
1009
|
+
export function useBlocker(
|
|
1010
|
+
message: string,
|
|
1011
|
+
condition: boolean | any = true,
|
|
1012
|
+
): void {
|
|
577
1013
|
const router = useRouter()
|
|
578
1014
|
|
|
579
1015
|
React.useEffect(() => {
|
|
580
|
-
if (!
|
|
1016
|
+
if (!condition) return
|
|
581
1017
|
|
|
582
|
-
let unblock = router.history.block((
|
|
1018
|
+
let unblock = router.history.block((retry, cancel) => {
|
|
583
1019
|
if (window.confirm(message)) {
|
|
584
1020
|
unblock()
|
|
585
|
-
|
|
586
|
-
} else {
|
|
587
|
-
router.location.pathname = window.location.pathname
|
|
1021
|
+
retry()
|
|
588
1022
|
}
|
|
589
1023
|
})
|
|
590
1024
|
|
|
591
1025
|
return unblock
|
|
592
|
-
}
|
|
1026
|
+
})
|
|
593
1027
|
}
|
|
594
1028
|
|
|
595
|
-
export function
|
|
596
|
-
|
|
597
|
-
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
|
|
598
1062
|
}
|