@tanstack/react-router 0.0.1-beta.20 → 0.0.1-beta.201

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