@tanstack/router-core 0.0.1-alpha.9 → 0.0.1-beta.10

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
@@ -23,10 +23,13 @@ import {
23
23
  matchPathname,
24
24
  resolvePath,
25
25
  } from './path'
26
- import { AnyRoute, cascadeLoaderData, createRoute, Route } from './route'
26
+ import { AnyRoute, createRoute, Route } from './route'
27
27
  import {
28
+ AnyLoaderData,
29
+ AnyPathParams,
28
30
  AnyRouteConfig,
29
31
  AnySearchSchema,
32
+ LoaderContext,
30
33
  RouteConfig,
31
34
  SearchFilter,
32
35
  } from './routeConfig'
@@ -42,7 +45,9 @@ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
42
45
  import {
43
46
  functionalUpdate,
44
47
  last,
48
+ pick,
45
49
  PickAsRequired,
50
+ PickRequired,
46
51
  replaceEqualDeep,
47
52
  Timeout,
48
53
  Updater,
@@ -99,6 +104,9 @@ export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
99
104
  basepath?: string
100
105
  createRouter?: (router: Router<any, any>) => void
101
106
  createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
107
+ createElement?: (
108
+ element: GetFrameworkGeneric<'SyncOrAsyncElement'>,
109
+ ) => Promise<GetFrameworkGeneric<'Element'>>
102
110
  }
103
111
 
104
112
  export interface Action<
@@ -106,10 +114,13 @@ export interface Action<
106
114
  TResponse = unknown,
107
115
  // TError = unknown,
108
116
  > {
109
- submit: (submission?: TPayload) => Promise<TResponse>
117
+ submit: (
118
+ submission?: TPayload,
119
+ actionOpts?: { invalidate?: boolean; multi?: boolean },
120
+ ) => Promise<TResponse>
110
121
  current?: ActionState<TPayload, TResponse>
111
122
  latest?: ActionState<TPayload, TResponse>
112
- pending: ActionState<TPayload, TResponse>[]
123
+ submissions: ActionState<TPayload, TResponse>[]
113
124
  }
114
125
 
115
126
  export interface ActionState<
@@ -120,19 +131,54 @@ export interface ActionState<
120
131
  submittedAt: number
121
132
  status: 'idle' | 'pending' | 'success' | 'error'
122
133
  submission: TPayload
134
+ isMulti: boolean
123
135
  data?: TResponse
124
136
  error?: unknown
125
137
  }
126
138
 
139
+ export interface Loader<
140
+ TFullSearchSchema extends AnySearchSchema = {},
141
+ TAllParams extends AnyPathParams = {},
142
+ TRouteLoaderData = AnyLoaderData,
143
+ > {
144
+ fetch: keyof PickRequired<TFullSearchSchema> extends never
145
+ ? keyof TAllParams extends never
146
+ ? (loaderContext: { signal?: AbortSignal }) => Promise<TRouteLoaderData>
147
+ : (loaderContext: {
148
+ params: TAllParams
149
+ search?: TFullSearchSchema
150
+ signal?: AbortSignal
151
+ }) => Promise<TRouteLoaderData>
152
+ : keyof TAllParams extends never
153
+ ? (loaderContext: {
154
+ search: TFullSearchSchema
155
+ params: TAllParams
156
+ signal?: AbortSignal
157
+ }) => Promise<TRouteLoaderData>
158
+ : (loaderContext: {
159
+ search: TFullSearchSchema
160
+ signal?: AbortSignal
161
+ }) => Promise<TRouteLoaderData>
162
+ current?: LoaderState<TFullSearchSchema, TAllParams>
163
+ latest?: LoaderState<TFullSearchSchema, TAllParams>
164
+ pending: LoaderState<TFullSearchSchema, TAllParams>[]
165
+ }
166
+
167
+ export interface LoaderState<
168
+ TFullSearchSchema = unknown,
169
+ TAllParams = unknown,
170
+ > {
171
+ loadedAt: number
172
+ loaderContext: LoaderContext<TFullSearchSchema, TAllParams>
173
+ }
174
+
127
175
  export interface RouterState {
128
176
  status: 'idle' | 'loading'
129
177
  location: Location
130
178
  matches: RouteMatch[]
131
179
  lastUpdated: number
132
- loaderData: unknown
133
- currentAction?: ActionState
134
- latestAction?: ActionState
135
180
  actions: Record<string, Action>
181
+ loaders: Record<string, Loader>
136
182
  pending?: PendingState
137
183
  isFetching: boolean
138
184
  isPreloading: boolean
@@ -143,7 +189,7 @@ export interface PendingState {
143
189
  matches: RouteMatch[]
144
190
  }
145
191
 
146
- type Listener = () => void
192
+ type Listener = (router: Router<any, any>) => void
147
193
 
148
194
  export type ListenerFn = () => void
149
195
 
@@ -181,10 +227,27 @@ type LinkCurrentTargetElement = {
181
227
  preloadTimeout?: null | ReturnType<typeof setTimeout>
182
228
  }
183
229
 
230
+ interface DehydratedRouterState
231
+ extends Pick<RouterState, 'status' | 'location' | 'lastUpdated'> {
232
+ matches: DehydratedRouteMatch[]
233
+ }
234
+
235
+ interface DehydratedRouteMatch
236
+ extends Pick<
237
+ RouteMatch<any, any>,
238
+ | 'matchId'
239
+ | 'status'
240
+ | 'routeLoaderData'
241
+ | 'loaderData'
242
+ | 'isInvalid'
243
+ | 'invalidAt'
244
+ > {}
245
+
184
246
  export interface Router<
185
247
  TRouteConfig extends AnyRouteConfig = RouteConfig,
186
248
  TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
187
249
  > {
250
+ history: BrowserHistory | MemoryHistory | HashHistory
188
251
  options: PickAsRequired<
189
252
  RouterOptions<TRouteConfig>,
190
253
  'stringifySearch' | 'parseSearch'
@@ -201,28 +264,20 @@ export interface Router<
201
264
  routeTree: Route<TAllRouteInfo, RouteInfo>
202
265
  routesById: RoutesById<TAllRouteInfo>
203
266
  navigationPromise: Promise<void>
204
- removeActionQueue: { action: Action; actionState: ActionState }[]
205
267
  startedLoadingAt: number
206
268
  resolveNavigation: () => void
207
269
  subscribe: (listener: Listener) => () => void
270
+ reset: () => void
208
271
  notify: () => void
209
272
  mount: () => () => void
210
273
  onFocus: () => void
211
274
  update: <TRouteConfig extends RouteConfig = RouteConfig>(
212
275
  opts?: RouterOptions<TRouteConfig>,
213
276
  ) => Router<TRouteConfig>
214
- buildRouteTree: (
215
- routeConfig: RouteConfig,
216
- ) => Route<TAllRouteInfo, AnyRouteInfo>
217
- parseLocation: (
218
- location: History['location'],
219
- previousLocation?: Location,
220
- ) => Location
221
- buildLocation: (dest: BuildNextOptions) => Location
222
- commitLocation: (next: Location, replace?: boolean) => Promise<void>
277
+
223
278
  buildNext: (opts: BuildNextOptions) => Location
224
279
  cancelMatches: () => void
225
- loadLocation: (next?: Location) => Promise<void>
280
+ load: (next?: Location) => Promise<void>
226
281
  matchCache: Record<string, MatchCacheEntry>
227
282
  cleanMatchCache: () => void
228
283
  getRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
@@ -247,9 +302,6 @@ export interface Router<
247
302
  invalidateRoute: (opts: MatchLocation) => void
248
303
  reload: () => Promise<void>
249
304
  resolvePath: (from: string, path: string) => string
250
- _navigate: (
251
- location: BuildNextOptions & { replace?: boolean },
252
- ) => Promise<void>
253
305
  navigate: <
254
306
  TFrom extends ValidFromPath<TAllRouteInfo> = '/',
255
307
  TTo extends string = '.',
@@ -269,16 +321,44 @@ export interface Router<
269
321
  >(
270
322
  opts: LinkOptions<TAllRouteInfo, TFrom, TTo>,
271
323
  ) => LinkInfo
324
+ dehydrateState: () => DehydratedRouterState
325
+ hydrateState: (state: DehydratedRouterState) => void
326
+ __: {
327
+ buildRouteTree: (
328
+ routeConfig: RouteConfig,
329
+ ) => Route<TAllRouteInfo, AnyRouteInfo>
330
+ parseLocation: (
331
+ location: History['location'],
332
+ previousLocation?: Location,
333
+ ) => Location
334
+ buildLocation: (dest: BuildNextOptions) => Location
335
+ commitLocation: (next: Location, replace?: boolean) => Promise<void>
336
+ navigate: (
337
+ location: BuildNextOptions & { replace?: boolean },
338
+ ) => Promise<void>
339
+ }
272
340
  }
273
341
 
274
342
  // Detect if we're in the DOM
275
- const isServer = Boolean(
276
- typeof window === 'undefined' || !window.document?.createElement,
277
- )
343
+ const isServer =
344
+ typeof window === 'undefined' || !window.document?.createElement
278
345
 
279
346
  // This is the default history object if none is defined
280
347
  const createDefaultHistory = () =>
281
- !isServer ? createBrowserHistory() : createMemoryHistory()
348
+ isServer ? createMemoryHistory() : createBrowserHistory()
349
+
350
+ function getInitialRouterState(): RouterState {
351
+ return {
352
+ status: 'idle',
353
+ location: null!,
354
+ matches: [],
355
+ actions: {},
356
+ loaders: {},
357
+ lastUpdated: Date.now(),
358
+ isFetching: false,
359
+ isPreloading: false,
360
+ }
361
+ }
282
362
 
283
363
  export function createRouter<
284
364
  TRouteConfig extends AnyRouteConfig = RouteConfig,
@@ -299,9 +379,9 @@ export function createRouter<
299
379
  }
300
380
 
301
381
  let router: Router<TRouteConfig, TAllRouteInfo> = {
382
+ history,
302
383
  options: originalOptions,
303
384
  listeners: [],
304
- removeActionQueue: [],
305
385
  // Resolved after construction
306
386
  basepath: '',
307
387
  routeTree: undefined!,
@@ -312,15 +392,10 @@ export function createRouter<
312
392
  navigationPromise: Promise.resolve(),
313
393
  resolveNavigation: () => {},
314
394
  matchCache: {},
315
- state: {
316
- status: 'idle',
317
- location: null!,
318
- matches: [],
319
- actions: {},
320
- loaderData: {} as any,
321
- lastUpdated: Date.now(),
322
- isFetching: false,
323
- isPreloading: false,
395
+ state: getInitialRouterState(),
396
+ reset: () => {
397
+ router.state = getInitialRouterState()
398
+ router.notify()
324
399
  },
325
400
  startedLoadingAt: Date.now(),
326
401
  subscribe: (listener: Listener): (() => void) => {
@@ -346,11 +421,51 @@ export function createRouter<
346
421
  }
347
422
 
348
423
  cascadeLoaderData(router.state.matches)
349
- router.listeners.forEach((listener) => listener())
424
+ router.listeners.forEach((listener) => listener(router))
425
+ },
426
+
427
+ dehydrateState: () => {
428
+ return {
429
+ ...pick(router.state, ['status', 'location', 'lastUpdated']),
430
+ matches: router.state.matches.map((match) =>
431
+ pick(match, [
432
+ 'matchId',
433
+ 'status',
434
+ 'routeLoaderData',
435
+ 'loaderData',
436
+ 'isInvalid',
437
+ 'invalidAt',
438
+ ]),
439
+ ),
440
+ }
441
+ },
442
+
443
+ hydrateState: (dehydratedState) => {
444
+ // Match the routes
445
+ const matches = router.matchRoutes(router.location.pathname, {
446
+ strictParseParams: true,
447
+ })
448
+
449
+ matches.forEach((match, index) => {
450
+ const dehydratedMatch = dehydratedState.matches[index]
451
+ invariant(
452
+ dehydratedMatch,
453
+ 'Oh no! Dehydrated route matches did not match the active state of the router 😬',
454
+ )
455
+ Object.assign(match, dehydratedMatch)
456
+ })
457
+
458
+ router.loadMatches(matches)
459
+
460
+ router.state = {
461
+ ...router.state,
462
+ ...dehydratedState,
463
+ matches,
464
+ }
350
465
  },
351
466
 
352
467
  mount: () => {
353
- const next = router.buildLocation({
468
+ const next = router.__.buildLocation({
354
469
  to: '.',
355
470
  search: true,
356
471
  hash: true,
@@ -359,15 +474,13 @@ export function createRouter<
359
474
  // If the current location isn't updated, trigger a navigation
360
475
  // to the current location. Otherwise, load the current location.
361
476
  if (next.href !== router.location.href) {
362
- router.commitLocation(next, true)
363
- } else {
364
- router.loadLocation()
477
+ router.__.commitLocation(next, true)
365
478
  }
366
479
 
367
- const unsub = history.listen((event) => {
368
- router.loadLocation(
369
- router.parseLocation(event.location, router.location),
370
- )
480
+ // router.load()
481
+
482
+ const unsub = router.history.listen((event) => {
483
+ router.load(router.__.parseLocation(event.location, router.location))
371
484
  })
372
485
 
373
486
  // addEventListener does not exist in React Native, but window does
@@ -380,17 +493,28 @@ export function createRouter<
380
493
 
381
494
  return () => {
382
495
  unsub()
383
- // Be sure to unsubscribe if a new handler is set
384
- window.removeEventListener('visibilitychange', router.onFocus)
385
- window.removeEventListener('focus', router.onFocus)
496
+ if (!isServer && window.removeEventListener) {
497
+ // Be sure to unsubscribe if a new handler is set
498
+ window.removeEventListener('visibilitychange', router.onFocus)
499
+ window.removeEventListener('focus', router.onFocus)
500
+ }
386
501
  }
387
502
  },
388
503
 
389
504
  onFocus: () => {
390
- router.loadLocation()
505
+ router.load()
391
506
  },
392
507
 
393
508
  update: (opts) => {
509
+ const newHistory = opts?.history !== router.history
510
+ if (!router.location || newHistory) {
511
+ if (opts?.history) {
512
+ router.history = opts.history
513
+ }
514
+ router.location = router.__.parseLocation(router.history.location)
515
+ router.state.location = router.location
516
+ }
517
+
394
518
  Object.assign(router.options, opts)
395
519
 
396
520
  const { basepath, routeConfig } = router.options
@@ -399,235 +523,12 @@ export function createRouter<
399
523
 
400
524
  if (routeConfig) {
401
525
  router.routesById = {} as any
402
- router.routeTree = router.buildRouteTree(routeConfig)
526
+ router.routeTree = router.__.buildRouteTree(routeConfig)
403
527
  }
404
528
 
405
529
  return router as any
406
530
  },
407
531
 
408
- buildRouteTree: (rootRouteConfig: RouteConfig) => {
409
- const recurseRoutes = (
410
- routeConfigs: RouteConfig[],
411
- parent?: Route<TAllRouteInfo, any>,
412
- ): Route<TAllRouteInfo, any>[] => {
413
- return routeConfigs.map((routeConfig) => {
414
- const routeOptions = routeConfig.options
415
- const route = createRoute(routeConfig, routeOptions, parent, router)
416
-
417
- // {
418
- // pendingMs: routeOptions.pendingMs ?? router.defaultPendingMs,
419
- // pendingMinMs: routeOptions.pendingMinMs ?? router.defaultPendingMinMs,
420
- // }
421
-
422
- const existingRoute = (router.routesById as any)[route.routeId]
423
-
424
- if (existingRoute) {
425
- if (process.env.NODE_ENV !== 'production') {
426
- console.warn(
427
- `Duplicate routes found with id: ${String(route.routeId)}`,
428
- router.routesById,
429
- route,
430
- )
431
- }
432
- throw new Error()
433
- }
434
-
435
- ;(router.routesById as any)[route.routeId] = route
436
-
437
- const children = routeConfig.children as RouteConfig[]
438
-
439
- route.childRoutes = children?.length
440
- ? recurseRoutes(children, route)
441
- : undefined
442
-
443
- return route
444
- })
445
- }
446
-
447
- const routes = recurseRoutes([rootRouteConfig])
448
-
449
- return routes[0]!
450
- },
451
-
452
- parseLocation: (
453
- location: History['location'],
454
- previousLocation?: Location,
455
- ): Location => {
456
- const parsedSearch = router.options.parseSearch(location.search)
457
-
458
- return {
459
- pathname: location.pathname,
460
- searchStr: location.search,
461
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
462
- hash: location.hash.split('#').reverse()[0] ?? '',
463
- href: `${location.pathname}${location.search}${location.hash}`,
464
- state: location.state as LocationState,
465
- key: location.key,
466
- }
467
- },
468
-
469
- buildLocation: (dest: BuildNextOptions = {}): Location => {
470
- // const resolvedFrom: Location = {
471
- // ...router.location,
472
- const fromPathname = dest.fromCurrent
473
- ? router.location.pathname
474
- : dest.from ?? router.location.pathname
475
-
476
- let pathname = resolvePath(
477
- router.basepath ?? '/',
478
- fromPathname,
479
- `${dest.to ?? '.'}`,
480
- )
481
-
482
- const fromMatches = router.matchRoutes(router.location.pathname, {
483
- strictParseParams: true,
484
- })
485
-
486
- const toMatches = router.matchRoutes(pathname)
487
-
488
- const prevParams = { ...last(fromMatches)?.params }
489
-
490
- let nextParams =
491
- (dest.params ?? true) === true
492
- ? prevParams
493
- : functionalUpdate(dest.params!, prevParams)
494
-
495
- if (nextParams) {
496
- toMatches
497
- .map((d) => d.options.stringifyParams)
498
- .filter(Boolean)
499
- .forEach((fn) => {
500
- Object.assign({}, nextParams!, fn!(nextParams!))
501
- })
502
- }
503
-
504
- pathname = interpolatePath(pathname, nextParams ?? {})
505
-
506
- // Pre filters first
507
- const preFilteredSearch = dest.__preSearchFilters?.length
508
- ? dest.__preSearchFilters.reduce(
509
- (prev, next) => next(prev),
510
- router.location.search,
511
- )
512
- : router.location.search
513
-
514
- // Then the link/navigate function
515
- const destSearch =
516
- dest.search === true
517
- ? preFilteredSearch // Preserve resolvedFrom true
518
- : dest.search
519
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
520
- : dest.__preSearchFilters?.length
521
- ? preFilteredSearch // Preserve resolvedFrom filters
522
- : {}
523
-
524
- // Then post filters
525
- const postFilteredSearch = dest.__postSearchFilters?.length
526
- ? dest.__postSearchFilters.reduce(
527
- (prev, next) => next(prev),
528
- destSearch,
529
- )
530
- : destSearch
531
-
532
- const search = replaceEqualDeep(
533
- router.location.search,
534
- postFilteredSearch,
535
- )
536
-
537
- const searchStr = router.options.stringifySearch(search)
538
- let hash =
539
- dest.hash === true
540
- ? router.location.hash
541
- : functionalUpdate(dest.hash!, router.location.hash)
542
- hash = hash ? `#${hash}` : ''
543
-
544
- return {
545
- pathname,
546
- search,
547
- searchStr,
548
- state: router.location.state,
549
- hash,
550
- href: `${pathname}${searchStr}${hash}`,
551
- key: dest.key,
552
- }
553
- },
554
-
555
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
556
- const id = '' + Date.now() + Math.random()
557
-
558
- if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
559
-
560
- let nextAction: 'push' | 'replace' = 'replace'
561
-
562
- if (!replace) {
563
- nextAction = 'push'
564
- }
565
-
566
- const isSameUrl =
567
- router.parseLocation(history.location).href === next.href
568
-
569
- if (isSameUrl && !next.key) {
570
- nextAction = 'replace'
571
- }
572
-
573
- if (nextAction === 'replace') {
574
- history.replace(
575
- {
576
- pathname: next.pathname,
577
- hash: next.hash,
578
- search: next.searchStr,
579
- },
580
- {
581
- id,
582
- },
583
- )
584
- } else {
585
- history.push(
586
- {
587
- pathname: next.pathname,
588
- hash: next.hash,
589
- search: next.searchStr,
590
- },
591
- {
592
- id,
593
- },
594
- )
595
- }
596
-
597
- router.navigationPromise = new Promise((resolve) => {
598
- const previousNavigationResolve = router.resolveNavigation
599
-
600
- router.resolveNavigation = () => {
601
- previousNavigationResolve()
602
- resolve()
603
- }
604
- })
605
-
606
- return router.navigationPromise
607
- },
608
-
609
- buildNext: (opts: BuildNextOptions) => {
610
- const next = router.buildLocation(opts)
611
-
612
- const matches = router.matchRoutes(next.pathname)
613
-
614
- const __preSearchFilters = matches
615
- .map((match) => match.options.preSearchFilters ?? [])
616
- .flat()
617
- .filter(Boolean)
618
-
619
- const __postSearchFilters = matches
620
- .map((match) => match.options.postSearchFilters ?? [])
621
- .flat()
622
- .filter(Boolean)
623
-
624
- return router.buildLocation({
625
- ...opts,
626
- __preSearchFilters,
627
- __postSearchFilters,
628
- })
629
- },
630
-
631
532
  cancelMatches: () => {
632
533
  ;[
633
534
  ...router.state.matches,
@@ -637,7 +538,7 @@ export function createRouter<
637
538
  })
638
539
  },
639
540
 
640
- loadLocation: async (next?: Location) => {
541
+ load: async (next?: Location) => {
641
542
  const id = Math.random()
642
543
  router.startedLoadingAt = id
643
544
 
@@ -646,22 +547,11 @@ export function createRouter<
646
547
  router.location = next
647
548
  }
648
549
 
649
- // Clear out old actions
650
- router.removeActionQueue.forEach(({ action, actionState }) => {
651
- if (router.state.currentAction === actionState) {
652
- router.state.currentAction = undefined
653
- }
654
- if (action.current === actionState) {
655
- action.current = undefined
656
- }
657
- })
658
- router.removeActionQueue = []
659
-
660
550
  // Cancel any pending matches
661
551
  router.cancelMatches()
662
552
 
663
553
  // Match the routes
664
- const matches = router.matchRoutes(location.pathname, {
554
+ const matches = router.matchRoutes(router.location.pathname, {
665
555
  strictParseParams: true,
666
556
  })
667
557
 
@@ -706,6 +596,13 @@ export function createRouter<
706
596
  params: d.params,
707
597
  search: d.routeSearch,
708
598
  })
599
+
600
+ // // Clear actions
601
+ // if (d.action) {
602
+ // d.action.current = undefined
603
+ // d.action.submissions = []
604
+ // }
605
+
709
606
  // Clear idle error states when match leaves
710
607
  if (d.status === 'error' && !d.isFetching) {
711
608
  d.status = 'idle'
@@ -739,19 +636,24 @@ export function createRouter<
739
636
  params: d.params,
740
637
  search: d.search,
741
638
  })
639
+ delete router.matchCache[d.matchId]
742
640
  })
743
641
 
744
- if (matches.some((d) => d.status === 'loading')) {
745
- router.notify()
746
- await Promise.all(
747
- matches.map((d) => d.__.loaderPromise || Promise.resolve()),
748
- )
749
- }
642
+ // router.notify()
643
+
750
644
  if (router.startedLoadingAt !== id) {
751
645
  // Ignore side-effects of match loading
752
646
  return
753
647
  }
754
648
 
649
+ matches.forEach((match) => {
650
+ // Clear actions
651
+ if (match.action) {
652
+ match.action.current = undefined
653
+ match.action.submissions = []
654
+ }
655
+ })
656
+
755
657
  router.state = {
756
658
  ...router.state,
757
659
  location: router.location,
@@ -922,43 +824,17 @@ export function createRouter<
922
824
  },
923
825
 
924
826
  loadMatches: async (resolvedMatches, loaderOpts) => {
925
- const now = Date.now()
926
- const minMaxAge = loaderOpts?.preload
927
- ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
928
- : 0
929
-
930
827
  const matchPromises = resolvedMatches.map(async (match) => {
931
828
  // Validate the match (loads search params etc)
932
829
  match.__.validate()
933
-
934
- // If this is a preload, add it to the preload cache
935
- if (loaderOpts?.preload && minMaxAge > 0) {
936
- // If the match is currently active, don't preload it
937
- if (router.state.matches.find((d) => d.matchId === match.matchId)) {
938
- return
939
- }
940
-
941
- router.matchCache[match.matchId] = {
942
- gc: now + loaderOpts.gcMaxAge,
943
- match,
944
- }
945
- }
946
-
947
- // If the match is invalid, errored or idle, trigger it to load
948
- if (
949
- (match.status === 'success' && match.getIsInvalid()) ||
950
- match.status === 'error' ||
951
- match.status === 'idle'
952
- ) {
953
- const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
954
-
955
- match.load({ maxAge })
956
- }
830
+ match.load(loaderOpts)
957
831
 
958
832
  if (match.status === 'loading') {
959
833
  // If requested, start the pending timers
960
834
  if (loaderOpts?.withPending) match.__.startPending()
835
+ }
961
836
 
837
+ if (match.__.loadPromise) {
962
838
  // Wait for the first sign of activity from the match
963
839
  // This might be completion, error, or a pending state
964
840
  await match.__.loadPromise
@@ -986,7 +862,7 @@ export function createRouter<
986
862
  },
987
863
 
988
864
  reload: () =>
989
- router._navigate({
865
+ router.__.navigate({
990
866
  fromCurrent: true,
991
867
  replace: true,
992
868
  search: true,
@@ -1024,11 +900,6 @@ export function createRouter<
1024
900
  })
1025
901
  },
1026
902
 
1027
- _navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1028
- const next = router.buildNext(location)
1029
- return router.commitLocation(next, location.replace)
1030
- },
1031
-
1032
903
  navigate: async ({ from, to = '.', search, hash, replace, params }) => {
1033
904
  // If this link simply reloads the current route,
1034
905
  // make sure it has a new key so it will trigger a data refresh
@@ -1050,7 +921,7 @@ export function createRouter<
1050
921
  'Attempting to navigate to external url with router.navigate!',
1051
922
  )
1052
923
 
1053
- return router._navigate({
924
+ return router.__.navigate({
1054
925
  from: fromString,
1055
926
  to: toString,
1056
927
  search,
@@ -1134,7 +1005,7 @@ export function createRouter<
1134
1005
  }
1135
1006
 
1136
1007
  // All is well? Navigate!)
1137
- router._navigate(nextOpts)
1008
+ router.__.navigate(nextOpts)
1138
1009
  }
1139
1010
  }
1140
1011
 
@@ -1186,10 +1057,230 @@ export function createRouter<
1186
1057
  disabled,
1187
1058
  }
1188
1059
  },
1189
- }
1060
+ buildNext: (opts: BuildNextOptions) => {
1061
+ const next = router.__.buildLocation(opts)
1062
+
1063
+ const matches = router.matchRoutes(next.pathname)
1064
+
1065
+ const __preSearchFilters = matches
1066
+ .map((match) => match.options.preSearchFilters ?? [])
1067
+ .flat()
1068
+ .filter(Boolean)
1069
+
1070
+ const __postSearchFilters = matches
1071
+ .map((match) => match.options.postSearchFilters ?? [])
1072
+ .flat()
1073
+ .filter(Boolean)
1074
+
1075
+ return router.__.buildLocation({
1076
+ ...opts,
1077
+ __preSearchFilters,
1078
+ __postSearchFilters,
1079
+ })
1080
+ },
1190
1081
 
1191
- router.location = router.parseLocation(history.location)
1192
- router.state.location = router.location
1082
+ __: {
1083
+ buildRouteTree: (rootRouteConfig: RouteConfig) => {
1084
+ const recurseRoutes = (
1085
+ routeConfigs: RouteConfig[],
1086
+ parent?: Route<TAllRouteInfo, any>,
1087
+ ): Route<TAllRouteInfo, any>[] => {
1088
+ return routeConfigs.map((routeConfig) => {
1089
+ const routeOptions = routeConfig.options
1090
+ const route = createRoute(routeConfig, routeOptions, parent, router)
1091
+ const existingRoute = (router.routesById as any)[route.routeId]
1092
+
1093
+ if (existingRoute) {
1094
+ if (process.env.NODE_ENV !== 'production') {
1095
+ console.warn(
1096
+ `Duplicate routes found with id: ${String(route.routeId)}`,
1097
+ router.routesById,
1098
+ route,
1099
+ )
1100
+ }
1101
+ throw new Error()
1102
+ }
1103
+
1104
+ ;(router.routesById as any)[route.routeId] = route
1105
+
1106
+ const children = routeConfig.children as RouteConfig[]
1107
+
1108
+ route.childRoutes = children?.length
1109
+ ? recurseRoutes(children, route)
1110
+ : undefined
1111
+
1112
+ return route
1113
+ })
1114
+ }
1115
+
1116
+ const routes = recurseRoutes([rootRouteConfig])
1117
+
1118
+ return routes[0]!
1119
+ },
1120
+
1121
+ parseLocation: (
1122
+ location: History['location'],
1123
+ previousLocation?: Location,
1124
+ ): Location => {
1125
+ const parsedSearch = router.options.parseSearch(location.search)
1126
+
1127
+ return {
1128
+ pathname: location.pathname,
1129
+ searchStr: location.search,
1130
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1131
+ hash: location.hash.split('#').reverse()[0] ?? '',
1132
+ href: `${location.pathname}${location.search}${location.hash}`,
1133
+ state: location.state as LocationState,
1134
+ key: location.key,
1135
+ }
1136
+ },
1137
+
1138
+ navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1139
+ const next = router.buildNext(location)
1140
+ return router.__.commitLocation(next, location.replace)
1141
+ },
1142
+
1143
+ buildLocation: (dest: BuildNextOptions = {}): Location => {
1144
+ // const resolvedFrom: Location = {
1145
+ // ...router.location,
1146
+ const fromPathname = dest.fromCurrent
1147
+ ? router.location.pathname
1148
+ : dest.from ?? router.location.pathname
1149
+
1150
+ let pathname = resolvePath(
1151
+ router.basepath ?? '/',
1152
+ fromPathname,
1153
+ `${dest.to ?? '.'}`,
1154
+ )
1155
+
1156
+ const fromMatches = router.matchRoutes(router.location.pathname, {
1157
+ strictParseParams: true,
1158
+ })
1159
+
1160
+ const toMatches = router.matchRoutes(pathname)
1161
+
1162
+ const prevParams = { ...last(fromMatches)?.params }
1163
+
1164
+ let nextParams =
1165
+ (dest.params ?? true) === true
1166
+ ? prevParams
1167
+ : functionalUpdate(dest.params!, prevParams)
1168
+
1169
+ if (nextParams) {
1170
+ toMatches
1171
+ .map((d) => d.options.stringifyParams)
1172
+ .filter(Boolean)
1173
+ .forEach((fn) => {
1174
+ Object.assign({}, nextParams!, fn!(nextParams!))
1175
+ })
1176
+ }
1177
+
1178
+ pathname = interpolatePath(pathname, nextParams ?? {})
1179
+
1180
+ // Pre filters first
1181
+ const preFilteredSearch = dest.__preSearchFilters?.length
1182
+ ? dest.__preSearchFilters.reduce(
1183
+ (prev, next) => next(prev),
1184
+ router.location.search,
1185
+ )
1186
+ : router.location.search
1187
+
1188
+ // Then the link/navigate function
1189
+ const destSearch =
1190
+ dest.search === true
1191
+ ? preFilteredSearch // Preserve resolvedFrom true
1192
+ : dest.search
1193
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1194
+ : dest.__preSearchFilters?.length
1195
+ ? preFilteredSearch // Preserve resolvedFrom filters
1196
+ : {}
1197
+
1198
+ // Then post filters
1199
+ const postFilteredSearch = dest.__postSearchFilters?.length
1200
+ ? dest.__postSearchFilters.reduce(
1201
+ (prev, next) => next(prev),
1202
+ destSearch,
1203
+ )
1204
+ : destSearch
1205
+
1206
+ const search = replaceEqualDeep(
1207
+ router.location.search,
1208
+ postFilteredSearch,
1209
+ )
1210
+
1211
+ const searchStr = router.options.stringifySearch(search)
1212
+ let hash =
1213
+ dest.hash === true
1214
+ ? router.location.hash
1215
+ : functionalUpdate(dest.hash!, router.location.hash)
1216
+ hash = hash ? `#${hash}` : ''
1217
+
1218
+ return {
1219
+ pathname,
1220
+ search,
1221
+ searchStr,
1222
+ state: router.location.state,
1223
+ hash,
1224
+ href: `${pathname}${searchStr}${hash}`,
1225
+ key: dest.key,
1226
+ }
1227
+ },
1228
+
1229
+ commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1230
+ const id = '' + Date.now() + Math.random()
1231
+
1232
+ if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
1233
+
1234
+ let nextAction: 'push' | 'replace' = 'replace'
1235
+
1236
+ if (!replace) {
1237
+ nextAction = 'push'
1238
+ }
1239
+
1240
+ const isSameUrl =
1241
+ router.__.parseLocation(history.location).href === next.href
1242
+
1243
+ if (isSameUrl && !next.key) {
1244
+ nextAction = 'replace'
1245
+ }
1246
+
1247
+ if (nextAction === 'replace') {
1248
+ history.replace(
1249
+ {
1250
+ pathname: next.pathname,
1251
+ hash: next.hash,
1252
+ search: next.searchStr,
1253
+ },
1254
+ {
1255
+ id,
1256
+ },
1257
+ )
1258
+ } else {
1259
+ history.push(
1260
+ {
1261
+ pathname: next.pathname,
1262
+ hash: next.hash,
1263
+ search: next.searchStr,
1264
+ },
1265
+ {
1266
+ id,
1267
+ },
1268
+ )
1269
+ }
1270
+
1271
+ router.navigationPromise = new Promise((resolve) => {
1272
+ const previousNavigationResolve = router.resolveNavigation
1273
+
1274
+ router.resolveNavigation = () => {
1275
+ previousNavigationResolve()
1276
+ resolve()
1277
+ }
1278
+ })
1279
+
1280
+ return router.navigationPromise
1281
+ },
1282
+ },
1283
+ }
1193
1284
 
1194
1285
  router.update(userOptions)
1195
1286
 
@@ -1202,3 +1293,16 @@ export function createRouter<
1202
1293
  function isCtrlEvent(e: MouseEvent) {
1203
1294
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1204
1295
  }
1296
+
1297
+ function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
1298
+ matches.forEach((match, index) => {
1299
+ const parent = matches[index - 1]
1300
+
1301
+ if (parent) {
1302
+ match.loaderData = replaceEqualDeep(match.loaderData, {
1303
+ ...parent.loaderData,
1304
+ ...match.routeLoaderData,
1305
+ })
1306
+ }
1307
+ })
1308
+ }