@tanstack/react-router 0.0.1-beta.21 → 0.0.1-beta.210

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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/build/cjs/CatchBoundary.js +125 -0
  3. package/build/cjs/CatchBoundary.js.map +1 -0
  4. package/build/cjs/Matches.js +223 -0
  5. package/build/cjs/Matches.js.map +1 -0
  6. package/build/cjs/RouterProvider.js +1021 -0
  7. package/build/cjs/RouterProvider.js.map +1 -0
  8. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +1 -19
  9. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
  10. package/build/cjs/fileRoute.js +29 -0
  11. package/build/cjs/fileRoute.js.map +1 -0
  12. package/build/cjs/index.js +124 -0
  13. package/build/cjs/index.js.map +1 -0
  14. package/build/cjs/lazyRouteComponent.js +57 -0
  15. package/build/cjs/lazyRouteComponent.js.map +1 -0
  16. package/build/cjs/link.js +150 -0
  17. package/build/cjs/link.js.map +1 -0
  18. package/build/cjs/path.js +211 -0
  19. package/build/cjs/path.js.map +1 -0
  20. package/build/cjs/qss.js +65 -0
  21. package/build/cjs/qss.js.map +1 -0
  22. package/build/cjs/redirects.js +27 -0
  23. package/build/cjs/redirects.js.map +1 -0
  24. package/build/cjs/route.js +133 -0
  25. package/build/cjs/route.js.map +1 -0
  26. package/build/cjs/router.js +203 -0
  27. package/build/cjs/router.js.map +1 -0
  28. package/build/cjs/searchParams.js +83 -0
  29. package/build/cjs/searchParams.js.map +1 -0
  30. package/build/cjs/useBlocker.js +64 -0
  31. package/build/cjs/useBlocker.js.map +1 -0
  32. package/build/cjs/useNavigate.js +78 -0
  33. package/build/cjs/useNavigate.js.map +1 -0
  34. package/build/cjs/useParams.js +28 -0
  35. package/build/cjs/useParams.js.map +1 -0
  36. package/build/cjs/useSearch.js +27 -0
  37. package/build/cjs/useSearch.js.map +1 -0
  38. package/build/cjs/utils.js +225 -0
  39. package/build/cjs/utils.js.map +1 -0
  40. package/build/esm/index.js +1895 -2565
  41. package/build/esm/index.js.map +1 -1
  42. package/build/stats-html.html +59 -49
  43. package/build/stats-react.json +815 -43
  44. package/build/types/CatchBoundary.d.ts +33 -0
  45. package/build/types/Matches.d.ts +31 -0
  46. package/build/types/RouterProvider.d.ts +78 -0
  47. package/build/types/awaited.d.ts +0 -0
  48. package/build/types/defer.d.ts +0 -0
  49. package/build/types/fileRoute.d.ts +32 -0
  50. package/build/types/history.d.ts +7 -0
  51. package/build/types/index.d.ts +24 -104
  52. package/build/types/injectHtml.d.ts +0 -0
  53. package/build/types/lazyRouteComponent.d.ts +2 -0
  54. package/build/types/link.d.ts +105 -0
  55. package/build/types/location.d.ts +14 -0
  56. package/build/types/path.d.ts +16 -0
  57. package/build/types/qss.d.ts +2 -0
  58. package/build/types/redirects.d.ts +10 -0
  59. package/build/types/route.d.ts +261 -0
  60. package/build/types/routeInfo.d.ts +22 -0
  61. package/build/types/router.d.ts +117 -0
  62. package/build/types/scroll-restoration.d.ts +0 -0
  63. package/build/types/searchParams.d.ts +7 -0
  64. package/build/types/useBlocker.d.ts +8 -0
  65. package/build/types/useNavigate.d.ts +20 -0
  66. package/build/types/useParams.d.ts +7 -0
  67. package/build/types/useSearch.d.ts +7 -0
  68. package/build/types/utils.d.ts +65 -0
  69. package/build/umd/index.development.js +2132 -2469
  70. package/build/umd/index.development.js.map +1 -1
  71. package/build/umd/index.production.js +4 -4
  72. package/build/umd/index.production.js.map +1 -1
  73. package/package.json +9 -10
  74. package/src/CatchBoundary.tsx +97 -0
  75. package/src/Matches.tsx +315 -0
  76. package/src/RouterProvider.tsx +1457 -0
  77. package/src/awaited.tsx +40 -0
  78. package/src/defer.ts +55 -0
  79. package/src/fileRoute.ts +145 -0
  80. package/src/history.ts +8 -0
  81. package/src/index.tsx +28 -693
  82. package/src/injectHtml.ts +28 -0
  83. package/src/lazyRouteComponent.tsx +33 -0
  84. package/src/link.tsx +507 -0
  85. package/src/location.ts +15 -0
  86. package/src/path.ts +256 -0
  87. package/src/qss.ts +53 -0
  88. package/src/redirects.ts +31 -0
  89. package/src/route.ts +786 -0
  90. package/src/routeInfo.ts +68 -0
  91. package/src/router.ts +374 -0
  92. package/src/scroll-restoration.tsx +205 -0
  93. package/src/searchParams.ts +79 -0
  94. package/src/useBlocker.tsx +34 -0
  95. package/src/useNavigate.tsx +109 -0
  96. package/src/useParams.tsx +25 -0
  97. package/src/useSearch.tsx +25 -0
  98. package/src/utils.ts +337 -0
  99. package/build/cjs/react-router/src/index.js +0 -466
  100. package/build/cjs/react-router/src/index.js.map +0 -1
  101. package/build/cjs/router-core/build/esm/index.js +0 -2523
  102. package/build/cjs/router-core/build/esm/index.js.map +0 -1
@@ -0,0 +1,1457 @@
1
+ import {
2
+ HistoryLocation,
3
+ HistoryState,
4
+ RouterHistory,
5
+ createBrowserHistory,
6
+ } from '@tanstack/history'
7
+ import * as React from 'react'
8
+ import invariant from 'tiny-invariant'
9
+ import warning from 'tiny-warning'
10
+ import { Matches } from './Matches'
11
+ import {
12
+ LinkInfo,
13
+ LinkOptions,
14
+ NavigateOptions,
15
+ ResolveRelativePath,
16
+ ToOptions,
17
+ } from './link'
18
+ import { ParsedLocation } from './location'
19
+ import {
20
+ cleanPath,
21
+ interpolatePath,
22
+ joinPaths,
23
+ matchPathname,
24
+ parsePathname,
25
+ resolvePath,
26
+ trimPath,
27
+ trimPathRight,
28
+ } from './path'
29
+ import { isRedirect } from './redirects'
30
+ import { AnyPathParams, AnyRoute, AnySearchSchema, Route } from './route'
31
+ import {
32
+ FullSearchSchema,
33
+ ParseRoute,
34
+ RouteById,
35
+ RouteIds,
36
+ RoutePaths,
37
+ RoutesById,
38
+ RoutesByPath,
39
+ } from './routeInfo'
40
+ import {
41
+ BuildNextOptions,
42
+ DehydratedRouteMatch,
43
+ RegisteredRouter,
44
+ Router,
45
+ RouterOptions,
46
+ RouterState,
47
+ componentTypes,
48
+ } from './router'
49
+ import {
50
+ NoInfer,
51
+ PickAsRequired,
52
+ functionalUpdate,
53
+ last,
54
+ partialDeepEqual,
55
+ pick,
56
+ replaceEqualDeep,
57
+ useStableCallback,
58
+ } from './utils'
59
+ import { MatchRouteOptions } from './Matches'
60
+
61
+ export interface CommitLocationOptions {
62
+ replace?: boolean
63
+ resetScroll?: boolean
64
+ startTransition?: boolean
65
+ }
66
+
67
+ export interface MatchLocation {
68
+ to?: string | number | null
69
+ fuzzy?: boolean
70
+ caseSensitive?: boolean
71
+ from?: string
72
+ }
73
+
74
+ type LinkCurrentTargetElement = {
75
+ preloadTimeout?: null | ReturnType<typeof setTimeout>
76
+ }
77
+
78
+ export type BuildLinkFn<TRouteTree extends AnyRoute> = <
79
+ TFrom extends RoutePaths<TRouteTree> = '/',
80
+ TTo extends string = '',
81
+ >(
82
+ dest: LinkOptions<TRouteTree, TFrom, TTo>,
83
+ ) => LinkInfo
84
+
85
+ export type NavigateFn<TRouteTree extends AnyRoute> = <
86
+ TRouteTree extends AnyRoute,
87
+ TFrom extends RoutePaths<TRouteTree> = '/',
88
+ TTo extends string = '',
89
+ TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
90
+ TMaskTo extends string = '',
91
+ >({
92
+ from,
93
+ to = '' as any,
94
+ ...rest
95
+ }: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) => Promise<void>
96
+
97
+ export type MatchRouteFn<TRouteTree extends AnyRoute> = <
98
+ TFrom extends RoutePaths<TRouteTree> = '/',
99
+ TTo extends string = '',
100
+ TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
101
+ >(
102
+ location: ToOptions<TRouteTree, TFrom, TTo>,
103
+ opts?: MatchRouteOptions,
104
+ ) => false | RouteById<TRouteTree, TResolved>['types']['allParams']
105
+
106
+ export type LoadFn = (opts?: {
107
+ next?: ParsedLocation
108
+ throwOnError?: boolean
109
+ __dehydratedMatches?: DehydratedRouteMatch[]
110
+ }) => Promise<void>
111
+
112
+ export type BuildLocationFn<TRouteTree extends AnyRoute> = (
113
+ opts: BuildNextOptions,
114
+ ) => ParsedLocation
115
+
116
+ export type RouterContext<
117
+ TRouteTree extends AnyRoute,
118
+ // TDehydrated extends Record<string, any>,
119
+ > = {
120
+ buildLink: BuildLinkFn<TRouteTree>
121
+ state: RouterState<TRouteTree>
122
+ navigate: NavigateFn<TRouteTree>
123
+ matchRoute: MatchRouteFn<TRouteTree>
124
+ routeTree: TRouteTree
125
+ routesById: RoutesById<TRouteTree>
126
+ options: RouterOptions<TRouteTree>
127
+ history: RouterHistory
128
+ load: LoadFn
129
+ buildLocation: BuildLocationFn<TRouteTree>
130
+ }
131
+
132
+ export const routerContext = React.createContext<RouterContext<any>>(null!)
133
+
134
+ if (typeof document !== 'undefined') {
135
+ window.__TSR_ROUTER_CONTEXT__ = routerContext as any
136
+ }
137
+
138
+ const preloadWarning = 'Error preloading route! ☝️'
139
+
140
+ function isCtrlEvent(e: MouseEvent) {
141
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
142
+ }
143
+
144
+ export class SearchParamError extends Error {}
145
+
146
+ export class PathParamError extends Error {}
147
+
148
+ export function getInitialRouterState(
149
+ location: ParsedLocation,
150
+ ): RouterState<any> {
151
+ return {
152
+ status: 'idle',
153
+ resolvedLocation: undefined,
154
+ location: location!,
155
+ matches: [],
156
+ pendingMatches: [],
157
+ lastUpdated: Date.now(),
158
+ }
159
+ }
160
+
161
+ export function RouterProvider<
162
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
163
+ TDehydrated extends Record<string, any> = Record<string, any>,
164
+ >({ router, ...rest }: RouterProps<TRouteTree, TDehydrated>) {
165
+ const options = {
166
+ ...router.options,
167
+ ...rest,
168
+ context: {
169
+ ...router.options.context,
170
+ ...rest?.context,
171
+ },
172
+ } as PickAsRequired<
173
+ RouterOptions<TRouteTree, TDehydrated>,
174
+ 'stringifySearch' | 'parseSearch' | 'context'
175
+ >
176
+
177
+ const history = React.useState(
178
+ () => options.history ?? createBrowserHistory(),
179
+ )[0]
180
+
181
+ const tempLocationKeyRef = React.useRef<string | undefined>(
182
+ `${Math.round(Math.random() * 10000000)}`,
183
+ )
184
+ const resetNextScrollRef = React.useRef<boolean>(false)
185
+
186
+ const navigateTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)
187
+
188
+ const latestLoadPromiseRef = React.useRef<Promise<void>>(Promise.resolve())
189
+
190
+ const checkLatest = (promise: Promise<void>): undefined | Promise<void> => {
191
+ return latestLoadPromiseRef.current !== promise
192
+ ? latestLoadPromiseRef.current
193
+ : undefined
194
+ }
195
+
196
+ const parseLocation = useStableCallback(
197
+ (
198
+ previousLocation?: ParsedLocation,
199
+ ): ParsedLocation<FullSearchSchema<TRouteTree>> => {
200
+ const parse = ({
201
+ pathname,
202
+ search,
203
+ hash,
204
+ state,
205
+ }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
206
+ const parsedSearch = options.parseSearch(search)
207
+
208
+ return {
209
+ pathname: pathname,
210
+ searchStr: search,
211
+ search: replaceEqualDeep(
212
+ previousLocation?.search,
213
+ parsedSearch,
214
+ ) as any,
215
+ hash: hash.split('#').reverse()[0] ?? '',
216
+ href: `${pathname}${search}${hash}`,
217
+ state: replaceEqualDeep(
218
+ previousLocation?.state,
219
+ state,
220
+ ) as HistoryState,
221
+ }
222
+ }
223
+
224
+ const location = parse(history.location)
225
+
226
+ let { __tempLocation, __tempKey } = location.state
227
+
228
+ if (
229
+ __tempLocation &&
230
+ (!__tempKey || __tempKey === tempLocationKeyRef.current)
231
+ ) {
232
+ // Sync up the location keys
233
+ const parsedTempLocation = parse(__tempLocation) as any
234
+ parsedTempLocation.state.key = location.state.key
235
+
236
+ delete parsedTempLocation.state.__tempLocation
237
+
238
+ return {
239
+ ...parsedTempLocation,
240
+ maskedLocation: location,
241
+ }
242
+ }
243
+
244
+ return location
245
+ },
246
+ )
247
+
248
+ const [preState, setState] = React.useState<RouterState<TRouteTree>>(() =>
249
+ getInitialRouterState(parseLocation()),
250
+ )
251
+ const [isTransitioning, startReactTransition] = React.useTransition()
252
+
253
+ const state = React.useMemo<RouterState<TRouteTree>>(
254
+ () => ({
255
+ ...preState,
256
+ status: isTransitioning ? 'pending' : 'idle',
257
+ }),
258
+ [preState, isTransitioning],
259
+ )
260
+
261
+ React.useLayoutEffect(() => {
262
+ if (!isTransitioning && state.resolvedLocation !== state.location) {
263
+ setState((s) => ({
264
+ ...s,
265
+ resolvedLocation: s.location,
266
+ }))
267
+ }
268
+ })
269
+
270
+ const basepath = `/${trimPath(options.basepath ?? '') ?? ''}`
271
+
272
+ const resolvePathWithBase = useStableCallback(
273
+ (from: string, path: string) => {
274
+ return resolvePath(basepath!, from, cleanPath(path))
275
+ },
276
+ )
277
+
278
+ const [routesById, routesByPath] = React.useMemo(() => {
279
+ const routesById = {} as RoutesById<TRouteTree>
280
+ const routesByPath = {} as RoutesByPath<TRouteTree>
281
+
282
+ const recurseRoutes = (routes: AnyRoute[]) => {
283
+ routes.forEach((route, i) => {
284
+ route.init({ originalIndex: i })
285
+
286
+ const existingRoute = (routesById as any)[route.id]
287
+
288
+ invariant(
289
+ !existingRoute,
290
+ `Duplicate routes found with id: ${String(route.id)}`,
291
+ )
292
+ ;(routesById as any)[route.id] = route
293
+
294
+ if (!route.isRoot && route.path) {
295
+ const trimmedFullPath = trimPathRight(route.fullPath)
296
+ if (
297
+ !(routesByPath as any)[trimmedFullPath] ||
298
+ route.fullPath.endsWith('/')
299
+ ) {
300
+ ;(routesByPath as any)[trimmedFullPath] = route
301
+ }
302
+ }
303
+
304
+ const children = route.children as Route[]
305
+
306
+ if (children?.length) {
307
+ recurseRoutes(children)
308
+ }
309
+ })
310
+ }
311
+
312
+ recurseRoutes([router.routeTree])
313
+
314
+ return [routesById, routesByPath] as const
315
+ }, [])
316
+
317
+ const looseRoutesById = routesById as Record<string, AnyRoute>
318
+
319
+ const flatRoutes = React.useMemo(
320
+ () =>
321
+ (Object.values(routesByPath) as AnyRoute[])
322
+ .map((d, i) => {
323
+ const trimmed = trimPath(d.fullPath)
324
+ const parsed = parsePathname(trimmed)
325
+
326
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
327
+ parsed.shift()
328
+ }
329
+
330
+ const score = parsed.map((d) => {
331
+ if (d.type === 'param') {
332
+ return 0.5
333
+ }
334
+
335
+ if (d.type === 'wildcard') {
336
+ return 0.25
337
+ }
338
+
339
+ return 1
340
+ })
341
+
342
+ return { child: d, trimmed, parsed, index: i, score }
343
+ })
344
+ .sort((a, b) => {
345
+ let isIndex = a.trimmed === '/' ? 1 : b.trimmed === '/' ? -1 : 0
346
+
347
+ if (isIndex !== 0) return isIndex
348
+
349
+ const length = Math.min(a.score.length, b.score.length)
350
+
351
+ // Sort by length of score
352
+ if (a.score.length !== b.score.length) {
353
+ return b.score.length - a.score.length
354
+ }
355
+
356
+ // Sort by min available score
357
+ for (let i = 0; i < length; i++) {
358
+ if (a.score[i] !== b.score[i]) {
359
+ return b.score[i]! - a.score[i]!
360
+ }
361
+ }
362
+
363
+ // Sort by min available parsed value
364
+ for (let i = 0; i < length; i++) {
365
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
366
+ return a.parsed[i]!.value! > b.parsed[i]!.value! ? 1 : -1
367
+ }
368
+ }
369
+
370
+ // Sort by length of trimmed full path
371
+ if (a.trimmed !== b.trimmed) {
372
+ return a.trimmed > b.trimmed ? 1 : -1
373
+ }
374
+
375
+ // Sort by original index
376
+ return a.index - b.index
377
+ })
378
+ .map((d, i) => {
379
+ d.child.rank = i
380
+ return d.child
381
+ }),
382
+ [routesByPath],
383
+ )
384
+
385
+ const matchRoutes = useStableCallback(
386
+ <TRouteTree extends AnyRoute>(
387
+ pathname: string,
388
+ locationSearch: AnySearchSchema,
389
+ opts?: { throwOnError?: boolean; debug?: boolean },
390
+ ): RouteMatch<TRouteTree>[] => {
391
+ let routeParams: AnyPathParams = {}
392
+
393
+ let foundRoute = flatRoutes.find((route) => {
394
+ const matchedParams = matchPathname(basepath, trimPathRight(pathname), {
395
+ to: route.fullPath,
396
+ caseSensitive: route.options.caseSensitive ?? options.caseSensitive,
397
+ fuzzy: false,
398
+ })
399
+
400
+ if (matchedParams) {
401
+ routeParams = matchedParams
402
+ return true
403
+ }
404
+
405
+ return false
406
+ })
407
+
408
+ let routeCursor: AnyRoute = foundRoute || (routesById as any)['__root__']
409
+
410
+ let matchedRoutes: AnyRoute[] = [routeCursor]
411
+ // let includingLayouts = true
412
+ while (routeCursor?.parentRoute) {
413
+ routeCursor = routeCursor.parentRoute
414
+ if (routeCursor) matchedRoutes.unshift(routeCursor)
415
+ }
416
+
417
+ // Existing matches are matches that are already loaded along with
418
+ // pending matches that are still loading
419
+
420
+ const parseErrors = matchedRoutes.map((route) => {
421
+ let parsedParamsError
422
+
423
+ if (route.options.parseParams) {
424
+ try {
425
+ const parsedParams = route.options.parseParams(routeParams)
426
+ // Add the parsed params to the accumulated params bag
427
+ Object.assign(routeParams, parsedParams)
428
+ } catch (err: any) {
429
+ parsedParamsError = new PathParamError(err.message, {
430
+ cause: err,
431
+ })
432
+
433
+ if (opts?.throwOnError) {
434
+ throw parsedParamsError
435
+ }
436
+
437
+ return parsedParamsError
438
+ }
439
+ }
440
+
441
+ return
442
+ })
443
+
444
+ const matches = matchedRoutes.map((route, index) => {
445
+ const interpolatedPath = interpolatePath(route.path, routeParams)
446
+ const matchId = interpolatePath(route.id, routeParams, true)
447
+
448
+ // Waste not, want not. If we already have a match for this route,
449
+ // reuse it. This is important for layout routes, which might stick
450
+ // around between navigation actions that only change leaf routes.
451
+ const existingMatch = getRouteMatch(state, matchId)
452
+
453
+ if (existingMatch) {
454
+ return { ...existingMatch }
455
+ }
456
+
457
+ // Create a fresh route match
458
+ const hasLoaders = !!(
459
+ route.options.load ||
460
+ componentTypes.some((d) => (route.options[d] as any)?.preload)
461
+ )
462
+
463
+ const routeMatch: AnyRouteMatch = {
464
+ id: matchId,
465
+ routeId: route.id,
466
+ params: routeParams,
467
+ pathname: joinPaths([basepath, interpolatedPath]),
468
+ updatedAt: Date.now(),
469
+ routeSearch: {},
470
+ search: {} as any,
471
+ status: hasLoaders ? 'pending' : 'success',
472
+ isFetching: false,
473
+ invalid: false,
474
+ error: undefined,
475
+ paramsError: parseErrors[index],
476
+ searchError: undefined,
477
+ loadPromise: Promise.resolve(),
478
+ context: undefined!,
479
+ abortController: new AbortController(),
480
+ fetchedAt: 0,
481
+ }
482
+
483
+ return routeMatch
484
+ })
485
+
486
+ // Take each match and resolve its search params and context
487
+ // This has to happen after the matches are created or found
488
+ // so that we can use the parent match's search params and context
489
+ matches.forEach((match, i): any => {
490
+ const parentMatch = matches[i - 1]
491
+ const route = looseRoutesById[match.routeId]!
492
+
493
+ const searchInfo = (() => {
494
+ // Validate the search params and stabilize them
495
+ const parentSearchInfo = {
496
+ search: parentMatch?.search ?? locationSearch,
497
+ routeSearch: parentMatch?.routeSearch ?? locationSearch,
498
+ }
499
+
500
+ try {
501
+ const validator =
502
+ typeof route.options.validateSearch === 'object'
503
+ ? route.options.validateSearch.parse
504
+ : route.options.validateSearch
505
+
506
+ let routeSearch = validator?.(parentSearchInfo.search) ?? {}
507
+
508
+ let search = {
509
+ ...parentSearchInfo.search,
510
+ ...routeSearch,
511
+ }
512
+
513
+ routeSearch = replaceEqualDeep(match.routeSearch, routeSearch)
514
+ search = replaceEqualDeep(match.search, search)
515
+
516
+ return {
517
+ routeSearch,
518
+ search,
519
+ searchDidChange: match.routeSearch !== routeSearch,
520
+ }
521
+ } catch (err: any) {
522
+ match.searchError = new SearchParamError(err.message, {
523
+ cause: err,
524
+ })
525
+
526
+ if (opts?.throwOnError) {
527
+ throw match.searchError
528
+ }
529
+
530
+ return parentSearchInfo
531
+ }
532
+ })()
533
+
534
+ Object.assign(match, searchInfo)
535
+ })
536
+
537
+ return matches as any
538
+ },
539
+ )
540
+
541
+ const cancelMatch = useStableCallback(
542
+ <TRouteTree extends AnyRoute>(id: string) => {
543
+ getRouteMatch(state, id)?.abortController?.abort()
544
+ },
545
+ )
546
+
547
+ const cancelMatches = useStableCallback(
548
+ <TRouteTree extends AnyRoute>(state: RouterState<TRouteTree>) => {
549
+ state.matches.forEach((match) => {
550
+ cancelMatch(match.id)
551
+ })
552
+ },
553
+ )
554
+
555
+ const buildLocation = useStableCallback<BuildLocationFn<TRouteTree>>(
556
+ (opts) => {
557
+ const build = (
558
+ dest: BuildNextOptions & {
559
+ unmaskOnReload?: boolean
560
+ } = {},
561
+ matches?: AnyRouteMatch[],
562
+ ): ParsedLocation => {
563
+ const from = latestLocationRef.current
564
+ const fromPathname = dest.from ?? from.pathname
565
+
566
+ let pathname = resolvePathWithBase(fromPathname, `${dest.to ?? ''}`)
567
+
568
+ const fromMatches = matchRoutes(fromPathname, from.search)
569
+ const stayingMatches = matches?.filter((d) =>
570
+ fromMatches?.find((e) => e.routeId === d.routeId),
571
+ )
572
+
573
+ const prevParams = { ...last(fromMatches)?.params }
574
+
575
+ let nextParams =
576
+ (dest.params ?? true) === true
577
+ ? prevParams
578
+ : functionalUpdate(dest.params!, prevParams)
579
+
580
+ if (nextParams) {
581
+ matches
582
+ ?.map((d) => looseRoutesById[d.routeId]!.options.stringifyParams)
583
+ .filter(Boolean)
584
+ .forEach((fn) => {
585
+ nextParams = { ...nextParams!, ...fn!(nextParams!) }
586
+ })
587
+ }
588
+
589
+ pathname = interpolatePath(pathname, nextParams ?? {})
590
+
591
+ const preSearchFilters =
592
+ stayingMatches
593
+ ?.map(
594
+ (match) =>
595
+ looseRoutesById[match.routeId]!.options.preSearchFilters ?? [],
596
+ )
597
+ .flat()
598
+ .filter(Boolean) ?? []
599
+
600
+ const postSearchFilters =
601
+ stayingMatches
602
+ ?.map(
603
+ (match) =>
604
+ looseRoutesById[match.routeId]!.options.postSearchFilters ?? [],
605
+ )
606
+ .flat()
607
+ .filter(Boolean) ?? []
608
+
609
+ // Pre filters first
610
+ const preFilteredSearch = preSearchFilters?.length
611
+ ? preSearchFilters?.reduce(
612
+ (prev, next) => next(prev) as any,
613
+ from.search,
614
+ )
615
+ : from.search
616
+
617
+ // Then the link/navigate function
618
+ const destSearch =
619
+ dest.search === true
620
+ ? preFilteredSearch // Preserve resolvedFrom true
621
+ : dest.search
622
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
623
+ : preSearchFilters?.length
624
+ ? preFilteredSearch // Preserve resolvedFrom filters
625
+ : {}
626
+
627
+ // Then post filters
628
+ const postFilteredSearch = postSearchFilters?.length
629
+ ? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
630
+ : destSearch
631
+
632
+ const search = replaceEqualDeep(from.search, postFilteredSearch)
633
+
634
+ const searchStr = options.stringifySearch(search)
635
+
636
+ const hash =
637
+ dest.hash === true
638
+ ? from.hash
639
+ : dest.hash
640
+ ? functionalUpdate(dest.hash!, from.hash)
641
+ : from.hash
642
+
643
+ const hashStr = hash ? `#${hash}` : ''
644
+
645
+ let nextState =
646
+ dest.state === true
647
+ ? from.state
648
+ : dest.state
649
+ ? functionalUpdate(dest.state, from.state)
650
+ : from.state
651
+
652
+ nextState = replaceEqualDeep(from.state, nextState)
653
+
654
+ return {
655
+ pathname,
656
+ search,
657
+ searchStr,
658
+ state: nextState as any,
659
+ hash,
660
+ href: history.createHref(`${pathname}${searchStr}${hashStr}`),
661
+ unmaskOnReload: dest.unmaskOnReload,
662
+ }
663
+ }
664
+
665
+ const buildWithMatches = (
666
+ dest: BuildNextOptions = {},
667
+ maskedDest?: BuildNextOptions,
668
+ ) => {
669
+ let next = build(dest)
670
+ let maskedNext = maskedDest ? build(maskedDest) : undefined
671
+
672
+ if (!maskedNext) {
673
+ let params = {}
674
+
675
+ let foundMask = options.routeMasks?.find((d) => {
676
+ const match = matchPathname(basepath, next.pathname, {
677
+ to: d.from,
678
+ caseSensitive: false,
679
+ fuzzy: false,
680
+ })
681
+
682
+ if (match) {
683
+ params = match
684
+ return true
685
+ }
686
+
687
+ return false
688
+ })
689
+
690
+ if (foundMask) {
691
+ foundMask = {
692
+ ...foundMask,
693
+ from: interpolatePath(foundMask.from, params) as any,
694
+ }
695
+ maskedDest = foundMask
696
+ maskedNext = build(maskedDest)
697
+ }
698
+ }
699
+
700
+ const nextMatches = matchRoutes(next.pathname, next.search)
701
+ const maskedMatches = maskedNext
702
+ ? matchRoutes(maskedNext.pathname, maskedNext.search)
703
+ : undefined
704
+ const maskedFinal = maskedNext
705
+ ? build(maskedDest, maskedMatches)
706
+ : undefined
707
+
708
+ const final = build(dest, nextMatches)
709
+
710
+ if (maskedFinal) {
711
+ final.maskedLocation = maskedFinal
712
+ }
713
+
714
+ return final
715
+ }
716
+
717
+ if (opts.mask) {
718
+ return buildWithMatches(opts, {
719
+ ...pick(opts, ['from']),
720
+ ...opts.mask,
721
+ })
722
+ }
723
+
724
+ return buildWithMatches(opts)
725
+ },
726
+ )
727
+
728
+ const commitLocation = useStableCallback(
729
+ async ({
730
+ startTransition,
731
+ ...next
732
+ }: ParsedLocation & CommitLocationOptions) => {
733
+ if (navigateTimeoutRef.current) clearTimeout(navigateTimeoutRef.current)
734
+
735
+ const isSameUrl = latestLocationRef.current.href === next.href
736
+
737
+ // If the next urls are the same and we're not replacing,
738
+ // do nothing
739
+ if (!isSameUrl || !next.replace) {
740
+ let { maskedLocation, ...nextHistory } = next
741
+
742
+ if (maskedLocation) {
743
+ nextHistory = {
744
+ ...maskedLocation,
745
+ state: {
746
+ ...maskedLocation.state,
747
+ __tempKey: undefined,
748
+ __tempLocation: {
749
+ ...nextHistory,
750
+ search: nextHistory.searchStr,
751
+ state: {
752
+ ...nextHistory.state,
753
+ __tempKey: undefined!,
754
+ __tempLocation: undefined!,
755
+ key: undefined!,
756
+ },
757
+ },
758
+ },
759
+ }
760
+
761
+ if (nextHistory.unmaskOnReload ?? options.unmaskOnReload ?? false) {
762
+ nextHistory.state.__tempKey = tempLocationKeyRef.current
763
+ }
764
+ }
765
+
766
+ const apply = () => {
767
+ history[next.replace ? 'replace' : 'push'](
768
+ nextHistory.href,
769
+ nextHistory.state,
770
+ )
771
+ }
772
+
773
+ if (startTransition ?? true) {
774
+ startReactTransition(apply)
775
+ } else {
776
+ apply()
777
+ }
778
+ }
779
+
780
+ resetNextScrollRef.current = next.resetScroll ?? true
781
+
782
+ return latestLoadPromiseRef.current
783
+ },
784
+ )
785
+
786
+ const buildAndCommitLocation = useStableCallback(
787
+ ({
788
+ replace,
789
+ resetScroll,
790
+ startTransition,
791
+ ...rest
792
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
793
+ const location = buildLocation(rest)
794
+ return commitLocation({
795
+ ...location,
796
+ startTransition,
797
+ replace,
798
+ resetScroll,
799
+ })
800
+ },
801
+ )
802
+
803
+ const navigate = useStableCallback<NavigateFn<TRouteTree>>(
804
+ ({ from, to = '', ...rest }) => {
805
+ // If this link simply reloads the current route,
806
+ // make sure it has a new key so it will trigger a data refresh
807
+
808
+ // If this `to` is a valid external URL, return
809
+ // null for LinkUtils
810
+ const toString = String(to)
811
+ const fromString = typeof from === 'undefined' ? from : String(from)
812
+ let isExternal
813
+
814
+ try {
815
+ new URL(`${toString}`)
816
+ isExternal = true
817
+ } catch (e) {}
818
+
819
+ invariant(
820
+ !isExternal,
821
+ 'Attempting to navigate to external url with this.navigate!',
822
+ )
823
+
824
+ return buildAndCommitLocation({
825
+ ...rest,
826
+ from: fromString,
827
+ to: toString,
828
+ })
829
+ },
830
+ )
831
+
832
+ const loadMatches = useStableCallback(
833
+ async ({
834
+ checkLatest,
835
+ matches,
836
+ preload,
837
+ }: {
838
+ checkLatest: () => Promise<void> | undefined
839
+ matches: AnyRouteMatch[]
840
+ preload?: boolean
841
+ }): Promise<RouteMatch[]> => {
842
+ let latestPromise
843
+ let firstBadMatchIndex: number | undefined
844
+
845
+ // Check each match middleware to see if the route can be accessed
846
+ try {
847
+ for (let [index, match] of matches.entries()) {
848
+ const parentMatch = matches[index - 1]
849
+ const route = looseRoutesById[match.routeId]!
850
+
851
+ const handleError = (err: any, code: string) => {
852
+ err.routerCode = code
853
+ firstBadMatchIndex = firstBadMatchIndex ?? index
854
+
855
+ if (isRedirect(err)) {
856
+ throw err
857
+ }
858
+
859
+ try {
860
+ route.options.onError?.(err)
861
+ } catch (errorHandlerErr) {
862
+ err = errorHandlerErr
863
+
864
+ if (isRedirect(errorHandlerErr)) {
865
+ throw errorHandlerErr
866
+ }
867
+ }
868
+
869
+ matches[index] = match = {
870
+ ...match,
871
+ error: err,
872
+ status: 'error',
873
+ updatedAt: Date.now(),
874
+ }
875
+ }
876
+
877
+ try {
878
+ if (match.paramsError) {
879
+ handleError(match.paramsError, 'PARSE_PARAMS')
880
+ }
881
+
882
+ if (match.searchError) {
883
+ handleError(match.searchError, 'VALIDATE_SEARCH')
884
+ }
885
+
886
+ const parentContext = parentMatch?.context ?? options.context ?? {}
887
+
888
+ const beforeLoadContext =
889
+ (await route.options.beforeLoad?.({
890
+ search: match.search,
891
+ abortController: match.abortController,
892
+ params: match.params,
893
+ preload: !!preload,
894
+ context: parentContext,
895
+ location: state.location, // TODO: This might need to be latestLocationRef.current...?
896
+ navigate: (opts) =>
897
+ navigate({ ...opts, from: match.pathname } as any),
898
+ buildLocation,
899
+ })) ?? ({} as any)
900
+
901
+ const context = {
902
+ ...parentContext,
903
+ ...beforeLoadContext,
904
+ }
905
+
906
+ matches[index] = match = {
907
+ ...match,
908
+ context: replaceEqualDeep(match.context, context),
909
+ }
910
+ } catch (err) {
911
+ handleError(err, 'BEFORE_LOAD')
912
+ break
913
+ }
914
+ }
915
+ } catch (err) {
916
+ if (isRedirect(err)) {
917
+ if (!preload) navigate(err as any)
918
+ return matches
919
+ }
920
+
921
+ throw err
922
+ }
923
+
924
+ const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
925
+ const matchPromises: Promise<any>[] = []
926
+
927
+ validResolvedMatches.forEach((match, index) => {
928
+ matchPromises.push(
929
+ (async () => {
930
+ const parentMatchPromise = matchPromises[index - 1]
931
+ const route = looseRoutesById[match.routeId]!
932
+
933
+ if (match.isFetching) {
934
+ return getRouteMatch(state, match.id)?.loadPromise
935
+ }
936
+
937
+ const handleIfRedirect = (err: any) => {
938
+ if (isRedirect(err)) {
939
+ if (!preload) {
940
+ navigate(err as any)
941
+ }
942
+ return true
943
+ }
944
+ return false
945
+ }
946
+
947
+ const load = async () => {
948
+ try {
949
+ const componentsPromise = Promise.all(
950
+ componentTypes.map(async (type) => {
951
+ const component = route.options[type]
952
+
953
+ if ((component as any)?.preload) {
954
+ await (component as any).preload()
955
+ }
956
+ }),
957
+ )
958
+
959
+ const loaderPromise = route.options.load?.({
960
+ params: match.params,
961
+ search: match.search,
962
+ preload: !!preload,
963
+ parentMatchPromise,
964
+ abortController: match.abortController,
965
+ context: match.context,
966
+ location: state.location,
967
+ navigate: (opts) =>
968
+ navigate({ ...opts, from: match.pathname }),
969
+ })
970
+
971
+ const [_, loaderContext] = await Promise.all([
972
+ componentsPromise,
973
+ loaderPromise,
974
+ ])
975
+ if ((latestPromise = checkLatest())) return await latestPromise
976
+
977
+ matches[index] = match = {
978
+ ...match,
979
+ error: undefined,
980
+ status: 'success',
981
+ isFetching: false,
982
+ updatedAt: Date.now(),
983
+ }
984
+ } catch (error) {
985
+ if ((latestPromise = checkLatest())) return await latestPromise
986
+ if (handleIfRedirect(error)) return
987
+
988
+ try {
989
+ route.options.onError?.(error)
990
+ } catch (onErrorError) {
991
+ error = onErrorError
992
+ if (handleIfRedirect(onErrorError)) return
993
+ }
994
+
995
+ matches[index] = match = {
996
+ ...match,
997
+ error,
998
+ status: 'error',
999
+ isFetching: false,
1000
+ updatedAt: Date.now(),
1001
+ }
1002
+ }
1003
+
1004
+ if (!preload) {
1005
+ setState((s) => ({
1006
+ ...s,
1007
+ matches: s.matches.map((d) =>
1008
+ d.id === match.id ? match : d,
1009
+ ),
1010
+ }))
1011
+ }
1012
+ }
1013
+
1014
+ let loadPromise: Promise<void> | undefined
1015
+
1016
+ matches[index] = match = {
1017
+ ...match,
1018
+ isFetching: true,
1019
+ fetchedAt: Date.now(),
1020
+ invalid: false,
1021
+ }
1022
+
1023
+ loadPromise = load()
1024
+
1025
+ matches[index] = match = {
1026
+ ...match,
1027
+ loadPromise,
1028
+ }
1029
+
1030
+ await loadPromise
1031
+ })(),
1032
+ )
1033
+ })
1034
+
1035
+ await Promise.all(matchPromises)
1036
+ return matches
1037
+ },
1038
+ )
1039
+
1040
+ const load = useStableCallback<LoadFn>(async () => {
1041
+ const promise = new Promise<void>(async (resolve, reject) => {
1042
+ const next = latestLocationRef.current
1043
+ const prevLocation = state.resolvedLocation || state.location
1044
+ const pathDidChange = !!(next && prevLocation!.href !== next.href)
1045
+ let latestPromise: Promise<void> | undefined | null
1046
+
1047
+ // Cancel any pending matches
1048
+ cancelMatches(state)
1049
+
1050
+ router.emit({
1051
+ type: 'onBeforeLoad',
1052
+ from: prevLocation,
1053
+ to: next ?? state.location,
1054
+ pathChanged: pathDidChange,
1055
+ })
1056
+
1057
+ // Match the routes
1058
+ let matches: RouteMatch<any, any>[] = matchRoutes(
1059
+ next.pathname,
1060
+ next.search,
1061
+ {
1062
+ debug: true,
1063
+ },
1064
+ )
1065
+
1066
+ // Ingest the new matches
1067
+ setState((s) => ({
1068
+ ...s,
1069
+ matches,
1070
+ }))
1071
+
1072
+ try {
1073
+ try {
1074
+ // Load the matches
1075
+ await loadMatches({
1076
+ matches,
1077
+ checkLatest: () => checkLatest(promise),
1078
+ })
1079
+ } catch (err) {
1080
+ // swallow this error, since we'll display the
1081
+ // errors on the route components
1082
+ }
1083
+
1084
+ // Only apply the latest transition
1085
+ if ((latestPromise = checkLatest(promise))) {
1086
+ return latestPromise
1087
+ }
1088
+
1089
+ // TODO:
1090
+ // const exitingMatchIds = previousMatches.filter(
1091
+ // (id) => !state.pendingMatches.includes(id),
1092
+ // )
1093
+ // const enteringMatchIds = state.pendingMatches.filter(
1094
+ // (id) => !previousMatches.includes(id),
1095
+ // )
1096
+ // const stayingMatchIds = previousMatches.filter((id) =>
1097
+ // state.pendingMatches.includes(id),
1098
+ // )
1099
+
1100
+ // setState((s) => ({
1101
+ // ...s,
1102
+ // status: 'idle',
1103
+ // resolvedLocation: s.location,
1104
+ // }))
1105
+
1106
+ // TODO:
1107
+ // ;(
1108
+ // [
1109
+ // [exitingMatchIds, 'onLeave'],
1110
+ // [enteringMatchIds, 'onEnter'],
1111
+ // [stayingMatchIds, 'onTransition'],
1112
+ // ] as const
1113
+ // ).forEach(([matches, hook]) => {
1114
+ // matches.forEach((match) => {
1115
+ // const route = this.getRoute(match.routeId)
1116
+ // route.options[hook]?.(match)
1117
+ // })
1118
+ // })
1119
+ router.emit({
1120
+ type: 'onLoad',
1121
+ from: prevLocation,
1122
+ to: next,
1123
+ pathChanged: pathDidChange,
1124
+ })
1125
+
1126
+ resolve()
1127
+ } catch (err) {
1128
+ // Only apply the latest transition
1129
+ if ((latestPromise = checkLatest(promise))) {
1130
+ return latestPromise
1131
+ }
1132
+
1133
+ reject(err)
1134
+ }
1135
+ })
1136
+
1137
+ latestLoadPromiseRef.current = promise
1138
+
1139
+ return latestLoadPromiseRef.current
1140
+ })
1141
+
1142
+ const preloadRoute = useStableCallback(
1143
+ async (navigateOpts: BuildNextOptions = state.location) => {
1144
+ let next = buildLocation(navigateOpts)
1145
+
1146
+ let matches = matchRoutes(next.pathname, next.search, {
1147
+ throwOnError: true,
1148
+ })
1149
+
1150
+ await loadMatches({
1151
+ matches,
1152
+ preload: true,
1153
+ checkLatest: () => undefined,
1154
+ })
1155
+
1156
+ return [last(matches)!, matches] as const
1157
+ },
1158
+ )
1159
+
1160
+ const buildLink = useStableCallback<BuildLinkFn<TRouteTree>>((dest) => {
1161
+ // If this link simply reloads the current route,
1162
+ // make sure it has a new key so it will trigger a data refresh
1163
+
1164
+ // If this `to` is a valid external URL, return
1165
+ // null for LinkUtils
1166
+
1167
+ const {
1168
+ to,
1169
+ preload: userPreload,
1170
+ preloadDelay: userPreloadDelay,
1171
+ activeOptions,
1172
+ disabled,
1173
+ target,
1174
+ replace,
1175
+ resetScroll,
1176
+ startTransition,
1177
+ } = dest
1178
+
1179
+ try {
1180
+ new URL(`${to}`)
1181
+ return {
1182
+ type: 'external',
1183
+ href: to as any,
1184
+ }
1185
+ } catch (e) {}
1186
+
1187
+ const nextOpts = dest
1188
+ const next = buildLocation(nextOpts as any)
1189
+
1190
+ const preload = userPreload ?? options.defaultPreload
1191
+ const preloadDelay = userPreloadDelay ?? options.defaultPreloadDelay ?? 0
1192
+
1193
+ // Compare path/hash for matches
1194
+ const currentPathSplit = latestLocationRef.current.pathname.split('/')
1195
+ const nextPathSplit = next.pathname.split('/')
1196
+ const pathIsFuzzyEqual = nextPathSplit.every(
1197
+ (d, i) => d === currentPathSplit[i],
1198
+ )
1199
+ // Combine the matches based on user options
1200
+ const pathTest = activeOptions?.exact
1201
+ ? latestLocationRef.current.pathname === next.pathname
1202
+ : pathIsFuzzyEqual
1203
+ const hashTest = activeOptions?.includeHash
1204
+ ? latestLocationRef.current.hash === next.hash
1205
+ : true
1206
+ const searchTest =
1207
+ activeOptions?.includeSearch ?? true
1208
+ ? partialDeepEqual(latestLocationRef.current.search, next.search)
1209
+ : true
1210
+
1211
+ // The final "active" test
1212
+ const isActive = pathTest && hashTest && searchTest
1213
+
1214
+ // The click handler
1215
+ const handleClick = (e: MouseEvent) => {
1216
+ if (
1217
+ !disabled &&
1218
+ !isCtrlEvent(e) &&
1219
+ !e.defaultPrevented &&
1220
+ (!target || target === '_self') &&
1221
+ e.button === 0
1222
+ ) {
1223
+ e.preventDefault()
1224
+
1225
+ // All is well? Navigate!
1226
+ commitLocation({ ...next, replace, resetScroll, startTransition })
1227
+ }
1228
+ }
1229
+
1230
+ // The click handler
1231
+ const handleFocus = (e: MouseEvent) => {
1232
+ if (preload) {
1233
+ preloadRoute(nextOpts as any).catch((err) => {
1234
+ console.warn(err)
1235
+ console.warn(preloadWarning)
1236
+ })
1237
+ }
1238
+ }
1239
+
1240
+ const handleTouchStart = (e: TouchEvent) => {
1241
+ preloadRoute(nextOpts as any).catch((err) => {
1242
+ console.warn(err)
1243
+ console.warn(preloadWarning)
1244
+ })
1245
+ }
1246
+
1247
+ const handleEnter = (e: MouseEvent) => {
1248
+ const target = (e.target || {}) as LinkCurrentTargetElement
1249
+
1250
+ if (preload) {
1251
+ if (target.preloadTimeout) {
1252
+ return
1253
+ }
1254
+
1255
+ target.preloadTimeout = setTimeout(() => {
1256
+ target.preloadTimeout = null
1257
+ preloadRoute(nextOpts as any).catch((err) => {
1258
+ console.warn(err)
1259
+ console.warn(preloadWarning)
1260
+ })
1261
+ }, preloadDelay)
1262
+ }
1263
+ }
1264
+
1265
+ const handleLeave = (e: MouseEvent) => {
1266
+ const target = (e.target || {}) as LinkCurrentTargetElement
1267
+
1268
+ if (target.preloadTimeout) {
1269
+ clearTimeout(target.preloadTimeout)
1270
+ target.preloadTimeout = null
1271
+ }
1272
+ }
1273
+
1274
+ return {
1275
+ type: 'internal',
1276
+ next,
1277
+ handleFocus,
1278
+ handleClick,
1279
+ handleEnter,
1280
+ handleLeave,
1281
+ handleTouchStart,
1282
+ isActive,
1283
+ disabled,
1284
+ }
1285
+ })
1286
+
1287
+ const latestLocationRef = React.useRef(state.location)
1288
+
1289
+ React.useLayoutEffect(() => {
1290
+ const unsub = history.subscribe(() => {
1291
+ latestLocationRef.current = parseLocation(latestLocationRef.current)
1292
+
1293
+ setState((s) => ({
1294
+ ...s,
1295
+ status: 'pending',
1296
+ }))
1297
+ if (state.location !== latestLocationRef.current) {
1298
+ try {
1299
+ load()
1300
+ } catch (err) {
1301
+ console.error(err)
1302
+ }
1303
+ }
1304
+ })
1305
+
1306
+ const nextLocation = buildLocation({
1307
+ search: true,
1308
+ params: true,
1309
+ hash: true,
1310
+ state: true,
1311
+ })
1312
+
1313
+ if (state.location.href !== nextLocation.href) {
1314
+ commitLocation({ ...nextLocation, replace: true })
1315
+ }
1316
+
1317
+ return () => {
1318
+ unsub()
1319
+ }
1320
+ }, [history])
1321
+
1322
+ const initialLoad = React.useRef(true)
1323
+
1324
+ if (initialLoad.current) {
1325
+ initialLoad.current = false
1326
+ startReactTransition(() => {
1327
+ try {
1328
+ load()
1329
+ } catch (err) {
1330
+ console.error(err)
1331
+ }
1332
+ })
1333
+ }
1334
+
1335
+ const matchRoute = useStableCallback<MatchRouteFn<TRouteTree>>(
1336
+ (location, opts) => {
1337
+ location = {
1338
+ ...location,
1339
+ to: location.to
1340
+ ? resolvePathWithBase((location.from || '') as string, location.to)
1341
+ : undefined,
1342
+ } as any
1343
+
1344
+ const next = buildLocation(location as any)
1345
+
1346
+ if (opts?.pending && state.status !== 'pending') {
1347
+ return false
1348
+ }
1349
+
1350
+ const baseLocation = opts?.pending
1351
+ ? latestLocationRef.current
1352
+ : state.resolvedLocation
1353
+
1354
+ // const baseLocation = state.resolvedLocation
1355
+
1356
+ if (!baseLocation) {
1357
+ return false
1358
+ }
1359
+
1360
+ const match = matchPathname(basepath, baseLocation.pathname, {
1361
+ ...opts,
1362
+ to: next.pathname,
1363
+ }) as any
1364
+
1365
+ if (!match) {
1366
+ return false
1367
+ }
1368
+
1369
+ if (match && (opts?.includeSearch ?? true)) {
1370
+ return partialDeepEqual(baseLocation.search, next.search)
1371
+ ? match
1372
+ : false
1373
+ }
1374
+
1375
+ return match
1376
+ },
1377
+ )
1378
+
1379
+ const routerContextValue: RouterContext<TRouteTree> = {
1380
+ routeTree: router.routeTree,
1381
+ navigate,
1382
+ buildLink,
1383
+ state,
1384
+ matchRoute,
1385
+ routesById,
1386
+ options,
1387
+ history,
1388
+ load,
1389
+ buildLocation,
1390
+ }
1391
+
1392
+ return (
1393
+ <routerContext.Provider value={routerContextValue}>
1394
+ <Matches />
1395
+ </routerContext.Provider>
1396
+ )
1397
+ }
1398
+
1399
+ export function getRouteMatch<TRouteTree extends AnyRoute>(
1400
+ state: RouterState<TRouteTree>,
1401
+ id: string,
1402
+ ): undefined | RouteMatch<TRouteTree> {
1403
+ return [...state.pendingMatches, ...state.matches].find((d) => d.id === id)
1404
+ }
1405
+
1406
+ export function useRouterState<
1407
+ TSelected = RouterState<RegisteredRouter['routeTree']>,
1408
+ >(opts?: {
1409
+ select: (state: RouterState<RegisteredRouter['routeTree']>) => TSelected
1410
+ }): TSelected {
1411
+ const { state } = useRouter()
1412
+ // return useStore(router.__store, opts?.select as any)
1413
+ return opts?.select ? opts.select(state) : (state as any)
1414
+ }
1415
+
1416
+ export type RouterProps<
1417
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
1418
+ TDehydrated extends Record<string, any> = Record<string, any>,
1419
+ > = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> & {
1420
+ router: Router<TRouteTree>
1421
+ context?: Partial<RouterOptions<TRouteTree, TDehydrated>['context']>
1422
+ }
1423
+
1424
+ export function useRouter<
1425
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
1426
+ >(): RouterContext<TRouteTree> {
1427
+ const resolvedContext = window.__TSR_ROUTER_CONTEXT__ || routerContext
1428
+ const value = React.useContext(resolvedContext)
1429
+ warning(value, 'useRouter must be used inside a <RouterProvider> component!')
1430
+ return value as any
1431
+ }
1432
+ export interface RouteMatch<
1433
+ TRouteTree extends AnyRoute = AnyRoute,
1434
+ TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
1435
+ > {
1436
+ id: string
1437
+ routeId: TRouteId
1438
+ pathname: string
1439
+ params: RouteById<TRouteTree, TRouteId>['types']['allParams']
1440
+ status: 'pending' | 'success' | 'error'
1441
+ isFetching: boolean
1442
+ invalid: boolean
1443
+ error: unknown
1444
+ paramsError: unknown
1445
+ searchError: unknown
1446
+ updatedAt: number
1447
+ loadPromise?: Promise<void>
1448
+ __resolveLoadPromise?: () => void
1449
+ context: RouteById<TRouteTree, TRouteId>['types']['allContext']
1450
+ routeSearch: RouteById<TRouteTree, TRouteId>['types']['searchSchema']
1451
+ search: FullSearchSchema<TRouteTree> &
1452
+ RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
1453
+ fetchedAt: number
1454
+ abortController: AbortController
1455
+ }
1456
+
1457
+ export type AnyRouteMatch = RouteMatch<any>