@tanstack/react-router 0.0.1-beta.18 → 0.0.1-beta.181

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