@tanstack/react-router 0.0.1-beta.223 → 0.0.1-beta.224

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