@tanstack/react-router 0.0.1-beta.167 → 0.0.1-beta.168

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