@tanstack/react-router 0.0.1-beta.7 → 0.0.1-beta.71

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/src/index.tsx CHANGED
@@ -1,141 +1,72 @@
1
1
  import * as React from 'react'
2
2
 
3
- import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
-
5
- import {
6
- AnyRoute,
7
- CheckId,
8
- rootRouteId,
9
- Router,
10
- RouterState,
11
- ToIdOption,
12
- } from '@tanstack/router-core'
13
3
  import {
4
+ Route,
5
+ RegisteredRoutesInfo,
6
+ RegisteredRouter,
7
+ RouterStore,
8
+ last,
14
9
  warning,
15
10
  RouterOptions,
16
11
  RouteMatch,
17
12
  MatchRouteOptions,
18
- RouteConfig,
19
- AnyRouteConfig,
20
- AnyAllRouteInfo,
21
- DefaultAllRouteInfo,
13
+ AnyRoutesInfo,
14
+ DefaultRoutesInfo,
22
15
  functionalUpdate,
23
- createRouter,
24
- AnyRouteInfo,
25
- AllRouteInfo,
26
- RouteInfo,
16
+ RoutesInfo,
27
17
  ValidFromPath,
28
18
  LinkOptions,
29
- RouteInfoByPath,
19
+ RouteByPath,
30
20
  ResolveRelativePath,
31
21
  NoInfer,
32
22
  ToOptions,
33
23
  invariant,
34
- } from '@tanstack/router-core'
24
+ Router,
25
+ AnyRootRoute,
26
+ RootRoute,
27
+ AnyRouteMatch,
28
+ NavigateOptions,
29
+ RouterConstructorOptions,
30
+ } from '@tanstack/router'
31
+ import { useStore } from '@tanstack/react-store'
35
32
 
36
- export * from '@tanstack/router-core'
33
+ //
37
34
 
38
- declare module '@tanstack/router-core' {
39
- interface FrameworkGenerics {
40
- Element: React.ReactNode
41
- // Any is required here so import() will work without having to do import().then(d => d.default)
42
- SyncOrAsyncElement: React.ReactNode | (() => Promise<any>)
43
- }
35
+ export * from '@tanstack/router'
44
36
 
45
- interface Router<
46
- TRouteConfig extends AnyRouteConfig = RouteConfig,
47
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
48
- > {
49
- useState: () => RouterState
50
- useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
51
- routeId: TId,
52
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
53
- useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
54
- routeId: TId,
55
- ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
56
- linkProps: <TTo extends string = '.'>(
57
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
58
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
59
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
60
- Link: <TTo extends string = '.'>(
61
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
62
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
63
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
64
- // 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
65
- children?:
66
- | React.ReactNode
67
- | ((state: { isActive: boolean }) => React.ReactNode)
68
- },
69
- ) => JSX.Element
70
- MatchRoute: <TTo extends string = '.'>(
71
- props: ToOptions<TAllRouteInfo, '/', TTo> &
72
- MatchRouteOptions & {
73
- // 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
74
- children?:
75
- | React.ReactNode
76
- | ((
77
- params: RouteInfoByPath<
78
- TAllRouteInfo,
79
- ResolveRelativePath<'/', NoInfer<TTo>>
80
- >['allParams'],
81
- ) => React.ReactNode)
82
- },
83
- ) => JSX.Element
84
- }
37
+ export { useStore }
85
38
 
86
- interface Route<
87
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
88
- TRouteInfo extends AnyRouteInfo = RouteInfo,
89
- > {
90
- useRoute: <
91
- TTo extends string = '.',
92
- TResolved extends string = ResolveRelativePath<
93
- TRouteInfo['id'],
94
- NoInfer<TTo>
95
- >,
96
- >(
97
- routeId: CheckId<
98
- TAllRouteInfo,
99
- TResolved,
100
- ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
101
- >,
102
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
103
- linkProps: <TTo extends string = '.'>(
104
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
105
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
106
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
107
- Link: <TTo extends string = '.'>(
108
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
109
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
110
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
111
- // 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
112
- children?:
113
- | React.ReactNode
114
- | ((state: { isActive: boolean }) => React.ReactNode)
115
- },
116
- ) => JSX.Element
117
- MatchRoute: <TTo extends string = '.'>(
118
- props: ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
119
- MatchRouteOptions & {
120
- // 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
121
- children?:
122
- | React.ReactNode
123
- | ((
124
- params: RouteInfoByPath<
125
- TAllRouteInfo,
126
- ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
127
- >['allParams'],
128
- ) => React.ReactNode)
129
- },
130
- ) => JSX.Element
39
+ //
40
+
41
+ type ReactNode = any
42
+
43
+ export type SyncRouteComponent<TProps = {}> = (props: TProps) => ReactNode
44
+
45
+ export type RouteComponent<TProps = {}> = SyncRouteComponent<TProps> & {
46
+ preload?: () => Promise<void>
47
+ }
48
+
49
+ export function lazy(
50
+ importer: () => Promise<{ default: SyncRouteComponent }>,
51
+ ): RouteComponent {
52
+ const lazyComp = React.lazy(importer as any)
53
+ let preloaded: Promise<SyncRouteComponent>
54
+
55
+ const finalComp = lazyComp as unknown as RouteComponent
56
+
57
+ finalComp.preload = async () => {
58
+ if (!preloaded) {
59
+ await importer()
60
+ }
131
61
  }
62
+
63
+ return finalComp
132
64
  }
133
65
 
134
- type LinkPropsOptions<
135
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
136
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
137
- TTo extends string = '.',
138
- > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
66
+ export type LinkPropsOptions<
67
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
68
+ TTo extends string = '',
69
+ > = LinkOptions<RegisteredRoutesInfo, TFrom, TTo> & {
139
70
  // 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)
140
71
  activeProps?:
141
72
  | React.AnchorHTMLAttributes<HTMLAnchorElement>
@@ -146,475 +77,674 @@ type LinkPropsOptions<
146
77
  | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
147
78
  }
148
79
 
80
+ export type MakeUseMatchRouteOptions<
81
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
82
+ TTo extends string = '',
83
+ > = ToOptions<RegisteredRoutesInfo, TFrom, TTo> & MatchRouteOptions
84
+
85
+ export type MakeMatchRouteOptions<
86
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
87
+ TTo extends string = '',
88
+ > = ToOptions<RegisteredRoutesInfo, TFrom, TTo> &
89
+ MatchRouteOptions & {
90
+ // 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
91
+ children?:
92
+ | ReactNode
93
+ | ((
94
+ params: RouteByPath<
95
+ RegisteredRoutesInfo,
96
+ ResolveRelativePath<TFrom, NoInfer<TTo>>
97
+ >['__types']['allParams'],
98
+ ) => ReactNode)
99
+ }
100
+
101
+ export type MakeLinkPropsOptions<
102
+ TFrom extends ValidFromPath<RegisteredRoutesInfo> = '/',
103
+ TTo extends string = '',
104
+ > = LinkPropsOptions<TFrom, TTo> & React.AnchorHTMLAttributes<HTMLAnchorElement>
105
+
106
+ export type MakeLinkOptions<
107
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
108
+ TTo extends string = '',
109
+ > = LinkPropsOptions<TFrom, TTo> &
110
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
111
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
112
+ // 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
113
+ children?: ReactNode | ((state: { isActive: boolean }) => ReactNode)
114
+ }
115
+
116
+ declare module '@tanstack/router' {
117
+ interface FrameworkGenerics {
118
+ Component: RouteComponent
119
+ ErrorComponent: RouteComponent<{
120
+ error: Error
121
+ info: { componentStack: string }
122
+ }>
123
+ }
124
+
125
+ interface RouterOptions<TRouteTree> {
126
+ // ssrFooter?: () => JSX.Element | Node
127
+ }
128
+
129
+ interface FrameworkRouteOptions {
130
+ wrapInSuspense?: boolean
131
+ }
132
+ }
133
+
149
134
  export type PromptProps = {
150
135
  message: string
151
136
  when?: boolean | any
152
- children?: React.ReactNode
137
+ children?: ReactNode
153
138
  }
154
139
 
155
140
  //
156
141
 
157
- const matchesContext = React.createContext<RouteMatch[]>(null!)
158
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
142
+ export function useLinkProps<
143
+ TFrom extends ValidFromPath<RegisteredRoutesInfo> = '/',
144
+ TTo extends string = '',
145
+ >(
146
+ options: MakeLinkPropsOptions<TFrom, TTo>,
147
+ ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
148
+ const router = useRouterContext()
149
+
150
+ const {
151
+ // custom props
152
+ type,
153
+ children,
154
+ target,
155
+ activeProps = () => ({ className: 'active' }),
156
+ inactiveProps = () => ({}),
157
+ activeOptions,
158
+ disabled,
159
+ // fromCurrent,
160
+ hash,
161
+ search,
162
+ params,
163
+ to = '.',
164
+ preload,
165
+ preloadDelay,
166
+ replace,
167
+ // element props
168
+ style,
169
+ className,
170
+ onClick,
171
+ onFocus,
172
+ onMouseEnter,
173
+ onMouseLeave,
174
+ onTouchStart,
175
+ ...rest
176
+ } = options
177
+
178
+ const linkInfo = router.buildLink(options as any)
179
+
180
+ if (linkInfo.type === 'external') {
181
+ const { href } = linkInfo
182
+ return { href }
183
+ }
159
184
 
160
- // Detect if we're in the DOM
161
- const isDOM = Boolean(
162
- typeof window !== 'undefined' &&
163
- window.document &&
164
- window.document.createElement,
165
- )
185
+ const {
186
+ handleClick,
187
+ handleFocus,
188
+ handleEnter,
189
+ handleLeave,
190
+ handleTouchStart,
191
+ isActive,
192
+ next,
193
+ } = linkInfo
194
+
195
+ const reactHandleClick = (e: Event) => {
196
+ if (React.startTransition) {
197
+ // This is a hack for react < 18
198
+ React.startTransition(() => {
199
+ handleClick(e)
200
+ })
201
+ } else {
202
+ handleClick(e)
203
+ }
204
+ }
166
205
 
167
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
206
+ const composeHandlers =
207
+ (handlers: (undefined | ((e: any) => void))[]) =>
208
+ (e: React.SyntheticEvent) => {
209
+ if (e.persist) e.persist()
210
+ handlers.filter(Boolean).forEach((handler) => {
211
+ if (e.defaultPrevented) return
212
+ handler!(e)
213
+ })
214
+ }
168
215
 
169
- export type MatchesProviderProps = {
170
- value: RouteMatch[]
171
- children: React.ReactNode
216
+ // Get the active props
217
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
218
+ ? functionalUpdate(activeProps as any, {}) ?? {}
219
+ : {}
220
+
221
+ // Get the inactive props
222
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
223
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
224
+
225
+ return {
226
+ ...resolvedActiveProps,
227
+ ...resolvedInactiveProps,
228
+ ...rest,
229
+ href: disabled ? undefined : next.href,
230
+ onClick: composeHandlers([onClick, reactHandleClick]),
231
+ onFocus: composeHandlers([onFocus, handleFocus]),
232
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
233
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
234
+ onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
235
+ target,
236
+ style: {
237
+ ...style,
238
+ ...resolvedActiveProps.style,
239
+ ...resolvedInactiveProps.style,
240
+ },
241
+ className:
242
+ [
243
+ className,
244
+ resolvedActiveProps.className,
245
+ resolvedInactiveProps.className,
246
+ ]
247
+ .filter(Boolean)
248
+ .join(' ') || undefined,
249
+ ...(disabled
250
+ ? {
251
+ role: 'link',
252
+ 'aria-disabled': true,
253
+ }
254
+ : undefined),
255
+ ['data-status']: isActive ? 'active' : undefined,
256
+ }
172
257
  }
173
258
 
174
- export function MatchesProvider(props: MatchesProviderProps) {
175
- return <matchesContext.Provider {...props} />
259
+ export interface LinkFn<
260
+ TDefaultFrom extends RegisteredRoutesInfo['routePaths'] = '/',
261
+ TDefaultTo extends string = '',
262
+ > {
263
+ <
264
+ TFrom extends RegisteredRoutesInfo['routePaths'] = TDefaultFrom,
265
+ TTo extends string = TDefaultTo,
266
+ >(
267
+ props: MakeLinkOptions<TFrom, TTo> & React.RefAttributes<HTMLAnchorElement>,
268
+ ): ReactNode
176
269
  }
177
270
 
178
- const useRouterSubscription = (router: Router<any, any>) => {
179
- useSyncExternalStore(
180
- (cb) => router.subscribe(() => cb()),
181
- () => router.state,
182
- () => router.state,
271
+ export const Link: LinkFn = React.forwardRef((props: any, ref) => {
272
+ const linkProps = useLinkProps(props)
273
+
274
+ return (
275
+ <a
276
+ {...{
277
+ ref: ref as any,
278
+ ...linkProps,
279
+ children:
280
+ typeof props.children === 'function'
281
+ ? props.children({
282
+ isActive: (linkProps as any)['data-status'] === 'active',
283
+ })
284
+ : props.children,
285
+ }}
286
+ />
183
287
  )
184
- }
288
+ }) as any
185
289
 
186
- export function createReactRouter<
187
- TRouteConfig extends AnyRouteConfig = RouteConfig,
188
- >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
189
- const makeRouteExt = (
190
- route: AnyRoute,
191
- router: Router<any, any>,
192
- ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
193
- return {
194
- useRoute: (subRouteId = '.' as any) => {
195
- const resolvedRouteId = router.resolvePath(
196
- route.routeId,
197
- subRouteId as string,
198
- )
199
- const resolvedRoute = router.getRoute(resolvedRouteId)
200
- useRouterSubscription(router)
201
- invariant(
202
- resolvedRoute,
203
- `Could not find a route for route "${
204
- resolvedRouteId as string
205
- }"! Did you forget to add it to your route config?`,
206
- )
207
- return resolvedRoute
208
- },
209
- linkProps: (options) => {
210
- const {
211
- // custom props
212
- type,
213
- children,
214
- target,
215
- activeProps = () => ({ className: 'active' }),
216
- inactiveProps = () => ({}),
217
- activeOptions,
218
- disabled,
219
- // fromCurrent,
220
- hash,
221
- search,
222
- params,
223
- to,
224
- preload,
225
- preloadDelay,
226
- preloadMaxAge,
227
- replace,
228
- // element props
229
- style,
230
- className,
231
- onClick,
232
- onFocus,
233
- onMouseEnter,
234
- onMouseLeave,
235
- onTouchStart,
236
- onTouchEnd,
237
- ...rest
238
- } = options
239
-
240
- const linkInfo = route.buildLink(options)
241
-
242
- if (linkInfo.type === 'external') {
243
- const { href } = linkInfo
244
- return { href }
245
- }
290
+ export function Navigate<
291
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
292
+ TTo extends string = '',
293
+ >(props: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>): null {
294
+ const router = useRouterContext()
246
295
 
247
- const {
248
- handleClick,
249
- handleFocus,
250
- handleEnter,
251
- handleLeave,
252
- isActive,
253
- next,
254
- } = linkInfo
255
-
256
- const composeHandlers =
257
- (handlers: (undefined | ((e: any) => void))[]) =>
258
- (e: React.SyntheticEvent) => {
259
- e.persist()
260
- handlers.forEach((handler) => {
261
- if (handler) handler(e)
262
- })
263
- }
264
-
265
- // Get the active props
266
- const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
267
- isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
268
-
269
- // Get the inactive props
270
- const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
271
- isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
272
-
273
- return {
274
- ...resolvedActiveProps,
275
- ...resolvedInactiveProps,
276
- ...rest,
277
- href: disabled ? undefined : next.href,
278
- onClick: composeHandlers([handleClick, onClick]),
279
- onFocus: composeHandlers([handleFocus, onFocus]),
280
- onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
281
- onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
282
- target,
283
- style: {
284
- ...style,
285
- ...resolvedActiveProps.style,
286
- ...resolvedInactiveProps.style,
287
- },
288
- className:
289
- [
290
- className,
291
- resolvedActiveProps.className,
292
- resolvedInactiveProps.className,
293
- ]
294
- .filter(Boolean)
295
- .join(' ') || undefined,
296
- ...(disabled
297
- ? {
298
- role: 'link',
299
- 'aria-disabled': true,
300
- }
301
- : undefined),
302
- ['data-status']: isActive ? 'active' : undefined,
303
- }
304
- },
305
- Link: React.forwardRef((props: any, ref) => {
306
- const linkProps = route.linkProps(props)
307
-
308
- useRouterSubscription(router)
309
-
310
- return (
311
- <a
312
- {...{
313
- ref: ref as any,
314
- ...linkProps,
315
- children:
316
- typeof props.children === 'function'
317
- ? props.children({
318
- isActive: (linkProps as any)['data-status'] === 'active',
319
- })
320
- : props.children,
321
- }}
322
- />
323
- )
324
- }) as any,
325
- MatchRoute: (opts) => {
326
- const { pending, caseSensitive, children, ...rest } = opts
327
-
328
- const params = route.matchRoute(rest as any, {
329
- pending,
330
- caseSensitive,
331
- })
332
-
333
- if (!params) {
334
- return null
335
- }
296
+ React.useLayoutEffect(() => {
297
+ router.navigate(props as any)
298
+ }, [])
336
299
 
337
- return typeof opts.children === 'function'
338
- ? opts.children(params as any)
339
- : (opts.children as any)
340
- },
341
- }
342
- }
300
+ return null
301
+ }
343
302
 
344
- const coreRouter = createRouter<TRouteConfig>({
345
- ...opts,
346
- createRouter: (router) => {
347
- const routerExt: Pick<Router<any, any>, 'useMatch' | 'useState'> = {
348
- useState: () => {
349
- useRouterSubscription(router)
350
- return router.state
351
- },
352
- useMatch: (routeId) => {
353
- useRouterSubscription(router)
354
-
355
- invariant(
356
- routeId !== rootRouteId,
357
- `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
358
- )
359
-
360
- const runtimeMatch = useMatch()
361
- const match = router.state.matches.find((d) => d.routeId === routeId)
362
-
363
- invariant(
364
- match,
365
- `Could not find a match for route "${
366
- routeId as string
367
- }" being rendered in this component!`,
368
- )
369
-
370
- invariant(
371
- runtimeMatch.routeId == match?.routeId,
372
- `useMatch('${
373
- match?.routeId as string
374
- }') is being called in a component that is meant to render the '${
375
- runtimeMatch.routeId
376
- }' route. Did you mean to 'useRoute(${
377
- match?.routeId as string
378
- })' instead?`,
379
- )
380
-
381
- if (!match) {
382
- invariant('Match not found!')
383
- }
384
-
385
- return match
386
- },
387
- }
303
+ type MatchesContextValue = AnyRouteMatch[]
388
304
 
389
- const routeExt = makeRouteExt(router.getRoute('/'), router)
305
+ export const matchesContext = React.createContext<MatchesContextValue>(null!)
306
+ export const routerContext = React.createContext<{ router: RegisteredRouter }>(
307
+ null!,
308
+ )
390
309
 
391
- Object.assign(router, routerExt, routeExt)
392
- },
393
- createRoute: ({ router, route }) => {
394
- const routeExt = makeRouteExt(route, router)
310
+ export type MatchesProviderProps = {
311
+ value: MatchesContextValue
312
+ children: ReactNode
313
+ }
395
314
 
396
- Object.assign(route, routeExt)
397
- },
398
- createElement: async (element) => {
399
- if (typeof element === 'function') {
400
- const res = (await element()) as any
401
-
402
- // Support direct import() calls
403
- if (typeof res === 'object' && res.default) {
404
- return React.createElement(res.default)
405
- } else {
406
- return res
315
+ export class ReactRouter<
316
+ TRouteConfig extends AnyRootRoute = RootRoute,
317
+ TRoutesInfo extends AnyRoutesInfo = RoutesInfo<TRouteConfig>,
318
+ > extends Router<TRouteConfig, TRoutesInfo> {
319
+ constructor(opts: RouterConstructorOptions<TRouteConfig>) {
320
+ super({
321
+ ...opts,
322
+ loadComponent: async (component) => {
323
+ if (component.preload) {
324
+ await component.preload()
407
325
  }
408
- }
409
-
410
- return element
411
- },
412
- })
413
326
 
414
- return coreRouter as any
327
+ return component as any
328
+ },
329
+ })
330
+ }
415
331
  }
416
332
 
417
333
  export type RouterProps<
418
- TRouteConfig extends AnyRouteConfig = RouteConfig,
419
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
334
+ TRouteConfig extends AnyRootRoute = RootRoute,
335
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
420
336
  > = RouterOptions<TRouteConfig> & {
421
- router: Router<TRouteConfig, TAllRouteInfo>
422
- // Children will default to `<Outlet />` if not provided
423
- children?: React.ReactNode
337
+ router: Router<TRouteConfig, TRoutesInfo>
424
338
  }
425
339
 
426
340
  export function RouterProvider<
427
- TRouteConfig extends AnyRouteConfig = RouteConfig,
428
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
429
- >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
341
+ TRouteConfig extends AnyRootRoute = RootRoute,
342
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
343
+ >({ router, ...rest }: RouterProps<TRouteConfig, TRoutesInfo>) {
430
344
  router.update(rest)
431
345
 
432
- useRouterSubscription(router)
346
+ const currentMatches = useStore(router.store, (s) => s.currentMatches)
433
347
 
434
- useLayoutEffect(() => {
435
- return router.mount()
436
- }, [router])
348
+ React.useEffect(router.mount, [router])
437
349
 
438
350
  return (
439
- <routerContext.Provider value={{ router }}>
440
- <MatchesProvider value={router.state.matches}>
441
- {children ?? <Outlet />}
442
- </MatchesProvider>
351
+ <routerContext.Provider value={{ router: router as any }}>
352
+ <matchesContext.Provider value={[undefined!, ...currentMatches]}>
353
+ <CatchBoundary
354
+ errorComponent={ErrorComponent}
355
+ onCatch={() => {
356
+ warning(
357
+ false,
358
+ `Error in router! Consider setting an 'errorComponent' in your RootRoute! 👍`,
359
+ )
360
+ }}
361
+ >
362
+ <Outlet />
363
+ </CatchBoundary>
364
+ </matchesContext.Provider>
443
365
  </routerContext.Provider>
444
366
  )
445
367
  }
446
368
 
447
- function useRouter(): Router {
369
+ export function useRouterContext(): RegisteredRouter {
448
370
  const value = React.useContext(routerContext)
449
- warning(!value, 'useRouter must be used inside a <Router> component!')
371
+ warning(value, 'useRouter must be used inside a <Router> component!')
450
372
 
451
- useRouterSubscription(value.router)
373
+ useStore(value.router.store)
374
+
375
+ return value.router
376
+ }
452
377
 
453
- return value.router as Router
378
+ export function useRouter<T = RouterStore>(
379
+ track?: (state: Router['store']) => T,
380
+ shallow?: boolean,
381
+ ): RegisteredRouter {
382
+ const router = useRouterContext()
383
+ useStore(router.store, track as any, shallow)
384
+ return router
454
385
  }
455
386
 
456
- function useMatches(): RouteMatch[] {
387
+ export function useMatches(): RouteMatch[] {
457
388
  return React.useContext(matchesContext)
458
389
  }
459
390
 
460
- // function useParentMatches(): RouteMatch[] {
461
- // const router = useRouter()
462
- // const match = useMatch()
463
- // const matches = router.state.matches
464
- // return matches.slice(
465
- // 0,
466
- // matches.findIndex((d) => d.matchId === match.matchId) - 1,
467
- // )
468
- // }
391
+ export function useMatch<
392
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
393
+ TStrict extends boolean = true,
394
+ TRouteMatch = RouteMatch<
395
+ RegisteredRoutesInfo,
396
+ RegisteredRoutesInfo['routesById'][TFrom]
397
+ >,
398
+ >(opts?: {
399
+ from: TFrom
400
+ strict?: TStrict
401
+ track?: (match: TRouteMatch) => any
402
+ shallow?: boolean
403
+ }): TStrict extends true ? TRouteMatch : TRouteMatch | undefined {
404
+ const router = useRouterContext()
405
+ const nearestMatch = useMatches()[0]!
406
+ const match = opts?.from
407
+ ? router.state.currentMatches.find((d) => d.route.id === opts?.from)
408
+ : nearestMatch
409
+
410
+ invariant(
411
+ match,
412
+ `Could not find ${
413
+ opts?.from ? `an active match from "${opts.from}"` : 'a nearest match!'
414
+ }`,
415
+ )
469
416
 
470
- function useMatch<T>(): RouteMatch {
471
- return useMatches()?.[0] as RouteMatch
417
+ if (opts?.strict ?? true) {
418
+ invariant(
419
+ nearestMatch.route.id == match?.route.id,
420
+ `useMatch("${
421
+ match?.route.id as string
422
+ }") is being called in a component that is meant to render the '${
423
+ nearestMatch.route.id
424
+ }' route. Did you mean to 'useMatch("${
425
+ match?.route.id as string
426
+ }", { strict: false })' or 'useRoute("${
427
+ match?.route.id as string
428
+ }")' instead?`,
429
+ )
430
+ }
431
+
432
+ useStore(
433
+ match!.store as any,
434
+ (d) => opts?.track?.(match as any) ?? match,
435
+ opts?.shallow,
436
+ )
437
+
438
+ return match as any
472
439
  }
473
440
 
474
- export function Outlet() {
475
- const router = useRouter()
476
- const [, ...matches] = useMatches()
441
+ export function useRoute<
442
+ TId extends keyof RegisteredRoutesInfo['routesById'] = '/',
443
+ >(routeId: TId): RegisteredRoutesInfo['routesById'][TId] {
444
+ const router = useRouterContext()
445
+ const resolvedRoute = router.getRoute(routeId as any)
446
+
447
+ invariant(
448
+ resolvedRoute,
449
+ `Could not find a route for route "${
450
+ routeId as string
451
+ }"! Did you forget to add it to your route?`,
452
+ )
477
453
 
478
- const childMatch = matches[0]
454
+ return resolvedRoute as any
455
+ }
479
456
 
480
- if (!childMatch) return null
457
+ export function useSearch<
458
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
459
+ TStrict extends boolean = true,
460
+ TSearch = RegisteredRoutesInfo['routesById'][TFrom]['__types']['fullSearchSchema'],
461
+ TSelected = TSearch,
462
+ >(opts?: {
463
+ from: TFrom
464
+ strict?: TStrict
465
+ track?: (search: TSearch) => TSelected
466
+ }): TStrict extends true ? TSelected : TSelected | undefined {
467
+ const match = useMatch(opts)
468
+ useStore(
469
+ (match as any).store,
470
+ (d: any) => opts?.track?.(d.search) ?? d.search,
471
+ true,
472
+ )
481
473
 
482
- const element = ((): React.ReactNode => {
483
- if (!childMatch) {
484
- return null
485
- }
474
+ return (match as unknown as RouteMatch).state.search as any
475
+ }
486
476
 
487
- const errorElement =
488
- childMatch.__.errorElement ?? router.options.defaultErrorElement
477
+ export function useParams<
478
+ TFrom extends keyof RegisteredRoutesInfo['routesById'] = '/',
479
+ TDefaultSelected = RegisteredRoutesInfo['allParams'] &
480
+ RegisteredRoutesInfo['routesById'][TFrom]['__types']['allParams'],
481
+ TSelected = TDefaultSelected,
482
+ >(opts?: {
483
+ from: TFrom
484
+ track?: (search: TDefaultSelected) => TSelected
485
+ }): TSelected {
486
+ const router = useRouterContext()
487
+ useStore(
488
+ router.store,
489
+ (d) => {
490
+ const params = last(d.currentMatches)?.params as any
491
+ return opts?.track?.(params) ?? params
492
+ },
493
+ true,
494
+ )
489
495
 
490
- if (childMatch.status === 'error') {
491
- if (errorElement) {
492
- return errorElement as any
493
- }
496
+ return last(router.state.currentMatches)?.params as any
497
+ }
494
498
 
495
- if (
496
- childMatch.options.useErrorBoundary ||
497
- router.options.useErrorBoundary
498
- ) {
499
- throw childMatch.error
500
- }
499
+ export function useNavigate<
500
+ TDefaultFrom extends keyof RegisteredRoutesInfo['routesById'] = '/',
501
+ >(defaultOpts?: { from?: TDefaultFrom }) {
502
+ const router = useRouterContext()
503
+ return React.useCallback(
504
+ <
505
+ TFrom extends keyof RegisteredRoutesInfo['routesById'] = TDefaultFrom,
506
+ TTo extends string = '',
507
+ >(
508
+ opts?: MakeLinkOptions<TFrom, TTo>,
509
+ ) => {
510
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
511
+ },
512
+ [],
513
+ )
514
+ }
501
515
 
502
- return <DefaultErrorBoundary error={childMatch.error} />
503
- }
516
+ export function useMatchRoute() {
517
+ const router = useRouterContext()
504
518
 
505
- if (childMatch.status === 'loading' || childMatch.status === 'idle') {
506
- if (childMatch.isPending) {
507
- const pendingElement =
508
- childMatch.__.pendingElement ?? router.options.defaultPendingElement
519
+ return React.useCallback(
520
+ <
521
+ TFrom extends ValidFromPath<RegisteredRoutesInfo> = '/',
522
+ TTo extends string = '',
523
+ >(
524
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
525
+ ) => {
526
+ const { pending, caseSensitive, ...rest } = opts
527
+
528
+ return router.matchRoute(rest as any, {
529
+ pending,
530
+ caseSensitive,
531
+ })
532
+ },
533
+ [],
534
+ )
535
+ }
509
536
 
510
- if (childMatch.options.pendingMs || pendingElement) {
511
- return (pendingElement as any) ?? null
512
- }
513
- }
537
+ export function MatchRoute<
538
+ TFrom extends ValidFromPath<RegisteredRoutesInfo> = '/',
539
+ TTo extends string = '',
540
+ >(props: MakeMatchRouteOptions<TFrom, TTo>): any {
541
+ const matchRoute = useMatchRoute()
542
+ const params = matchRoute(props)
543
+
544
+ if (!params) {
545
+ return null
546
+ }
547
+
548
+ if (typeof props.children === 'function') {
549
+ return (props.children as any)(params)
550
+ }
551
+
552
+ return params ? props.children : null
553
+ }
554
+
555
+ export function Outlet() {
556
+ const matches = useMatches().slice(1)
557
+ const match = matches[0]
558
+
559
+ if (!match) {
560
+ return null
561
+ }
514
562
 
515
- return null
563
+ return <SubOutlet matches={matches} match={match} />
564
+ }
565
+
566
+ function SubOutlet({
567
+ matches,
568
+ match,
569
+ }: {
570
+ matches: RouteMatch[]
571
+ match: RouteMatch
572
+ }) {
573
+ const router = useRouterContext()
574
+ useStore(match!.store, (store) => [store.status, store.error], true)
575
+
576
+ const defaultPending = React.useCallback(() => null, [])
577
+
578
+ const Inner = React.useCallback((props: { match: RouteMatch }): any => {
579
+ if (props.match.state.status === 'error') {
580
+ throw props.match.state.error
516
581
  }
517
582
 
518
- return (childMatch.__.element as any) ?? router.options.defaultElement
519
- })() as JSX.Element
583
+ if (props.match.state.status === 'success') {
584
+ return React.createElement(
585
+ (props.match.component as any) ??
586
+ router.options.defaultComponent ??
587
+ Outlet,
588
+ )
589
+ }
590
+
591
+ if (props.match.state.status === 'pending') {
592
+ throw props.match.__loadPromise
593
+ }
520
594
 
521
- const catchElement =
522
- childMatch?.options.catchElement ?? router.options.defaultCatchElement
595
+ invariant(
596
+ false,
597
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
598
+ )
599
+ }, [])
600
+
601
+ const PendingComponent = (match.pendingComponent ??
602
+ router.options.defaultPendingComponent ??
603
+ defaultPending) as any
604
+
605
+ const errorComponent =
606
+ match.errorComponent ?? router.options.defaultErrorComponent
607
+
608
+ const ResolvedSuspenseBoundary =
609
+ match.route.options.wrapInSuspense ?? true ? React.Suspense : SafeFragment
610
+ const ResolvedCatchBoundary = errorComponent ? CatchBoundary : SafeFragment
523
611
 
524
612
  return (
525
- <MatchesProvider value={matches} key={childMatch.matchId}>
526
- <CatchBoundary catchElement={catchElement}>{element}</CatchBoundary>
527
- </MatchesProvider>
613
+ <matchesContext.Provider value={matches}>
614
+ <ResolvedSuspenseBoundary fallback={<PendingComponent />}>
615
+ <ResolvedCatchBoundary
616
+ key={match.route.id}
617
+ errorComponent={errorComponent}
618
+ onCatch={() => {
619
+ warning(false, `Error in route match: ${match.id}`)
620
+ }}
621
+ >
622
+ <Inner match={match} />
623
+ </ResolvedCatchBoundary>
624
+ </ResolvedSuspenseBoundary>
625
+ </matchesContext.Provider>
528
626
  )
529
627
  }
530
628
 
629
+ function SafeFragment(props: any) {
630
+ return <>{props.children}</>
631
+ }
632
+
633
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
634
+ // there has to be a better way to reset error boundaries when the
635
+ // router's location key changes.
636
+
531
637
  class CatchBoundary extends React.Component<{
532
638
  children: any
533
- catchElement: any
639
+ errorComponent: any
640
+ onCatch: (error: any, info: any) => void
534
641
  }> {
535
642
  state = {
536
643
  error: false,
644
+ info: undefined,
537
645
  }
538
646
  componentDidCatch(error: any, info: any) {
647
+ this.props.onCatch(error, info)
539
648
  console.error(error)
540
-
541
649
  this.setState({
542
650
  error,
543
651
  info,
544
652
  })
545
653
  }
546
654
  render() {
547
- const catchElement = this.props.catchElement ?? DefaultErrorBoundary
655
+ return (
656
+ <CatchBoundaryInner
657
+ {...this.props}
658
+ errorState={this.state}
659
+ reset={() => this.setState({})}
660
+ />
661
+ )
662
+ }
663
+ }
664
+
665
+ function CatchBoundaryInner(props: {
666
+ children: any
667
+ errorComponent: any
668
+ errorState: { error: unknown; info: any }
669
+ reset: () => void
670
+ }) {
671
+ const [activeErrorState, setActiveErrorState] = React.useState(
672
+ props.errorState,
673
+ )
674
+ const router = useRouterContext()
675
+ const errorComponent = props.errorComponent ?? ErrorComponent
676
+ const prevKeyRef = React.useRef('' as any)
548
677
 
549
- if (this.state.error) {
550
- return typeof catchElement === 'function'
551
- ? catchElement(this.state)
552
- : catchElement
678
+ React.useEffect(() => {
679
+ if (activeErrorState) {
680
+ if (router.state.currentLocation.key !== prevKeyRef.current) {
681
+ setActiveErrorState({} as any)
682
+ }
553
683
  }
554
684
 
555
- return this.props.children
685
+ prevKeyRef.current = router.state.currentLocation.key
686
+ }, [activeErrorState, router.state.currentLocation.key])
687
+
688
+ React.useEffect(() => {
689
+ if (props.errorState.error) {
690
+ setActiveErrorState(props.errorState)
691
+ }
692
+ // props.reset()
693
+ }, [props.errorState.error])
694
+
695
+ if (props.errorState.error && activeErrorState.error) {
696
+ return React.createElement(errorComponent, activeErrorState)
556
697
  }
698
+
699
+ return props.children
557
700
  }
558
701
 
559
- export function DefaultErrorBoundary({ error }: { error: any }) {
702
+ export function ErrorComponent({ error }: { error: any }) {
560
703
  return (
561
704
  <div style={{ padding: '.5rem', maxWidth: '100%' }}>
562
705
  <strong style={{ fontSize: '1.2rem' }}>Something went wrong!</strong>
563
706
  <div style={{ height: '.5rem' }} />
564
707
  <div>
565
- <pre>
566
- {error.message ? (
567
- <code
568
- style={{
569
- fontSize: '.7em',
570
- border: '1px solid red',
571
- borderRadius: '.25rem',
572
- padding: '.5rem',
573
- color: 'red',
574
- }}
575
- >
576
- {error.message}
577
- </code>
578
- ) : null}
708
+ <pre
709
+ style={{
710
+ fontSize: '.7em',
711
+ border: '1px solid red',
712
+ borderRadius: '.25rem',
713
+ padding: '.5rem',
714
+ color: 'red',
715
+ overflow: 'auto',
716
+ }}
717
+ >
718
+ {error.message ? <code>{error.message}</code> : null}
579
719
  </pre>
580
720
  </div>
581
- <div style={{ height: '1rem' }} />
582
- <div
583
- style={{
584
- fontSize: '.8em',
585
- borderLeft: '3px solid rgba(127, 127, 127, 1)',
586
- paddingLeft: '.5rem',
587
- opacity: 0.5,
588
- }}
589
- >
590
- If you are the owner of this website, it's highly recommended that you
591
- configure your own custom Catch/Error boundaries for the router. You can
592
- optionally configure a boundary for each route.
593
- </div>
594
721
  </div>
595
722
  )
596
723
  }
597
724
 
598
- export function usePrompt(message: string, when: boolean | any): void {
599
- const router = useRouter()
600
-
601
- React.useEffect(() => {
602
- if (!when) return
603
-
604
- let unblock = router.history.block((transition) => {
605
- if (window.confirm(message)) {
606
- unblock()
607
- transition.retry()
608
- } else {
609
- router.location.pathname = window.location.pathname
610
- }
611
- })
725
+ // TODO: While we migrate away from the history package, these need to be disabled
726
+ // export function usePrompt(message: string, when: boolean | any): void {
727
+ // const router = useRouter()
612
728
 
613
- return unblock
614
- }, [when, location, message])
615
- }
729
+ // React.useEffect(() => {
730
+ // if (!when) return
731
+
732
+ // let unblock = router.getHistory().block((transition) => {
733
+ // if (window.confirm(message)) {
734
+ // unblock()
735
+ // transition.retry()
736
+ // } else {
737
+ // router.setStore((s) => {
738
+ // s.currentLocation.pathname = window.location.pathname
739
+ // })
740
+ // }
741
+ // })
742
+
743
+ // return unblock
744
+ // }, [when, message])
745
+ // }
616
746
 
617
- export function Prompt({ message, when, children }: PromptProps) {
618
- usePrompt(message, when ?? true)
619
- return (children ?? null) as React.ReactNode
620
- }
747
+ // export function Prompt({ message, when, children }: PromptProps) {
748
+ // usePrompt(message, when ?? true)
749
+ // return (children ?? null) as ReactNode
750
+ // }