@tanstack/react-router 0.0.1-beta.204 → 0.0.1-beta.206

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