@tanstack/react-router 0.0.1-beta.8 → 0.0.1-beta.81

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