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

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,1228 @@
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
+
389
+ const {
390
+ // custom props
391
+ type,
392
+ children,
393
+ target,
394
+ activeProps = () => ({ className: 'active' }),
395
+ inactiveProps = () => ({}),
396
+ activeOptions,
397
+ disabled,
398
+ // fromCurrent,
399
+ hash,
400
+ search,
401
+ params,
402
+ to = '.',
403
+ state,
404
+ mask,
405
+ preload,
406
+ preloadDelay,
407
+ replace,
408
+ // element props
409
+ style,
410
+ className,
411
+ onClick,
412
+ onFocus,
413
+ onMouseEnter,
414
+ onMouseLeave,
415
+ onTouchStart,
416
+ ...rest
417
+ } = options
418
+
419
+ const linkInfo = router.buildLink(options as any)
420
+
421
+ if (linkInfo.type === 'external') {
422
+ const { href } = linkInfo
423
+ return { href }
424
+ }
425
+
426
+ const {
427
+ handleClick,
428
+ handleFocus,
429
+ handleEnter,
430
+ handleLeave,
431
+ handleTouchStart,
432
+ isActive,
433
+ next,
434
+ } = linkInfo
435
+
436
+ const handleReactClick = (e: Event) => {
437
+ if (options.startTransition ?? true) {
438
+ ;(React.startTransition || ((d) => d))(() => {
439
+ handleClick(e)
440
+ })
441
+ }
442
+ }
443
+
444
+ const composeHandlers =
445
+ (handlers: (undefined | ((e: any) => void))[]) =>
446
+ (e: React.SyntheticEvent) => {
447
+ if (e.persist) e.persist()
448
+ handlers.filter(Boolean).forEach((handler) => {
449
+ if (e.defaultPrevented) return
450
+ handler!(e)
451
+ })
452
+ }
453
+
454
+ // Get the active props
455
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
456
+ ? functionalUpdate(activeProps as any, {}) ?? {}
457
+ : {}
458
+
459
+ // Get the inactive props
460
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
461
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
462
+
463
+ return {
464
+ ...resolvedActiveProps,
465
+ ...resolvedInactiveProps,
466
+ ...rest,
467
+ href: disabled
468
+ ? undefined
469
+ : next.maskedLocation
470
+ ? next.maskedLocation.href
471
+ : next.href,
472
+ onClick: composeHandlers([onClick, handleReactClick]),
473
+ onFocus: composeHandlers([onFocus, handleFocus]),
474
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
475
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
476
+ onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
477
+ target,
478
+ style: {
479
+ ...style,
480
+ ...resolvedActiveProps.style,
481
+ ...resolvedInactiveProps.style,
482
+ },
483
+ className:
484
+ [
485
+ className,
486
+ resolvedActiveProps.className,
487
+ resolvedInactiveProps.className,
488
+ ]
489
+ .filter(Boolean)
490
+ .join(' ') || undefined,
491
+ ...(disabled
492
+ ? {
493
+ role: 'link',
494
+ 'aria-disabled': true,
495
+ }
496
+ : undefined),
497
+ ['data-status']: isActive ? 'active' : undefined,
498
+ }
499
+ }
500
+
501
+ export interface LinkComponent<TProps extends Record<string, any> = {}> {
502
+ <
503
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
504
+ TFrom extends RoutePaths<TRouteTree> = '/',
505
+ TTo extends string = '',
506
+ TMaskFrom extends RoutePaths<TRouteTree> = '/',
507
+ TMaskTo extends string = '',
508
+ >(
509
+ props: MakeLinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
510
+ TProps &
511
+ React.RefAttributes<HTMLAnchorElement>,
512
+ ): ReactNode
513
+ }
514
+
515
+ export const Link: LinkComponent = React.forwardRef((props: any, ref) => {
516
+ const linkProps = useLinkProps(props)
517
+
518
+ return (
519
+ <a
520
+ {...{
521
+ ref: ref as any,
522
+ ...linkProps,
523
+ children:
524
+ typeof props.children === 'function'
525
+ ? props.children({
526
+ isActive: (linkProps as any)['data-status'] === 'active',
527
+ })
528
+ : props.children,
529
+ }}
530
+ />
531
+ )
532
+ }) as any
533
+
534
+ export function Navigate<
535
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
536
+ TFrom extends RoutePaths<TRouteTree> = '/',
537
+ TTo extends string = '',
538
+ TMaskFrom extends RoutePaths<TRouteTree> = '/',
539
+ TMaskTo extends string = '',
540
+ >(props: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>): null {
541
+ const router = useRouter()
542
+
543
+ useLayoutEffect(() => {
544
+ router.navigate(props as any)
545
+ }, [])
546
+
547
+ return null
548
+ }
549
+
550
+ export const matchIdsContext = React.createContext<string[]>(null!)
551
+ export const routerContext = React.createContext<RegisteredRouter>(null!)
552
+
553
+ export type RouterProps<
554
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
555
+ TDehydrated extends Record<string, any> = Record<string, any>,
556
+ > = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> & {
557
+ router: Router<TRouteTree>
558
+ context?: Partial<RouterOptions<TRouteTree, TDehydrated>['context']>
559
+ }
560
+
561
+ export function useRouterState<TSelected = RegisteredRouter['state']>(opts?: {
562
+ select: (state: RegisteredRouter['state']) => TSelected
563
+ }): TSelected {
564
+ const router = useRouter()
565
+ return useStore(router.__store, opts?.select as any)
566
+ }
567
+
568
+ export function RouterProvider<
569
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
570
+ TDehydrated extends Record<string, any> = Record<string, any>,
571
+ >({ router, ...rest }: RouterProps<TRouteTree, TDehydrated>) {
572
+ router.update(rest)
573
+
574
+ React.useEffect(() => {
575
+ let unsub
576
+
577
+ React.startTransition(() => {
578
+ unsub = router.mount()
579
+ })
580
+
581
+ return unsub
582
+ }, [router])
583
+
584
+ const Wrap = router.options.Wrap || React.Fragment
585
+
586
+ return (
587
+ <Wrap>
588
+ <routerContext.Provider value={router as any}>
589
+ <Matches />
590
+ </routerContext.Provider>
591
+ </Wrap>
592
+ )
593
+ }
594
+
595
+ function Matches() {
596
+ const router = useRouter()
597
+
598
+ const matchIds = useRouterState({
599
+ select: (state) => {
600
+ return state.renderedMatchIds
601
+ },
602
+ })
603
+
604
+ const locationKey = useRouterState({
605
+ select: (d) => d.resolvedLocation.state?.key,
606
+ })
607
+
608
+ const route = router.getRoute(rootRouteId)
609
+
610
+ const errorComponent = React.useCallback(
611
+ (props: any) => {
612
+ return React.createElement(ErrorComponent, {
613
+ ...props,
614
+ useMatch: route.useMatch,
615
+ useRouteContext: route.useRouteContext,
616
+ useSearch: route.useSearch,
617
+ useParams: route.useParams,
618
+ })
619
+ },
620
+ [route],
621
+ )
622
+
623
+ return (
624
+ <matchIdsContext.Provider value={[undefined!, ...matchIds]}>
625
+ <CatchBoundary
626
+ resetKey={locationKey}
627
+ errorComponent={errorComponent}
628
+ onCatch={() => {
629
+ warning(
630
+ false,
631
+ `Error in router! Consider setting an 'errorComponent' in your RootRoute! 👍`,
632
+ )
633
+ }}
634
+ >
635
+ <Outlet />
636
+ </CatchBoundary>
637
+ </matchIdsContext.Provider>
638
+ )
639
+ }
640
+
641
+ export function useRouter(): RegisteredRouter {
642
+ const value = React.useContext(routerContext)
643
+ warning(value, 'useRouter must be used inside a <Router> component!')
644
+ return value
645
+ }
646
+
647
+ export function useMatches<T = RouteMatch[]>(opts?: {
648
+ select?: (matches: RouteMatch[]) => T
649
+ }): T {
650
+ const matchIds = React.useContext(matchIdsContext)
651
+
652
+ return useRouterState({
653
+ select: (state) => {
654
+ const matches = state.renderedMatches.slice(
655
+ state.renderedMatches.findIndex((d) => d.id === matchIds[0]),
656
+ )
657
+ return opts?.select ? opts.select(matches) : (matches as T)
658
+ },
659
+ })
660
+ }
661
+
662
+ type StrictOrFrom<TFrom> =
663
+ | {
664
+ from: TFrom
665
+ strict?: true
666
+ }
667
+ | {
668
+ from?: never
669
+ strict: false
670
+ }
671
+
672
+ export function useMatch<
673
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
674
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
675
+ TStrict extends boolean = true,
676
+ TRouteMatchState = RouteMatch<TRouteTree, TFrom>,
677
+ TSelected = TRouteMatchState,
678
+ >(
679
+ opts: StrictOrFrom<TFrom> & {
680
+ select?: (match: TRouteMatchState) => TSelected
681
+ },
682
+ ): TStrict extends true ? TRouteMatchState : TRouteMatchState | undefined {
683
+ const router = useRouter()
684
+ const nearestMatchId = React.useContext(matchIdsContext)[0]!
685
+ const nearestMatchRouteId = router.getRouteMatch(nearestMatchId)?.routeId
686
+
687
+ const matchRouteId = useRouterState({
688
+ select: (state) => {
689
+ const match = opts?.from
690
+ ? state.renderedMatches.find((d) => d.routeId === opts?.from)
691
+ : state.renderedMatches.find((d) => d.id === nearestMatchId)
692
+
693
+ return match!.routeId
694
+ },
695
+ })
696
+
697
+ if (opts?.strict ?? true) {
698
+ invariant(
699
+ nearestMatchRouteId == matchRouteId,
700
+ `useMatch("${
701
+ matchRouteId as string
702
+ }") is being called in a component that is meant to render the '${nearestMatchRouteId}' route. Did you mean to 'useMatch("${
703
+ matchRouteId as string
704
+ }", { strict: false })' or 'useRoute("${
705
+ matchRouteId as string
706
+ }")' instead?`,
707
+ )
708
+ }
709
+
710
+ const matchSelection = useRouterState({
711
+ select: (state) => {
712
+ const match = opts?.from
713
+ ? state.renderedMatches.find((d) => d.routeId === opts?.from)
714
+ : state.renderedMatches.find((d) => d.id === nearestMatchId)
715
+
716
+ invariant(
717
+ match,
718
+ `Could not find ${
719
+ opts?.from
720
+ ? `an active match from "${opts.from}"`
721
+ : 'a nearest match!'
722
+ }`,
723
+ )
724
+
725
+ return opts?.select ? opts.select(match as any) : match
726
+ },
727
+ })
728
+
729
+ return matchSelection as any
730
+ }
731
+
732
+ export type RouteFromIdOrRoute<
733
+ T,
734
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
735
+ > = T extends ParseRoute<TRouteTree>
736
+ ? T
737
+ : T extends RouteIds<TRouteTree>
738
+ ? RoutesById<TRouteTree>[T]
739
+ : T extends string
740
+ ? RouteIds<TRouteTree>
741
+ : never
742
+
743
+ export function useLoader<
744
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
745
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
746
+ TStrict extends boolean = true,
747
+ TLoader = RouteById<TRouteTree, TFrom>['types']['loader'],
748
+ TSelected = TLoader,
749
+ >(
750
+ opts: StrictOrFrom<TFrom> & {
751
+ select?: (search: TLoader) => TSelected
752
+ },
753
+ ): TStrict extends true ? TSelected : TSelected | undefined {
754
+ return useMatch({
755
+ ...(opts as any),
756
+ select: (match: RouteMatch) =>
757
+ opts?.select
758
+ ? opts?.select(match.loaderData as TLoader)
759
+ : match.loaderData,
760
+ })
761
+ }
762
+
763
+ export function useRouterContext<
764
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
765
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
766
+ TStrict extends boolean = true,
767
+ TContext = RouteById<TRouteTree, TFrom>['types']['context'],
768
+ TSelected = TContext,
769
+ >(
770
+ opts: StrictOrFrom<TFrom> & {
771
+ select?: (search: TContext) => TSelected
772
+ },
773
+ ): TStrict extends true ? TSelected : TSelected | undefined {
774
+ return useMatch({
775
+ ...(opts as any),
776
+ select: (match: RouteMatch) =>
777
+ opts?.select ? opts.select(match.context as TContext) : match.context,
778
+ })
779
+ }
780
+
781
+ export function useRouteContext<
782
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
783
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
784
+ TStrict extends boolean = true,
785
+ TRouteContext = RouteById<TRouteTree, TFrom>['types']['context'],
786
+ TSelected = TRouteContext,
787
+ >(
788
+ opts: StrictOrFrom<TFrom> & {
789
+ select?: (search: TRouteContext) => TSelected
790
+ },
791
+ ): TStrict extends true ? TSelected : TSelected | undefined {
792
+ return useMatch({
793
+ ...(opts as any),
794
+ select: (match: RouteMatch) =>
795
+ opts?.select
796
+ ? opts.select(match.context as TRouteContext)
797
+ : match.context,
798
+ })
799
+ }
800
+
801
+ export function useSearch<
802
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
803
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
804
+ TStrict extends boolean = true,
805
+ TSearch = RouteById<TRouteTree, TFrom>['types']['fullSearchSchema'],
806
+ TSelected = TSearch,
807
+ >(
808
+ opts: StrictOrFrom<TFrom> & {
809
+ select?: (search: TSearch) => TSelected
810
+ },
811
+ ): TStrict extends true ? TSelected : TSelected | undefined {
812
+ return useMatch({
813
+ ...(opts as any),
814
+ select: (match: RouteMatch) => {
815
+ return opts?.select ? opts.select(match.search as TSearch) : match.search
816
+ },
817
+ })
818
+ }
819
+
820
+ export function useParams<
821
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
822
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
823
+ TDefaultSelected = AllParams<TRouteTree> &
824
+ RouteById<TRouteTree, 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.renderedMatches) as any)?.params
834
+ return opts?.select ? opts.select(params) : params
835
+ },
836
+ })
837
+ }
838
+
839
+ export function useNavigate<
840
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
841
+ TDefaultFrom extends RoutePaths<TRouteTree> = '/',
842
+ >(defaultOpts?: { from?: TDefaultFrom }) {
843
+ const router = useRouter()
844
+ return React.useCallback(
845
+ <
846
+ TFrom extends RoutePaths<TRouteTree> = TDefaultFrom,
847
+ TTo extends string = '',
848
+ TMaskFrom extends RoutePaths<TRouteTree> = '/',
849
+ TMaskTo extends string = '',
850
+ >(
851
+ opts?: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
852
+ ) => {
853
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
854
+ },
855
+ [],
856
+ )
857
+ }
858
+
859
+ export function useMatchRoute<
860
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
861
+ >() {
862
+ const router = useRouter()
863
+
864
+ return React.useCallback(
865
+ <
866
+ TFrom extends RoutePaths<TRouteTree> = '/',
867
+ TTo extends string = '',
868
+ TMaskFrom extends RoutePaths<TRouteTree> = '/',
869
+ TMaskTo extends string = '',
870
+ TResolved extends string = ResolveRelativePath<TFrom, NoInfer<TTo>>,
871
+ >(
872
+ opts: MakeUseMatchRouteOptions<
873
+ TRouteTree,
874
+ TFrom,
875
+ TTo,
876
+ TMaskFrom,
877
+ TMaskTo
878
+ >,
879
+ ): false | RouteById<TRouteTree, TResolved>['types']['allParams'] => {
880
+ const { pending, caseSensitive, ...rest } = opts
881
+
882
+ return router.matchRoute(rest as any, {
883
+ pending,
884
+ caseSensitive,
885
+ })
886
+ },
887
+ [],
888
+ )
889
+ }
890
+
891
+ export function MatchRoute<
892
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
893
+ TFrom extends RoutePaths<TRouteTree> = '/',
894
+ TTo extends string = '',
895
+ TMaskFrom extends RoutePaths<TRouteTree> = '/',
896
+ TMaskTo extends string = '',
897
+ >(
898
+ props: MakeMatchRouteOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
899
+ ): any {
900
+ const matchRoute = useMatchRoute()
901
+ const params = matchRoute(props as any)
902
+
903
+ if (typeof props.children === 'function') {
904
+ return (props.children as any)(params)
905
+ }
906
+
907
+ return !!params ? props.children : null
908
+ }
909
+
910
+ export function Outlet() {
911
+ const matchIds = React.useContext(matchIdsContext).slice(1)
912
+
913
+ if (!matchIds[0]) {
914
+ return null
915
+ }
916
+
917
+ return <Match matchIds={matchIds} />
918
+ }
919
+
920
+ const defaultPending = () => null
921
+
922
+ function Match({ matchIds }: { matchIds: string[] }) {
923
+ const router = useRouter()
924
+ const matchId = matchIds[0]!
925
+ const routeId = router.getRouteMatch(matchId)!.routeId
926
+ const route = router.getRoute(routeId)
927
+ const locationKey = useRouterState({
928
+ select: (s) => s.resolvedLocation.state?.key,
929
+ })
930
+
931
+ const PendingComponent = (route.options.pendingComponent ??
932
+ router.options.defaultPendingComponent ??
933
+ defaultPending) as any
934
+
935
+ const routeErrorComponent =
936
+ route.options.errorComponent ??
937
+ router.options.defaultErrorComponent ??
938
+ ErrorComponent
939
+
940
+ const ResolvedSuspenseBoundary =
941
+ route.options.wrapInSuspense ?? !route.isRoot
942
+ ? React.Suspense
943
+ : SafeFragment
944
+
945
+ const ResolvedCatchBoundary = !!routeErrorComponent
946
+ ? CatchBoundary
947
+ : SafeFragment
948
+
949
+ const errorComponent = React.useCallback(
950
+ (props: any) => {
951
+ return React.createElement(routeErrorComponent, {
952
+ ...props,
953
+ useMatch: route.useMatch,
954
+ useRouteContext: route.useRouteContext,
955
+ useSearch: route.useSearch,
956
+ useParams: route.useParams,
957
+ })
958
+ },
959
+ [route],
960
+ )
961
+
962
+ return (
963
+ <matchIdsContext.Provider value={matchIds}>
964
+ <ResolvedSuspenseBoundary
965
+ fallback={React.createElement(PendingComponent, {
966
+ useMatch: route.useMatch,
967
+ useRouteContext: route.useRouteContext,
968
+ useSearch: route.useSearch,
969
+ useParams: route.useParams,
970
+ })}
971
+ >
972
+ <ResolvedCatchBoundary
973
+ resetKey={locationKey}
974
+ errorComponent={errorComponent}
975
+ onCatch={() => {
976
+ warning(false, `Error in route match: ${matchId}`)
977
+ }}
978
+ >
979
+ <MatchInner matchId={matchId} PendingComponent={PendingComponent} />
980
+ </ResolvedCatchBoundary>
981
+ </ResolvedSuspenseBoundary>
982
+ </matchIdsContext.Provider>
983
+ )
984
+ }
985
+
986
+ function MatchInner({
987
+ matchId,
988
+ PendingComponent,
989
+ }: {
990
+ matchId: string
991
+ PendingComponent: any
992
+ }): any {
993
+ const router = useRouter()
994
+
995
+ const match = useRouterState({
996
+ select: (d) => {
997
+ const match = d.matchesById[matchId]
998
+ return pick(match!, ['status', 'loadPromise', 'routeId', 'error'])
999
+ },
1000
+ })
1001
+
1002
+ const route = router.getRoute(match.routeId)
1003
+
1004
+ if (match.status === 'error') {
1005
+ throw match.error
1006
+ }
1007
+
1008
+ if (match.status === 'pending') {
1009
+ return React.createElement(PendingComponent, {
1010
+ useLoader: route.useLoader,
1011
+ useMatch: route.useMatch,
1012
+ useRouteContext: route.useRouteContext,
1013
+ useSearch: route.useSearch,
1014
+ useParams: route.useParams,
1015
+ })
1016
+ }
1017
+
1018
+ if (match.status === 'success') {
1019
+ let comp = route.options.component ?? router.options.defaultComponent
1020
+
1021
+ if (comp) {
1022
+ return React.createElement(comp, {
1023
+ useLoader: route.useLoader,
1024
+ useMatch: route.useMatch,
1025
+ useRouteContext: route.useRouteContext as any,
1026
+ useSearch: route.useSearch,
1027
+ useParams: route.useParams as any,
1028
+ } as any)
1029
+ }
1030
+
1031
+ return <Outlet />
1032
+ }
1033
+
1034
+ invariant(
1035
+ false,
1036
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
1037
+ )
1038
+ }
1039
+
1040
+ function SafeFragment(props: any) {
1041
+ return <>{props.children}</>
1042
+ }
1043
+
1044
+ export function useInjectHtml() {
1045
+ const router = useRouter()
1046
+
1047
+ return React.useCallback(
1048
+ (html: string | (() => Promise<string> | string)) => {
1049
+ router.injectHtml(html)
1050
+ },
1051
+ [],
1052
+ )
1053
+ }
1054
+
1055
+ export function useDehydrate() {
1056
+ const router = useRouter()
1057
+
1058
+ return React.useCallback(function dehydrate<T>(
1059
+ key: any,
1060
+ data: T | (() => Promise<T> | T),
1061
+ ) {
1062
+ return router.dehydrateData(key, data)
1063
+ },
1064
+ [])
1065
+ }
1066
+
1067
+ export function useHydrate() {
1068
+ const router = useRouter()
1069
+
1070
+ return function hydrate<T = unknown>(key: any) {
1071
+ return router.hydrateData(key) as T
1072
+ }
1073
+ }
1074
+
1075
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
1076
+ // there has to be a better way to reset error boundaries when the
1077
+ // router's location key changes.
1078
+
1079
+ export function CatchBoundary(props: {
1080
+ resetKey: string
1081
+ children: any
1082
+ errorComponent?: any
1083
+ onCatch: (error: any) => void
1084
+ }) {
1085
+ const errorComponent = props.errorComponent ?? ErrorComponent
1086
+
1087
+ return (
1088
+ <CatchBoundaryImpl
1089
+ resetKey={props.resetKey}
1090
+ onCatch={props.onCatch}
1091
+ children={({ error }) => {
1092
+ if (error) {
1093
+ return React.createElement(errorComponent, {
1094
+ error,
1095
+ })
1096
+ }
1097
+
1098
+ return props.children
1099
+ }}
1100
+ />
1101
+ )
1102
+ }
1103
+
1104
+ export class CatchBoundaryImpl extends React.Component<{
1105
+ resetKey: string
1106
+ children: (props: { error: any; reset: () => void }) => any
1107
+ onCatch?: (error: any) => void
1108
+ }> {
1109
+ state = { error: null } as any
1110
+ static getDerivedStateFromError(error: any) {
1111
+ return { error }
1112
+ }
1113
+ componentDidUpdate(
1114
+ prevProps: Readonly<{
1115
+ resetKey: string
1116
+ children: (props: { error: any; reset: () => void }) => any
1117
+ onCatch?: ((error: any, info: any) => void) | undefined
1118
+ }>,
1119
+ prevState: any,
1120
+ ): void {
1121
+ if (prevState.error && prevProps.resetKey !== this.props.resetKey) {
1122
+ this.setState({ error: null })
1123
+ }
1124
+ }
1125
+ componentDidCatch(error: any) {
1126
+ this.props.onCatch?.(error)
1127
+ }
1128
+ render() {
1129
+ return this.props.children(this.state)
1130
+ }
1131
+ }
1132
+
1133
+ export function ErrorComponent({ error }: { error: any }) {
1134
+ const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
1135
+
1136
+ return (
1137
+ <div style={{ padding: '.5rem', maxWidth: '100%' }}>
1138
+ <div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
1139
+ <strong style={{ fontSize: '1rem' }}>Something went wrong!</strong>
1140
+ <button
1141
+ style={{
1142
+ appearance: 'none',
1143
+ fontSize: '.6em',
1144
+ border: '1px solid currentColor',
1145
+ padding: '.1rem .2rem',
1146
+ fontWeight: 'bold',
1147
+ borderRadius: '.25rem',
1148
+ }}
1149
+ onClick={() => setShow((d) => !d)}
1150
+ >
1151
+ {show ? 'Hide Error' : 'Show Error'}
1152
+ </button>
1153
+ </div>
1154
+ <div style={{ height: '.25rem' }} />
1155
+ {show ? (
1156
+ <div>
1157
+ <pre
1158
+ style={{
1159
+ fontSize: '.7em',
1160
+ border: '1px solid red',
1161
+ borderRadius: '.25rem',
1162
+ padding: '.3rem',
1163
+ color: 'red',
1164
+ overflow: 'auto',
1165
+ }}
1166
+ >
1167
+ {error.message ? <code>{error.message}</code> : null}
1168
+ </pre>
1169
+ </div>
1170
+ ) : null}
1171
+ </div>
1172
+ )
1173
+ }
1174
+
1175
+ export function useBlocker(
1176
+ message: string,
1177
+ condition: boolean | any = true,
1178
+ ): void {
1179
+ const router = useRouter()
1180
+
1181
+ React.useEffect(() => {
1182
+ if (!condition) return
1183
+
1184
+ let unblock = router.history.block((retry, cancel) => {
1185
+ if (window.confirm(message)) {
1186
+ unblock()
1187
+ retry()
1188
+ }
1189
+ })
1190
+
1191
+ return unblock
1192
+ })
1193
+ }
1194
+
1195
+ export function Block({ message, condition, children }: PromptProps) {
1196
+ useBlocker(message, condition)
1197
+ return (children ?? null) as ReactNode
1198
+ }
1199
+
1200
+ export function shallow<T>(objA: T, objB: T) {
1201
+ if (Object.is(objA, objB)) {
1202
+ return true
1203
+ }
1204
+
1205
+ if (
1206
+ typeof objA !== 'object' ||
1207
+ objA === null ||
1208
+ typeof objB !== 'object' ||
1209
+ objB === null
1210
+ ) {
1211
+ return false
1212
+ }
1213
+
1214
+ const keysA = Object.keys(objA)
1215
+ if (keysA.length !== Object.keys(objB).length) {
1216
+ return false
1217
+ }
1218
+
1219
+ for (let i = 0; i < keysA.length; i++) {
1220
+ if (
1221
+ !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
1222
+ !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
1223
+ ) {
1224
+ return false
1225
+ }
1226
+ }
1227
+ return true
1228
+ }