@tanstack/router-core 0.0.1-beta.36 → 0.0.1-beta.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.ts CHANGED
@@ -12,9 +12,10 @@ import { GetFrameworkGeneric } from './frameworks'
12
12
  import {
13
13
  LinkInfo,
14
14
  LinkOptions,
15
- NavigateOptionsAbsolute,
15
+ NavigateOptions,
16
16
  ToOptions,
17
17
  ValidFromPath,
18
+ ResolveRelativePath,
18
19
  } from './link'
19
20
  import {
20
21
  cleanPath,
@@ -41,18 +42,20 @@ import {
41
42
  RouteInfo,
42
43
  RoutesById,
43
44
  } from './routeInfo'
44
- import { createRouteMatch, RouteMatch } from './routeMatch'
45
+ import { createRouteMatch, RouteMatch, RouteMatchStore } from './routeMatch'
45
46
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
47
+ import { createStore, batch, SetStoreFunction } from '@solidjs/reactivity'
46
48
  import {
47
49
  functionalUpdate,
48
50
  last,
51
+ NoInfer,
49
52
  pick,
50
53
  PickAsRequired,
51
54
  PickRequired,
52
- replaceEqualDeep,
53
55
  Timeout,
54
56
  Updater,
55
57
  } from './utils'
58
+ import { sharedClone } from './sharedClone'
56
59
 
57
60
  export interface RegisterRouter {
58
61
  // router: Router
@@ -128,9 +131,6 @@ export interface RouterOptions<
128
131
  loadComponent?: (
129
132
  component: GetFrameworkGeneric<'Component'>,
130
133
  ) => Promise<GetFrameworkGeneric<'Component'>>
131
- // renderComponent?: (
132
- // component: GetFrameworkGeneric<'Component'>,
133
- // ) => GetFrameworkGeneric<'Element'>
134
134
  }
135
135
 
136
136
  export interface Action<
@@ -196,7 +196,7 @@ export interface LoaderState<
196
196
  loaderContext: LoaderContext<TFullSearchSchema, TAllParams>
197
197
  }
198
198
 
199
- export interface RouterState<
199
+ export interface RouterStore<
200
200
  TSearchObj extends AnySearchSchema = {},
201
201
  TState extends LocationState = LocationState,
202
202
  > {
@@ -211,6 +211,7 @@ export interface RouterState<
211
211
  loaders: Record<string, Loader>
212
212
  isFetching: boolean
213
213
  isPreloading: boolean
214
+ matchCache: Record<string, MatchCacheEntry>
214
215
  }
215
216
 
216
217
  type Listener = (router: Router<any, any, any>) => void
@@ -255,7 +256,7 @@ type LinkCurrentTargetElement = {
255
256
 
256
257
  export interface DehydratedRouterState
257
258
  extends Pick<
258
- RouterState,
259
+ RouterStore,
259
260
  'status' | 'latestLocation' | 'currentLocation' | 'lastUpdated'
260
261
  > {
261
262
  currentMatches: DehydratedRouteMatch[]
@@ -263,20 +264,19 @@ export interface DehydratedRouterState
263
264
 
264
265
  export interface DehydratedRouter<TRouterContext = unknown> {
265
266
  // location: Router['__location']
266
- state: DehydratedRouterState
267
+ store: DehydratedRouterState
267
268
  context: TRouterContext
268
269
  }
269
270
 
270
- interface DehydratedRouteMatch
271
- extends Pick<
272
- RouteMatch<any, any>,
273
- | 'matchId'
274
- | 'status'
275
- | 'routeLoaderData'
276
- | 'loaderData'
277
- | 'isInvalid'
278
- | 'invalidAt'
279
- > {}
271
+ export type MatchCache = Record<string, MatchCacheEntry>
272
+
273
+ interface DehydratedRouteMatch {
274
+ matchId: string
275
+ store: Pick<
276
+ RouteMatchStore<any, any>,
277
+ 'status' | 'routeLoaderData' | 'isInvalid' | 'invalidAt'
278
+ >
279
+ }
280
280
 
281
281
  export interface RouterContext {}
282
282
 
@@ -297,24 +297,14 @@ export interface Router<
297
297
  RouterOptions<TRouteConfig, TRouterContext>,
298
298
  'stringifySearch' | 'parseSearch' | 'context'
299
299
  >
300
- // Computed in this.update()
300
+ store: RouterStore<TAllRouteInfo['fullSearchSchema']>
301
+ setStore: SetStoreFunction<RouterStore<TAllRouteInfo['fullSearchSchema']>>
301
302
  basepath: string
302
- // Internal:
303
- listeners: Listener[]
304
303
  // __location: Location<TAllRouteInfo['fullSearchSchema']>
305
- navigateTimeout?: Timeout
306
- nextAction?: 'push' | 'replace'
307
- state: RouterState<TAllRouteInfo['fullSearchSchema']>
308
304
  routeTree: Route<TAllRouteInfo, RouteInfo>
309
305
  routesById: RoutesById<TAllRouteInfo>
310
- navigationPromise?: Promise<void>
311
- startedLoadingAt: number
312
- resolveNavigation: () => void
313
- subscribe: (listener: Listener) => () => void
314
306
  reset: () => void
315
- notify: () => void
316
307
  mount: () => () => void
317
- onFocus: () => void
318
308
  update: <
319
309
  TRouteConfig extends RouteConfig = RouteConfig,
320
310
  TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
@@ -326,7 +316,6 @@ export interface Router<
326
316
  buildNext: (opts: BuildNextOptions) => Location
327
317
  cancelMatches: () => void
328
318
  load: (next?: Location) => Promise<void>
329
- matchCache: Record<string, MatchCacheEntry>
330
319
  cleanMatchCache: () => void
331
320
  getRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
332
321
  id: TId,
@@ -356,7 +345,7 @@ export interface Router<
356
345
  TFrom extends ValidFromPath<TAllRouteInfo> = '/',
357
346
  TTo extends string = '.',
358
347
  >(
359
- opts: NavigateOptionsAbsolute<TAllRouteInfo, TFrom, TTo>,
348
+ opts: NavigateOptions<TAllRouteInfo, TFrom, TTo>,
360
349
  ) => Promise<void>
361
350
  matchRoute: <
362
351
  TFrom extends ValidFromPath<TAllRouteInfo> = '/',
@@ -364,7 +353,12 @@ export interface Router<
364
353
  >(
365
354
  matchLocation: ToOptions<TAllRouteInfo, TFrom, TTo>,
366
355
  opts?: MatchRouteOptions,
367
- ) => boolean
356
+ ) =>
357
+ | false
358
+ | TAllRouteInfo['routeInfoById'][ResolveRelativePath<
359
+ TFrom,
360
+ NoInfer<TTo>
361
+ >]['allParams']
368
362
  buildLink: <
369
363
  TFrom extends ValidFromPath<TAllRouteInfo> = '/',
370
364
  TTo extends string = '.',
@@ -373,20 +367,6 @@ export interface Router<
373
367
  ) => LinkInfo
374
368
  dehydrate: () => DehydratedRouter<TRouterContext>
375
369
  hydrate: (dehydratedRouter: DehydratedRouter<TRouterContext>) => void
376
- __: {
377
- buildRouteTree: (
378
- routeConfig: RouteConfig,
379
- ) => Route<TAllRouteInfo, AnyRouteInfo>
380
- parseLocation: (
381
- location: History['location'],
382
- previousLocation?: Location,
383
- ) => Location
384
- buildLocation: (dest: BuildNextOptions) => Location
385
- commitLocation: (next: Location, replace?: boolean) => Promise<void>
386
- navigate: (
387
- location: BuildNextOptions & { replace?: boolean },
388
- ) => Promise<void>
389
- }
390
370
  }
391
371
 
392
372
  // Detect if we're in the DOM
@@ -397,7 +377,7 @@ const isServer =
397
377
  const createDefaultHistory = () =>
398
378
  isServer ? createMemoryHistory() : createBrowserHistory()
399
379
 
400
- function getInitialRouterState(): RouterState {
380
+ function getInitialRouterState(): RouterStore {
401
381
  return {
402
382
  status: 'idle',
403
383
  latestLocation: null!,
@@ -406,8 +386,20 @@ function getInitialRouterState(): RouterState {
406
386
  actions: {},
407
387
  loaders: {},
408
388
  lastUpdated: Date.now(),
409
- isFetching: false,
410
- isPreloading: false,
389
+ matchCache: {},
390
+ get isFetching() {
391
+ return (
392
+ this.status === 'loading' ||
393
+ this.currentMatches.some((d) => d.store.isFetching)
394
+ )
395
+ },
396
+ get isPreloading() {
397
+ return Object.values(this.matchCache).some(
398
+ (d) =>
399
+ d.match.store.isFetching &&
400
+ !this.currentMatches.find((dd) => dd.matchId === d.match.matchId),
401
+ )
402
+ },
411
403
  }
412
404
  }
413
405
 
@@ -418,8 +410,6 @@ export function createRouter<
418
410
  >(
419
411
  userOptions?: RouterOptions<TRouteConfig, TRouterContext>,
420
412
  ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
421
- const history = userOptions?.history || createDefaultHistory()
422
-
423
413
  const originalOptions = {
424
414
  defaultLoaderGcMaxAge: 5 * 60 * 1000,
425
415
  defaultLoaderMaxAge: 0,
@@ -431,164 +421,311 @@ export function createRouter<
431
421
  parseSearch: userOptions?.parseSearch ?? defaultParseSearch,
432
422
  }
433
423
 
434
- let router: Router<TRouteConfig, TAllRouteInfo, TRouterContext> = {
424
+ const [store, setStore] = createStore<RouterStore>(getInitialRouterState())
425
+
426
+ let navigateTimeout: undefined | Timeout
427
+ let nextAction: undefined | 'push' | 'replace'
428
+ let navigationPromise: undefined | Promise<void>
429
+
430
+ let startedLoadingAt = Date.now()
431
+ let resolveNavigation = () => {}
432
+
433
+ function onFocus() {
434
+ router.load()
435
+ }
436
+
437
+ function buildRouteTree(rootRouteConfig: RouteConfig) {
438
+ const recurseRoutes = (
439
+ routeConfigs: RouteConfig[],
440
+ parent?: Route<TAllRouteInfo, any, any>,
441
+ ): Route<TAllRouteInfo, any, any>[] => {
442
+ return routeConfigs.map((routeConfig, i) => {
443
+ const routeOptions = routeConfig.options
444
+ const route = createRoute(routeConfig, routeOptions, i, parent, router)
445
+ const existingRoute = (router.routesById as any)[route.routeId]
446
+
447
+ if (existingRoute) {
448
+ if (process.env.NODE_ENV !== 'production') {
449
+ console.warn(
450
+ `Duplicate routes found with id: ${String(route.routeId)}`,
451
+ router.routesById,
452
+ route,
453
+ )
454
+ }
455
+ throw new Error()
456
+ }
457
+
458
+ ;(router.routesById as any)[route.routeId] = route
459
+
460
+ const children = routeConfig.children as RouteConfig[]
461
+
462
+ route.childRoutes = children?.length
463
+ ? recurseRoutes(children, route)
464
+ : undefined
465
+
466
+ return route
467
+ })
468
+ }
469
+
470
+ const routes = recurseRoutes([rootRouteConfig])
471
+
472
+ return routes[0]!
473
+ }
474
+
475
+ function parseLocation(
476
+ location: History['location'],
477
+ previousLocation?: Location,
478
+ ): Location {
479
+ const parsedSearch = router.options.parseSearch(location.search)
480
+
481
+ return {
482
+ pathname: location.pathname,
483
+ searchStr: location.search,
484
+ search: sharedClone(previousLocation?.search, parsedSearch),
485
+ hash: location.hash.split('#').reverse()[0] ?? '',
486
+ href: `${location.pathname}${location.search}${location.hash}`,
487
+ state: location.state as LocationState,
488
+ key: location.key,
489
+ }
490
+ }
491
+
492
+ function navigate(location: BuildNextOptions & { replace?: boolean }) {
493
+ const next = router.buildNext(location)
494
+ return commitLocation(next, location.replace)
495
+ }
496
+
497
+ function buildLocation(dest: BuildNextOptions = {}): Location {
498
+ const fromPathname = dest.fromCurrent
499
+ ? store.latestLocation.pathname
500
+ : dest.from ?? store.latestLocation.pathname
501
+
502
+ let pathname = resolvePath(
503
+ router.basepath ?? '/',
504
+ fromPathname,
505
+ `${dest.to ?? '.'}`,
506
+ )
507
+
508
+ const fromMatches = router.matchRoutes(store.latestLocation.pathname, {
509
+ strictParseParams: true,
510
+ })
511
+
512
+ const toMatches = router.matchRoutes(pathname)
513
+
514
+ const prevParams = { ...last(fromMatches)?.params }
515
+
516
+ let nextParams =
517
+ (dest.params ?? true) === true
518
+ ? prevParams
519
+ : functionalUpdate(dest.params!, prevParams)
520
+
521
+ if (nextParams) {
522
+ toMatches
523
+ .map((d) => d.options.stringifyParams)
524
+ .filter(Boolean)
525
+ .forEach((fn) => {
526
+ Object.assign({}, nextParams!, fn!(nextParams!))
527
+ })
528
+ }
529
+
530
+ pathname = interpolatePath(pathname, nextParams ?? {})
531
+
532
+ // Pre filters first
533
+ const preFilteredSearch = dest.__preSearchFilters?.length
534
+ ? dest.__preSearchFilters.reduce(
535
+ (prev, next) => next(prev),
536
+ store.latestLocation.search,
537
+ )
538
+ : store.latestLocation.search
539
+
540
+ // Then the link/navigate function
541
+ const destSearch =
542
+ dest.search === true
543
+ ? preFilteredSearch // Preserve resolvedFrom true
544
+ : dest.search
545
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
546
+ : dest.__preSearchFilters?.length
547
+ ? preFilteredSearch // Preserve resolvedFrom filters
548
+ : {}
549
+
550
+ // Then post filters
551
+ const postFilteredSearch = dest.__postSearchFilters?.length
552
+ ? dest.__postSearchFilters.reduce((prev, next) => next(prev), destSearch)
553
+ : destSearch
554
+
555
+ const search = sharedClone(store.latestLocation.search, postFilteredSearch)
556
+
557
+ const searchStr = router.options.stringifySearch(search)
558
+ let hash =
559
+ dest.hash === true
560
+ ? store.latestLocation.hash
561
+ : functionalUpdate(dest.hash!, store.latestLocation.hash)
562
+ hash = hash ? `#${hash}` : ''
563
+
564
+ return {
565
+ pathname,
566
+ search,
567
+ searchStr,
568
+ state: store.latestLocation.state,
569
+ hash,
570
+ href: `${pathname}${searchStr}${hash}`,
571
+ key: dest.key,
572
+ }
573
+ }
574
+
575
+ function commitLocation(next: Location, replace?: boolean): Promise<void> {
576
+ const id = '' + Date.now() + Math.random()
577
+
578
+ if (navigateTimeout) clearTimeout(navigateTimeout)
579
+
580
+ let nextAction: 'push' | 'replace' = 'replace'
581
+
582
+ if (!replace) {
583
+ nextAction = 'push'
584
+ }
585
+
586
+ const isSameUrl = parseLocation(router.history.location).href === next.href
587
+
588
+ if (isSameUrl && !next.key) {
589
+ nextAction = 'replace'
590
+ }
591
+
592
+ router.history[nextAction](
593
+ {
594
+ pathname: next.pathname,
595
+ hash: next.hash,
596
+ search: next.searchStr,
597
+ },
598
+ {
599
+ id,
600
+ ...next.state,
601
+ },
602
+ )
603
+
604
+ return (navigationPromise = new Promise((resolve) => {
605
+ const previousNavigationResolve = resolveNavigation
606
+
607
+ resolveNavigation = () => {
608
+ previousNavigationResolve()
609
+ resolve()
610
+ }
611
+ }))
612
+ }
613
+
614
+ const router: Router<TRouteConfig, TAllRouteInfo, TRouterContext> = {
435
615
  types: undefined!,
436
616
 
437
617
  // public api
438
- history,
618
+ history: userOptions?.history || createDefaultHistory(),
619
+ store,
620
+ setStore,
439
621
  options: originalOptions,
440
- listeners: [],
441
- // Resolved after construction
442
622
  basepath: '',
443
623
  routeTree: undefined!,
444
624
  routesById: {} as any,
445
- //
446
- resolveNavigation: () => {},
447
- matchCache: {},
448
- state: getInitialRouterState(),
625
+
449
626
  reset: () => {
450
- router.state = getInitialRouterState()
451
- router.notify()
452
- },
453
- startedLoadingAt: Date.now(),
454
- subscribe: (listener: Listener): (() => void) => {
455
- router.listeners.push(listener as Listener)
456
- return () => {
457
- router.listeners = router.listeners.filter((x) => x !== listener)
458
- }
627
+ setStore((s) => Object.assign(s, getInitialRouterState()))
459
628
  },
629
+
460
630
  getRoute: (id) => {
461
631
  return router.routesById[id]
462
632
  },
463
- notify: (): void => {
464
- const isFetching =
465
- router.state.status === 'loading' ||
466
- router.state.currentMatches.some((d) => d.isFetching)
467
-
468
- const isPreloading = Object.values(router.matchCache).some(
469
- (d) =>
470
- d.match.isFetching &&
471
- !router.state.currentMatches.find(
472
- (dd) => dd.matchId === d.match.matchId,
473
- ),
474
- )
475
-
476
- if (
477
- router.state.isFetching !== isFetching ||
478
- router.state.isPreloading !== isPreloading
479
- ) {
480
- router.state = {
481
- ...router.state,
482
- isFetching,
483
- isPreloading,
484
- }
485
- }
486
-
487
- cascadeLoaderData(router.state.currentMatches)
488
- router.listeners.forEach((listener) => listener(router))
489
- },
490
633
 
491
634
  dehydrate: () => {
492
635
  return {
493
- state: {
494
- ...pick(router.state, [
636
+ store: {
637
+ ...pick(store, [
495
638
  'latestLocation',
496
639
  'currentLocation',
497
640
  'status',
498
641
  'lastUpdated',
499
642
  ]),
500
- currentMatches: router.state.currentMatches.map((match) =>
501
- pick(match, [
502
- 'matchId',
643
+ currentMatches: store.currentMatches.map((match) => ({
644
+ matchId: match.matchId,
645
+ store: pick(match.store, [
503
646
  'status',
504
647
  'routeLoaderData',
505
- 'loaderData',
506
648
  'isInvalid',
507
649
  'invalidAt',
508
650
  ]),
509
- ),
651
+ })),
510
652
  },
511
653
  context: router.options.context as TRouterContext,
512
654
  }
513
655
  },
514
656
 
515
- hydrate: (dehydratedState) => {
516
- // Update the location
517
- router.state.latestLocation = dehydratedState.state.latestLocation
518
- router.state.currentLocation = dehydratedState.state.currentLocation
519
-
520
- // Update the context
521
- router.options.context = dehydratedState.context
657
+ hydrate: (dehydratedRouter) => {
658
+ setStore((s) => {
659
+ // Update the context TODO: make this part of state?
660
+ router.options.context = dehydratedRouter.context
522
661
 
523
- // Match the routes
524
- const currentMatches = router.matchRoutes(
525
- router.state.latestLocation.pathname,
526
- {
527
- strictParseParams: true,
528
- },
529
- )
530
-
531
- currentMatches.forEach((match, index) => {
532
- const dehydratedMatch = dehydratedState.state.currentMatches[index]
533
- invariant(
534
- dehydratedMatch,
535
- 'Oh no! Dehydrated route matches did not match the active state of the router 😬',
662
+ // Match the routes
663
+ const currentMatches = router.matchRoutes(
664
+ dehydratedRouter.store.latestLocation.pathname,
665
+ {
666
+ strictParseParams: true,
667
+ },
536
668
  )
537
- Object.assign(match, dehydratedMatch)
538
- })
539
669
 
540
- currentMatches.forEach((match) => match.__.validate())
670
+ currentMatches.forEach((match, index) => {
671
+ const dehydratedMatch = dehydratedRouter.store.currentMatches[index]
672
+ invariant(
673
+ dehydratedMatch && dehydratedMatch.matchId === match.matchId,
674
+ 'Oh no! There was a hydration mismatch when attempting to restore the state of the router! 😬',
675
+ )
676
+ Object.assign(match, dehydratedMatch)
677
+ })
541
678
 
542
- router.state = {
543
- ...router.state,
544
- ...dehydratedState,
545
- currentMatches,
546
- }
679
+ currentMatches.forEach((match) => match.__.validate())
680
+
681
+ Object.assign(s, { ...dehydratedRouter.store, currentMatches })
682
+ })
547
683
  },
548
684
 
549
685
  mount: () => {
550
- if (!router.state.currentMatches.length) {
551
- router.load()
552
- }
686
+ // Mount only does anything on the client
687
+ if (!isServer) {
688
+ // If the router matches are empty, load the matches
689
+ if (!store.currentMatches.length) {
690
+ router.load()
691
+ }
553
692
 
554
- const unsub = router.history.listen((event) => {
555
- router.load(
556
- router.__.parseLocation(event.location, router.state.latestLocation),
557
- )
558
- })
693
+ const unsub = router.history.listen((event) => {
694
+ router.load(parseLocation(event.location, store.latestLocation))
695
+ })
559
696
 
560
- // addEventListener does not exist in React Native, but window does
561
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
562
- if (!isServer && window.addEventListener) {
563
- // Listen to visibillitychange and focus
564
- window.addEventListener('visibilitychange', router.onFocus, false)
565
- window.addEventListener('focus', router.onFocus, false)
566
- }
697
+ // addEventListener does not exist in React Native, but window does
698
+ // In the future, we might need to invert control here for more adapters
699
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
700
+ if (window.addEventListener) {
701
+ // Listen to visibilitychange and focus
702
+ window.addEventListener('visibilitychange', onFocus, false)
703
+ window.addEventListener('focus', onFocus, false)
704
+ }
567
705
 
568
- return () => {
569
- unsub()
570
- if (!isServer && window.removeEventListener) {
571
- // Be sure to unsubscribe if a new handler is set
572
- window.removeEventListener('visibilitychange', router.onFocus)
573
- window.removeEventListener('focus', router.onFocus)
706
+ return () => {
707
+ unsub()
708
+ if (window.removeEventListener) {
709
+ // Be sure to unsubscribe if a new handler is set
710
+ window.removeEventListener('visibilitychange', onFocus)
711
+ window.removeEventListener('focus', onFocus)
712
+ }
574
713
  }
575
714
  }
576
- },
577
715
 
578
- onFocus: () => {
579
- router.load()
716
+ return () => {}
580
717
  },
581
718
 
582
719
  update: (opts) => {
583
720
  const newHistory = opts?.history !== router.history
584
- if (!router.state.latestLocation || newHistory) {
721
+ if (!store.latestLocation || newHistory) {
585
722
  if (opts?.history) {
586
723
  router.history = opts.history
587
724
  }
588
- router.state.latestLocation = router.__.parseLocation(
589
- router.history.location,
590
- )
591
- router.state.currentLocation = router.state.latestLocation
725
+ setStore((s) => {
726
+ s.latestLocation = parseLocation(router.history.location)
727
+ s.currentLocation = s.latestLocation
728
+ })
592
729
  }
593
730
 
594
731
  Object.assign(router.options, opts)
@@ -599,55 +736,50 @@ export function createRouter<
599
736
 
600
737
  if (routeConfig) {
601
738
  router.routesById = {} as any
602
- router.routeTree = router.__.buildRouteTree(routeConfig)
739
+ router.routeTree = buildRouteTree(routeConfig)
603
740
  }
604
741
 
605
742
  return router as any
606
743
  },
607
744
 
608
745
  cancelMatches: () => {
609
- ;[
610
- ...router.state.currentMatches,
611
- ...(router.state.pendingMatches ?? []),
612
- ].forEach((match) => {
613
- match.cancel()
614
- })
746
+ ;[...store.currentMatches, ...(store.pendingMatches || [])].forEach(
747
+ (match) => {
748
+ match.cancel()
749
+ },
750
+ )
615
751
  },
616
752
 
617
753
  load: async (next?: Location) => {
618
- const id = Math.random()
619
- router.startedLoadingAt = id
620
-
621
- if (next) {
622
- // Ingest the new location
623
- router.state.latestLocation = next
624
- }
754
+ let now = Date.now()
755
+ const startedAt = now
756
+ startedLoadingAt = startedAt
625
757
 
626
758
  // Cancel any pending matches
627
759
  router.cancelMatches()
628
760
 
629
- // Match the routes
630
- const matches = router.matchRoutes(router.state.latestLocation.pathname, {
631
- strictParseParams: true,
632
- })
761
+ let matches!: RouteMatch<any, any>[]
633
762
 
634
- if (typeof document !== 'undefined') {
635
- router.state = {
636
- ...router.state,
637
- status: 'loading',
638
- pendingMatches: matches,
639
- pendingLocation: router.state.latestLocation,
640
- }
641
- } else {
642
- router.state = {
643
- ...router.state,
644
- status: 'loading',
645
- currentMatches: matches,
646
- currentLocation: router.state.latestLocation,
763
+ batch(() => {
764
+ if (next) {
765
+ // Ingest the new location
766
+ setStore((s) => {
767
+ s.latestLocation = next
768
+ })
647
769
  }
648
- }
649
770
 
650
- router.notify()
771
+ // Match the routes
772
+ matches = router.matchRoutes(store.latestLocation.pathname, {
773
+ strictParseParams: true,
774
+ })
775
+
776
+ console.log('set loading', matches)
777
+ setStore((s) => {
778
+ s.status = 'loading'
779
+ s.pendingMatches = matches
780
+ s.pendingLocation = store.latestLocation
781
+ })
782
+ })
651
783
 
652
784
  // Load the matches
653
785
  try {
@@ -660,12 +792,12 @@ export function createRouter<
660
792
  )
661
793
  }
662
794
 
663
- if (router.startedLoadingAt !== id) {
664
- // Ignore side-effects of match loading
665
- return router.navigationPromise
795
+ if (startedLoadingAt !== startedAt) {
796
+ // Ignore side-effects of outdated side-effects
797
+ return navigationPromise
666
798
  }
667
799
 
668
- const previousMatches = router.state.currentMatches
800
+ const previousMatches = store.currentMatches
669
801
 
670
802
  const exiting: RouteMatch[] = [],
671
803
  staying: RouteMatch[] = []
@@ -682,18 +814,18 @@ export function createRouter<
682
814
  return !previousMatches.find((dd) => dd.matchId === d.matchId)
683
815
  })
684
816
 
685
- const now = Date.now()
817
+ now = Date.now()
686
818
 
687
819
  exiting.forEach((d) => {
688
820
  d.__.onExit?.({
689
821
  params: d.params,
690
- search: d.routeSearch,
822
+ search: d.store.routeSearch,
691
823
  })
692
824
 
693
825
  // Clear idle error states when match leaves
694
- if (d.status === 'error' && !d.isFetching) {
695
- d.status = 'idle'
696
- d.error = undefined
826
+ if (d.store.status === 'error' && !d.store.isFetching) {
827
+ d.store.status = 'idle'
828
+ d.store.error = undefined
697
829
  }
698
830
 
699
831
  const gc = Math.max(
@@ -702,7 +834,7 @@ export function createRouter<
702
834
  )
703
835
 
704
836
  if (gc > 0) {
705
- router.matchCache[d.matchId] = {
837
+ store.matchCache[d.matchId] = {
706
838
  gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
707
839
  match: d,
708
840
  }
@@ -712,19 +844,19 @@ export function createRouter<
712
844
  staying.forEach((d) => {
713
845
  d.options.onTransition?.({
714
846
  params: d.params,
715
- search: d.routeSearch,
847
+ search: d.store.routeSearch,
716
848
  })
717
849
  })
718
850
 
719
851
  entering.forEach((d) => {
720
852
  d.__.onExit = d.options.onLoaded?.({
721
853
  params: d.params,
722
- search: d.search,
854
+ search: d.store.search,
723
855
  })
724
- delete router.matchCache[d.matchId]
856
+ delete store.matchCache[d.matchId]
725
857
  })
726
858
 
727
- if (router.startedLoadingAt !== id) {
859
+ if (startedLoadingAt !== startedAt) {
728
860
  // Ignore side-effects of match loading
729
861
  return
730
862
  }
@@ -732,46 +864,50 @@ export function createRouter<
732
864
  matches.forEach((match) => {
733
865
  // Clear actions
734
866
  if (match.action) {
867
+ // TODO: Check reactivity here
735
868
  match.action.current = undefined
736
869
  match.action.submissions = []
737
870
  }
738
871
  })
739
872
 
740
- router.state = {
741
- ...router.state,
742
- status: 'idle',
743
- currentLocation: router.state.latestLocation,
744
- currentMatches: matches,
745
- pendingLocation: undefined,
746
- pendingMatches: undefined,
747
- }
873
+ setStore((s) => {
874
+ console.log('set', matches)
875
+ Object.assign(s, {
876
+ status: 'idle',
877
+ currentLocation: store.latestLocation,
878
+ currentMatches: matches,
879
+ pendingLocation: undefined,
880
+ pendingMatches: undefined,
881
+ })
882
+ })
748
883
 
749
- router.notify()
750
- router.resolveNavigation()
884
+ resolveNavigation()
751
885
  },
752
886
 
753
887
  cleanMatchCache: () => {
754
888
  const now = Date.now()
755
889
 
756
- Object.keys(router.matchCache).forEach((matchId) => {
757
- const entry = router.matchCache[matchId]!
890
+ setStore((s) => {
891
+ Object.keys(s.matchCache).forEach((matchId) => {
892
+ const entry = s.matchCache[matchId]!
758
893
 
759
- // Don't remove loading matches
760
- if (entry.match.status === 'loading') {
761
- return
762
- }
894
+ // Don't remove loading matches
895
+ if (entry.match.store.status === 'loading') {
896
+ return
897
+ }
763
898
 
764
- // Do not remove successful matches that are still valid
765
- if (entry.gc > 0 && entry.gc > now) {
766
- return
767
- }
899
+ // Do not remove successful matches that are still valid
900
+ if (entry.gc > 0 && entry.gc > now) {
901
+ return
902
+ }
768
903
 
769
- // Everything else gets removed
770
- delete router.matchCache[matchId]
904
+ // Everything else gets removed
905
+ delete s.matchCache[matchId]
906
+ })
771
907
  })
772
908
  },
773
909
 
774
- loadRoute: async (navigateOpts = router.state.latestLocation) => {
910
+ loadRoute: async (navigateOpts = store.latestLocation) => {
775
911
  const next = router.buildNext(navigateOpts)
776
912
  const matches = router.matchRoutes(next.pathname, {
777
913
  strictParseParams: true,
@@ -780,10 +916,7 @@ export function createRouter<
780
916
  return matches
781
917
  },
782
918
 
783
- preloadRoute: async (
784
- navigateOpts = router.state.latestLocation,
785
- loaderOpts,
786
- ) => {
919
+ preloadRoute: async (navigateOpts = store.latestLocation, loaderOpts) => {
787
920
  const next = router.buildNext(navigateOpts)
788
921
  const matches = router.matchRoutes(next.pathname, {
789
922
  strictParseParams: true,
@@ -815,8 +948,8 @@ export function createRouter<
815
948
  }
816
949
 
817
950
  const existingMatches = [
818
- ...router.state.currentMatches,
819
- ...(router.state.pendingMatches ?? []),
951
+ ...store.currentMatches,
952
+ ...(store.pendingMatches ?? []),
820
953
  ]
821
954
 
822
955
  const recurse = async (routes: Route<any, any>[]): Promise<void> => {
@@ -847,14 +980,6 @@ export function createRouter<
847
980
  route.options.caseSensitive ?? router.options.caseSensitive,
848
981
  })
849
982
 
850
- // console.log(
851
- // router.basepath,
852
- // route.fullPath,
853
- // fuzzy,
854
- // pathname,
855
- // matchParams,
856
- // )
857
-
858
983
  if (matchParams) {
859
984
  let parsedParams
860
985
 
@@ -895,7 +1020,7 @@ export function createRouter<
895
1020
 
896
1021
  const match =
897
1022
  existingMatches.find((d) => d.matchId === matchId) ||
898
- router.matchCache[matchId]?.match ||
1023
+ store.matchCache[matchId]?.match ||
899
1024
  createRouteMatch(router, foundRoute, {
900
1025
  parentMatch,
901
1026
  matchId,
@@ -915,7 +1040,7 @@ export function createRouter<
915
1040
 
916
1041
  recurse([router.routeTree])
917
1042
 
918
- cascadeLoaderData(matches)
1043
+ linkMatches(matches)
919
1044
 
920
1045
  return matches
921
1046
  },
@@ -945,7 +1070,7 @@ export function createRouter<
945
1070
  )
946
1071
 
947
1072
  const matchPromises = resolvedMatches.map(async (match) => {
948
- const search = match.search as { __data?: any }
1073
+ const search = match.store.search as { __data?: any }
949
1074
 
950
1075
  if (search.__data?.matchId && search.__data.matchId !== match.matchId) {
951
1076
  return
@@ -953,14 +1078,12 @@ export function createRouter<
953
1078
 
954
1079
  match.load(loaderOpts)
955
1080
 
956
- if (match.status !== 'success' && match.__.loadPromise) {
1081
+ if (match.store.status !== 'success' && match.__.loadPromise) {
957
1082
  // Wait for the first sign of activity from the match
958
1083
  await match.__.loadPromise
959
1084
  }
960
1085
  })
961
1086
 
962
- router.notify()
963
-
964
1087
  await Promise.all(matchPromises)
965
1088
  },
966
1089
 
@@ -970,9 +1093,9 @@ export function createRouter<
970
1093
  (await routeMatch.options.loader?.({
971
1094
  // parentLoaderPromise: routeMatch.parentMatch?.__.dataPromise,
972
1095
  params: routeMatch.params,
973
- search: routeMatch.routeSearch,
1096
+ search: routeMatch.store.routeSearch,
974
1097
  signal: routeMatch.__.abortController.signal,
975
- })) ?? {}
1098
+ })) || {}
976
1099
  )
977
1100
  } else {
978
1101
  const next = router.buildNext({
@@ -1013,18 +1136,17 @@ export function createRouter<
1013
1136
  const unloadedMatchIds = router
1014
1137
  .matchRoutes(next.pathname)
1015
1138
  .map((d) => d.matchId)
1016
- ;[
1017
- ...router.state.currentMatches,
1018
- ...(router.state.pendingMatches ?? []),
1019
- ].forEach((match) => {
1020
- if (unloadedMatchIds.includes(match.matchId)) {
1021
- match.invalidate()
1022
- }
1023
- })
1139
+ ;[...store.currentMatches, ...(store.pendingMatches ?? [])].forEach(
1140
+ (match) => {
1141
+ if (unloadedMatchIds.includes(match.matchId)) {
1142
+ match.invalidate()
1143
+ }
1144
+ },
1145
+ )
1024
1146
  },
1025
1147
 
1026
1148
  reload: () =>
1027
- router.__.navigate({
1149
+ navigate({
1028
1150
  fromCurrent: true,
1029
1151
  replace: true,
1030
1152
  search: true,
@@ -1047,13 +1169,13 @@ export function createRouter<
1047
1169
  const next = router.buildNext(location)
1048
1170
 
1049
1171
  if (opts?.pending) {
1050
- if (!router.state.pendingLocation) {
1172
+ if (!store.pendingLocation) {
1051
1173
  return false
1052
1174
  }
1053
1175
 
1054
1176
  return !!matchPathname(
1055
1177
  router.basepath,
1056
- router.state.pendingLocation.pathname,
1178
+ store.pendingLocation.pathname,
1057
1179
  {
1058
1180
  ...opts,
1059
1181
  to: next.pathname,
@@ -1061,14 +1183,10 @@ export function createRouter<
1061
1183
  )
1062
1184
  }
1063
1185
 
1064
- return !!matchPathname(
1065
- router.basepath,
1066
- router.state.currentLocation.pathname,
1067
- {
1068
- ...opts,
1069
- to: next.pathname,
1070
- },
1071
- )
1186
+ return matchPathname(router.basepath, store.currentLocation.pathname, {
1187
+ ...opts,
1188
+ to: next.pathname,
1189
+ }) as any
1072
1190
  },
1073
1191
 
1074
1192
  navigate: async ({ from, to = '.', search, hash, replace, params }) => {
@@ -1092,7 +1210,7 @@ export function createRouter<
1092
1210
  'Attempting to navigate to external url with router.navigate!',
1093
1211
  )
1094
1212
 
1095
- return router.__.navigate({
1213
+ return navigate({
1096
1214
  from: fromString,
1097
1215
  to: toString,
1098
1216
  search,
@@ -1147,14 +1265,13 @@ export function createRouter<
1147
1265
  userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
1148
1266
 
1149
1267
  // Compare path/hash for matches
1150
- const pathIsEqual =
1151
- router.state.currentLocation.pathname === next.pathname
1152
- const currentPathSplit = router.state.currentLocation.pathname.split('/')
1268
+ const pathIsEqual = store.currentLocation.pathname === next.pathname
1269
+ const currentPathSplit = store.currentLocation.pathname.split('/')
1153
1270
  const nextPathSplit = next.pathname.split('/')
1154
1271
  const pathIsFuzzyEqual = nextPathSplit.every(
1155
1272
  (d, i) => d === currentPathSplit[i],
1156
1273
  )
1157
- const hashIsEqual = router.state.currentLocation.hash === next.hash
1274
+ const hashIsEqual = store.currentLocation.hash === next.hash
1158
1275
  // Combine the matches based on user options
1159
1276
  const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
1160
1277
  const hashTest = activeOptions?.includeHash ? hashIsEqual : true
@@ -1176,8 +1293,8 @@ export function createRouter<
1176
1293
  router.invalidateRoute(nextOpts)
1177
1294
  }
1178
1295
 
1179
- // All is well? Navigate!)
1180
- router.__.navigate(nextOpts)
1296
+ // All is well? Navigate!
1297
+ navigate(nextOpts)
1181
1298
  }
1182
1299
  }
1183
1300
 
@@ -1240,7 +1357,7 @@ export function createRouter<
1240
1357
  }
1241
1358
  },
1242
1359
  buildNext: (opts: BuildNextOptions) => {
1243
- const next = router.__.buildLocation(opts)
1360
+ const next = buildLocation(opts)
1244
1361
 
1245
1362
  const matches = router.matchRoutes(next.pathname)
1246
1363
 
@@ -1254,217 +1371,12 @@ export function createRouter<
1254
1371
  .flat()
1255
1372
  .filter(Boolean)
1256
1373
 
1257
- return router.__.buildLocation({
1374
+ return buildLocation({
1258
1375
  ...opts,
1259
1376
  __preSearchFilters,
1260
1377
  __postSearchFilters,
1261
1378
  })
1262
1379
  },
1263
-
1264
- __: {
1265
- buildRouteTree: (rootRouteConfig: RouteConfig) => {
1266
- const recurseRoutes = (
1267
- routeConfigs: RouteConfig[],
1268
- parent?: Route<TAllRouteInfo, any, any>,
1269
- ): Route<TAllRouteInfo, any, any>[] => {
1270
- return routeConfigs.map((routeConfig) => {
1271
- const routeOptions = routeConfig.options
1272
- const route = createRoute(routeConfig, routeOptions, parent, router)
1273
- const existingRoute = (router.routesById as any)[route.routeId]
1274
-
1275
- if (existingRoute) {
1276
- if (process.env.NODE_ENV !== 'production') {
1277
- console.warn(
1278
- `Duplicate routes found with id: ${String(route.routeId)}`,
1279
- router.routesById,
1280
- route,
1281
- )
1282
- }
1283
- throw new Error()
1284
- }
1285
-
1286
- ;(router.routesById as any)[route.routeId] = route
1287
-
1288
- const children = routeConfig.children as RouteConfig[]
1289
-
1290
- route.childRoutes = children?.length
1291
- ? recurseRoutes(children, route)
1292
- : undefined
1293
-
1294
- return route
1295
- })
1296
- }
1297
-
1298
- const routes = recurseRoutes([rootRouteConfig])
1299
-
1300
- return routes[0]!
1301
- },
1302
-
1303
- parseLocation: (
1304
- location: History['location'],
1305
- previousLocation?: Location,
1306
- ): Location => {
1307
- const parsedSearch = router.options.parseSearch(location.search)
1308
-
1309
- return {
1310
- pathname: location.pathname,
1311
- searchStr: location.search,
1312
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1313
- hash: location.hash.split('#').reverse()[0] ?? '',
1314
- href: `${location.pathname}${location.search}${location.hash}`,
1315
- state: location.state as LocationState,
1316
- key: location.key,
1317
- }
1318
- },
1319
-
1320
- navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1321
- const next = router.buildNext(location)
1322
- return router.__.commitLocation(next, location.replace)
1323
- },
1324
-
1325
- buildLocation: (dest: BuildNextOptions = {}): Location => {
1326
- const fromPathname = dest.fromCurrent
1327
- ? router.state.latestLocation.pathname
1328
- : dest.from ?? router.state.latestLocation.pathname
1329
-
1330
- let pathname = resolvePath(
1331
- router.basepath ?? '/',
1332
- fromPathname,
1333
- `${dest.to ?? '.'}`,
1334
- )
1335
-
1336
- const fromMatches = router.matchRoutes(
1337
- router.state.latestLocation.pathname,
1338
- {
1339
- strictParseParams: true,
1340
- },
1341
- )
1342
-
1343
- const toMatches = router.matchRoutes(pathname)
1344
-
1345
- const prevParams = { ...last(fromMatches)?.params }
1346
-
1347
- let nextParams =
1348
- (dest.params ?? true) === true
1349
- ? prevParams
1350
- : functionalUpdate(dest.params!, prevParams)
1351
-
1352
- if (nextParams) {
1353
- toMatches
1354
- .map((d) => d.options.stringifyParams)
1355
- .filter(Boolean)
1356
- .forEach((fn) => {
1357
- Object.assign({}, nextParams!, fn!(nextParams!))
1358
- })
1359
- }
1360
-
1361
- pathname = interpolatePath(pathname, nextParams ?? {})
1362
-
1363
- // Pre filters first
1364
- const preFilteredSearch = dest.__preSearchFilters?.length
1365
- ? dest.__preSearchFilters.reduce(
1366
- (prev, next) => next(prev),
1367
- router.state.latestLocation.search,
1368
- )
1369
- : router.state.latestLocation.search
1370
-
1371
- // Then the link/navigate function
1372
- const destSearch =
1373
- dest.search === true
1374
- ? preFilteredSearch // Preserve resolvedFrom true
1375
- : dest.search
1376
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1377
- : dest.__preSearchFilters?.length
1378
- ? preFilteredSearch // Preserve resolvedFrom filters
1379
- : {}
1380
-
1381
- // Then post filters
1382
- const postFilteredSearch = dest.__postSearchFilters?.length
1383
- ? dest.__postSearchFilters.reduce(
1384
- (prev, next) => next(prev),
1385
- destSearch,
1386
- )
1387
- : destSearch
1388
-
1389
- const search = replaceEqualDeep(
1390
- router.state.latestLocation.search,
1391
- postFilteredSearch,
1392
- )
1393
-
1394
- const searchStr = router.options.stringifySearch(search)
1395
- let hash =
1396
- dest.hash === true
1397
- ? router.state.latestLocation.hash
1398
- : functionalUpdate(dest.hash!, router.state.latestLocation.hash)
1399
- hash = hash ? `#${hash}` : ''
1400
-
1401
- return {
1402
- pathname,
1403
- search,
1404
- searchStr,
1405
- state: router.state.latestLocation.state,
1406
- hash,
1407
- href: `${pathname}${searchStr}${hash}`,
1408
- key: dest.key,
1409
- }
1410
- },
1411
-
1412
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1413
- const id = '' + Date.now() + Math.random()
1414
-
1415
- if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
1416
-
1417
- let nextAction: 'push' | 'replace' = 'replace'
1418
-
1419
- if (!replace) {
1420
- nextAction = 'push'
1421
- }
1422
-
1423
- const isSameUrl =
1424
- router.__.parseLocation(history.location).href === next.href
1425
-
1426
- if (isSameUrl && !next.key) {
1427
- nextAction = 'replace'
1428
- }
1429
-
1430
- if (nextAction === 'replace') {
1431
- history.replace(
1432
- {
1433
- pathname: next.pathname,
1434
- hash: next.hash,
1435
- search: next.searchStr,
1436
- },
1437
- {
1438
- id,
1439
- ...next.state,
1440
- },
1441
- )
1442
- } else {
1443
- history.push(
1444
- {
1445
- pathname: next.pathname,
1446
- hash: next.hash,
1447
- search: next.searchStr,
1448
- },
1449
- {
1450
- id,
1451
- },
1452
- )
1453
- }
1454
-
1455
- router.navigationPromise = new Promise((resolve) => {
1456
- const previousNavigationResolve = router.resolveNavigation
1457
-
1458
- router.resolveNavigation = () => {
1459
- previousNavigationResolve()
1460
- resolve()
1461
- delete router.navigationPromise
1462
- }
1463
- })
1464
-
1465
- return router.navigationPromise
1466
- },
1467
- },
1468
1380
  }
1469
1381
 
1470
1382
  router.update(userOptions)
@@ -1479,15 +1391,14 @@ function isCtrlEvent(e: MouseEvent) {
1479
1391
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1480
1392
  }
1481
1393
 
1482
- function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
1394
+ function linkMatches(matches: RouteMatch<any, any>[]) {
1483
1395
  matches.forEach((match, index) => {
1484
1396
  const parent = matches[index - 1]
1485
1397
 
1486
1398
  if (parent) {
1487
- match.loaderData = replaceEqualDeep(match.loaderData, {
1488
- ...parent.loaderData,
1489
- ...match.routeLoaderData,
1490
- })
1399
+ match.__.setParentMatch(parent)
1400
+ } else {
1401
+ match.__.setParentMatch(undefined)
1491
1402
  }
1492
1403
  })
1493
1404
  }