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

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,1213 @@
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 = state.matches
685
+ const match = opts?.from
686
+ ? matches.find((d) => d.routeId === opts?.from)
687
+ : matches.find((d) => d.id === nearestMatchId)
688
+
689
+ return match!.routeId
690
+ },
691
+ })
692
+
693
+ if (opts?.strict ?? true) {
694
+ invariant(
695
+ nearestMatchRouteId == matchRouteId,
696
+ `useMatch("${
697
+ matchRouteId as string
698
+ }") is being called in a component that is meant to render the '${nearestMatchRouteId}' route. Did you mean to 'useMatch("${
699
+ matchRouteId as string
700
+ }", { strict: false })' or 'useRoute("${
701
+ matchRouteId as string
702
+ }")' instead?`,
703
+ )
704
+ }
705
+
706
+ const matchSelection = useRouterState({
707
+ select: (state) => {
708
+ const matches = state.matches
709
+ const match = opts?.from
710
+ ? matches.find((d) => d.routeId === opts?.from)
711
+ : matches.find((d) => d.id === nearestMatchId)
712
+
713
+ invariant(
714
+ match,
715
+ `Could not find ${
716
+ opts?.from
717
+ ? `an active match from "${opts.from}"`
718
+ : 'a nearest match!'
719
+ }`,
720
+ )
721
+
722
+ return opts?.select ? opts.select(match as any) : match
723
+ },
724
+ })
725
+
726
+ return matchSelection as any
727
+ }
728
+
729
+ export type RouteFromIdOrRoute<T> = T extends ParseRoute<
730
+ RegisteredRouter['routeTree']
731
+ >
732
+ ? T
733
+ : T extends RouteIds<RegisteredRouter['routeTree']>
734
+ ? RoutesById<RegisteredRouter['routeTree']>[T]
735
+ : T extends string
736
+ ? RouteIds<RegisteredRouter['routeTree']>
737
+ : never
738
+
739
+ export function useLoader<
740
+ TFrom extends RouteIds<RegisteredRouter['routeTree']>,
741
+ TStrict extends boolean = true,
742
+ TLoader = RouteById<RegisteredRouter['routeTree'], TFrom>['types']['loader'],
743
+ TSelected = TLoader,
744
+ >(
745
+ opts: StrictOrFrom<TFrom> & {
746
+ select?: (search: TLoader) => TSelected
747
+ },
748
+ ): TStrict extends true ? TSelected : TSelected | undefined {
749
+ return useMatch({
750
+ ...(opts as any),
751
+ select: (match: RouteMatch) =>
752
+ opts?.select
753
+ ? opts?.select(match.loaderData as TLoader)
754
+ : match.loaderData,
755
+ })
756
+ }
757
+
758
+ export function useRouterContext<
759
+ TFrom extends RouteIds<RegisteredRouter['routeTree']>,
760
+ TStrict extends boolean = true,
761
+ TContext = RouteById<
762
+ RegisteredRouter['routeTree'],
763
+ TFrom
764
+ >['types']['context'],
765
+ TSelected = TContext,
766
+ >(
767
+ opts: StrictOrFrom<TFrom> & {
768
+ select?: (search: TContext) => TSelected
769
+ },
770
+ ): TStrict extends true ? TSelected : TSelected | undefined {
771
+ return useMatch({
772
+ ...(opts as any),
773
+ select: (match: RouteMatch) =>
774
+ opts?.select ? opts.select(match.context as TContext) : match.context,
775
+ })
776
+ }
777
+
778
+ export function useRouteContext<
779
+ TFrom extends RouteIds<RegisteredRouter['routeTree']>,
780
+ TStrict extends boolean = true,
781
+ TRouteContext = RouteById<
782
+ RegisteredRouter['routeTree'],
783
+ TFrom
784
+ >['types']['routeContext'],
785
+ TSelected = TRouteContext,
786
+ >(
787
+ opts: StrictOrFrom<TFrom> & {
788
+ select?: (search: TRouteContext) => TSelected
789
+ },
790
+ ): TStrict extends true ? TSelected : TSelected | undefined {
791
+ return useMatch({
792
+ ...(opts as any),
793
+ select: (match: RouteMatch) =>
794
+ opts?.select
795
+ ? opts.select(match.routeContext as TRouteContext)
796
+ : match.routeContext,
797
+ })
798
+ }
799
+
800
+ export function useSearch<
801
+ TFrom extends RouteIds<RegisteredRouter['routeTree']>,
802
+ TStrict extends boolean = true,
803
+ TSearch = RouteById<
804
+ RegisteredRouter['routeTree'],
805
+ TFrom
806
+ >['types']['fullSearchSchema'],
807
+ TSelected = TSearch,
808
+ >(
809
+ opts: StrictOrFrom<TFrom> & {
810
+ select?: (search: TSearch) => TSelected
811
+ },
812
+ ): TStrict extends true ? TSelected : TSelected | undefined {
813
+ return useMatch({
814
+ ...(opts as any),
815
+ select: (match: RouteMatch) => {
816
+ return opts?.select ? opts.select(match.search as TSearch) : match.search
817
+ },
818
+ })
819
+ }
820
+
821
+ export function useParams<
822
+ TFrom extends RouteIds<RegisteredRouter['routeTree']> = '/',
823
+ TDefaultSelected = AllParams<RegisteredRouter['routeTree']> &
824
+ RouteById<RegisteredRouter['routeTree'], TFrom>['types']['allParams'],
825
+ TSelected = TDefaultSelected,
826
+ >(
827
+ opts: StrictOrFrom<TFrom> & {
828
+ select?: (search: TDefaultSelected) => TSelected
829
+ },
830
+ ): TSelected {
831
+ return useRouterState({
832
+ select: (state: any) => {
833
+ const params = (last(state.matches) as any)?.params
834
+ return opts?.select ? opts.select(params) : params
835
+ },
836
+ })
837
+ }
838
+
839
+ export function useNavigate<
840
+ TDefaultFrom extends RoutePaths<RegisteredRouter['routeTree']> = '/',
841
+ >(defaultOpts?: { from?: TDefaultFrom }) {
842
+ const router = useRouter()
843
+ return React.useCallback(
844
+ <
845
+ TFrom extends RoutePaths<RegisteredRouter['routeTree']> = TDefaultFrom,
846
+ TTo extends string = '',
847
+ >(
848
+ opts?: NavigateOptions<RegisteredRouter['routeTree'], TFrom, TTo>,
849
+ ) => {
850
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
851
+ },
852
+ [],
853
+ )
854
+ }
855
+
856
+ export function useMatchRoute() {
857
+ const router = useRouter()
858
+
859
+ return React.useCallback(
860
+ <TFrom extends string = '/', TTo extends string = ''>(
861
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
862
+ ) => {
863
+ const { pending, caseSensitive, ...rest } = opts
864
+
865
+ return router.matchRoute(rest as any, {
866
+ pending,
867
+ caseSensitive,
868
+ })
869
+ },
870
+ [],
871
+ )
872
+ }
873
+
874
+ export function MatchRoute<TFrom extends string = '/', TTo extends string = ''>(
875
+ props: MakeMatchRouteOptions<TFrom, TTo>,
876
+ ): any {
877
+ const matchRoute = useMatchRoute()
878
+ const params = matchRoute(props)
879
+
880
+ if (typeof props.children === 'function') {
881
+ return (props.children as any)(params)
882
+ }
883
+
884
+ return !!params ? props.children : null
885
+ }
886
+
887
+ export function Outlet() {
888
+ const matchIds = React.useContext(matchIdsContext).slice(1)
889
+
890
+ if (!matchIds[0]) {
891
+ return null
892
+ }
893
+
894
+ return <Match matchIds={matchIds} />
895
+ }
896
+
897
+ const defaultPending = () => null
898
+
899
+ function Match({ matchIds }: { matchIds: string[] }) {
900
+ const router = useRouter()
901
+ const matchId = matchIds[0]!
902
+ const routeId = router.getRouteMatch(matchId)!.routeId
903
+ const route = router.getRoute(routeId)
904
+
905
+ const PendingComponent = (route.options.pendingComponent ??
906
+ router.options.defaultPendingComponent ??
907
+ defaultPending) as any
908
+
909
+ const errorComponent =
910
+ route.options.errorComponent ??
911
+ router.options.defaultErrorComponent ??
912
+ ErrorComponent
913
+
914
+ const ResolvedSuspenseBoundary =
915
+ route.options.wrapInSuspense ?? !route.isRoot
916
+ ? React.Suspense
917
+ : SafeFragment
918
+
919
+ const ResolvedCatchBoundary = !!errorComponent ? CatchBoundary : SafeFragment
920
+
921
+ return (
922
+ <matchIdsContext.Provider value={matchIds}>
923
+ <ResolvedSuspenseBoundary
924
+ fallback={React.createElement(PendingComponent, {
925
+ useMatch: route.useMatch,
926
+ useContext: route.useContext,
927
+ useRouteContext: route.useRouteContext,
928
+ useSearch: route.useSearch,
929
+ useParams: route.useParams,
930
+ })}
931
+ >
932
+ <ResolvedCatchBoundary
933
+ key={route.id}
934
+ errorComponent={errorComponent}
935
+ route={route}
936
+ onCatch={() => {
937
+ warning(false, `Error in route match: ${matchId}`)
938
+ }}
939
+ >
940
+ <MatchInner matchId={matchId} PendingComponent={PendingComponent} />
941
+ </ResolvedCatchBoundary>
942
+ </ResolvedSuspenseBoundary>
943
+ </matchIdsContext.Provider>
944
+ )
945
+ }
946
+
947
+ function MatchInner({
948
+ matchId,
949
+ PendingComponent,
950
+ }: {
951
+ matchId: string
952
+ PendingComponent: any
953
+ }): any {
954
+ const router = useRouter()
955
+
956
+ const match = useRouterState({
957
+ select: (d) => {
958
+ const match = d.matchesById[matchId]
959
+ return pick(match!, ['status', 'loadPromise', 'routeId', 'error'])
960
+ },
961
+ })
962
+
963
+ const route = router.getRoute(match.routeId)
964
+
965
+ if (match.status === 'error') {
966
+ throw match.error
967
+ }
968
+
969
+ if (match.status === 'pending') {
970
+ return React.createElement(PendingComponent, {
971
+ useLoader: route.useLoader,
972
+ useMatch: route.useMatch,
973
+ useContext: route.useContext,
974
+ useRouteContext: route.useRouteContext,
975
+ useSearch: route.useSearch,
976
+ useParams: route.useParams,
977
+ })
978
+ }
979
+
980
+ if (match.status === 'success') {
981
+ let comp = route.options.component ?? router.options.defaultComponent
982
+
983
+ if (comp) {
984
+ return React.createElement(comp, {
985
+ useLoader: route.useLoader,
986
+ useMatch: route.useMatch,
987
+ useContext: route.useContext as any,
988
+ useRouteContext: route.useRouteContext as any,
989
+ useSearch: route.useSearch,
990
+ useParams: route.useParams as any,
991
+ } as any)
992
+ }
993
+
994
+ return <Outlet />
995
+ }
996
+
997
+ invariant(
998
+ false,
999
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
1000
+ )
1001
+ }
1002
+
1003
+ function SafeFragment(props: any) {
1004
+ return <>{props.children}</>
1005
+ }
1006
+
1007
+ export function useInjectHtml() {
1008
+ const router = useRouter()
1009
+
1010
+ return React.useCallback(
1011
+ (html: string | (() => Promise<string> | string)) => {
1012
+ router.injectHtml(html)
1013
+ },
1014
+ [],
1015
+ )
1016
+ }
1017
+
1018
+ export function useDehydrate() {
1019
+ const router = useRouter()
1020
+
1021
+ return React.useCallback(function dehydrate<T>(
1022
+ key: any,
1023
+ data: T | (() => Promise<T> | T),
1024
+ ) {
1025
+ return router.dehydrateData(key, data)
1026
+ },
1027
+ [])
1028
+ }
1029
+
1030
+ export function useHydrate() {
1031
+ const router = useRouter()
1032
+
1033
+ return function hydrate<T = unknown>(key: any) {
1034
+ return router.hydrateData(key) as T
1035
+ }
1036
+ }
1037
+
1038
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
1039
+ // there has to be a better way to reset error boundaries when the
1040
+ // router's location key changes.
1041
+
1042
+ class CatchBoundary extends React.Component<{
1043
+ children: any
1044
+ errorComponent: any
1045
+ route: AnyRoute
1046
+ onCatch: (error: any, info: any) => void
1047
+ }> {
1048
+ state = {
1049
+ error: false,
1050
+ info: undefined,
1051
+ }
1052
+ componentDidCatch(error: any, info: any) {
1053
+ this.props.onCatch(error, info)
1054
+ this.setState({
1055
+ error,
1056
+ info,
1057
+ })
1058
+ }
1059
+ render() {
1060
+ return (
1061
+ <CatchBoundaryInner
1062
+ {...this.props}
1063
+ errorState={this.state}
1064
+ reset={() => this.setState({})}
1065
+ />
1066
+ )
1067
+ }
1068
+ }
1069
+
1070
+ function CatchBoundaryInner(props: {
1071
+ children: any
1072
+ errorComponent: any
1073
+ route: AnyRoute
1074
+ errorState: { error: unknown; info: any }
1075
+ reset: () => void
1076
+ }) {
1077
+ const locationKey = useRouterState({
1078
+ select: (d) => d.resolvedLocation.key,
1079
+ })
1080
+
1081
+ const [activeErrorState, setActiveErrorState] = React.useState(
1082
+ props.errorState,
1083
+ )
1084
+ const errorComponent = props.errorComponent ?? ErrorComponent
1085
+ const prevKeyRef = React.useRef('' as any)
1086
+
1087
+ React.useEffect(() => {
1088
+ if (activeErrorState) {
1089
+ if (locationKey !== prevKeyRef.current) {
1090
+ setActiveErrorState({} as any)
1091
+ }
1092
+ }
1093
+
1094
+ prevKeyRef.current = locationKey
1095
+ }, [activeErrorState, locationKey])
1096
+
1097
+ React.useEffect(() => {
1098
+ if (props.errorState.error) {
1099
+ setActiveErrorState(props.errorState)
1100
+ }
1101
+ // props.reset()
1102
+ }, [props.errorState.error])
1103
+
1104
+ if (props.errorState.error && activeErrorState.error) {
1105
+ return React.createElement(errorComponent, {
1106
+ ...activeErrorState,
1107
+ useMatch: props.route.useMatch,
1108
+ useContext: props.route.useContext,
1109
+ useRouteContext: props.route.useRouteContext,
1110
+ useSearch: props.route.useSearch,
1111
+ useParams: props.route.useParams,
1112
+ })
1113
+ }
1114
+
1115
+ return props.children
1116
+ }
1117
+
1118
+ export function ErrorComponent({ error }: { error: any }) {
1119
+ const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
1120
+
1121
+ return (
1122
+ <div style={{ padding: '.5rem', maxWidth: '100%' }}>
1123
+ <div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
1124
+ <strong style={{ fontSize: '1rem' }}>Something went wrong!</strong>
1125
+ <button
1126
+ style={{
1127
+ appearance: 'none',
1128
+ fontSize: '.6em',
1129
+ border: '1px solid currentColor',
1130
+ padding: '.1rem .2rem',
1131
+ fontWeight: 'bold',
1132
+ borderRadius: '.25rem',
1133
+ }}
1134
+ onClick={() => setShow((d) => !d)}
1135
+ >
1136
+ {show ? 'Hide Error' : 'Show Error'}
1137
+ </button>
1138
+ </div>
1139
+ <div style={{ height: '.25rem' }} />
1140
+ {show ? (
1141
+ <div>
1142
+ <pre
1143
+ style={{
1144
+ fontSize: '.7em',
1145
+ border: '1px solid red',
1146
+ borderRadius: '.25rem',
1147
+ padding: '.3rem',
1148
+ color: 'red',
1149
+ overflow: 'auto',
1150
+ }}
1151
+ >
1152
+ {error.message ? <code>{error.message}</code> : null}
1153
+ </pre>
1154
+ </div>
1155
+ ) : null}
1156
+ </div>
1157
+ )
1158
+ }
1159
+
1160
+ export function useBlocker(
1161
+ message: string,
1162
+ condition: boolean | any = true,
1163
+ ): void {
1164
+ const router = useRouter()
1165
+
1166
+ React.useEffect(() => {
1167
+ if (!condition) return
1168
+
1169
+ let unblock = router.history.block((retry, cancel) => {
1170
+ if (window.confirm(message)) {
1171
+ unblock()
1172
+ retry()
1173
+ }
1174
+ })
1175
+
1176
+ return unblock
1177
+ })
1178
+ }
1179
+
1180
+ export function Block({ message, condition, children }: PromptProps) {
1181
+ useBlocker(message, condition)
1182
+ return (children ?? null) as ReactNode
1183
+ }
1184
+
1185
+ export function shallow<T>(objA: T, objB: T) {
1186
+ if (Object.is(objA, objB)) {
1187
+ return true
1188
+ }
1189
+
1190
+ if (
1191
+ typeof objA !== 'object' ||
1192
+ objA === null ||
1193
+ typeof objB !== 'object' ||
1194
+ objB === null
1195
+ ) {
1196
+ return false
1197
+ }
1198
+
1199
+ const keysA = Object.keys(objA)
1200
+ if (keysA.length !== Object.keys(objB).length) {
1201
+ return false
1202
+ }
1203
+
1204
+ for (let i = 0; i < keysA.length; i++) {
1205
+ if (
1206
+ !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
1207
+ !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
1208
+ ) {
1209
+ return false
1210
+ }
1211
+ }
1212
+ return true
1213
+ }