@tanstack/react-router 0.0.1-beta.6 → 0.0.1-beta.60

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