@tanstack/react-router 1.97.26 → 1.98.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/cjs/Match.cjs +78 -28
  2. package/dist/cjs/Match.cjs.map +1 -1
  3. package/dist/cjs/Matches.cjs +4 -1
  4. package/dist/cjs/Matches.cjs.map +1 -1
  5. package/dist/cjs/Matches.d.cts +2 -0
  6. package/dist/cjs/ScriptOnce.cjs +1 -1
  7. package/dist/cjs/ScriptOnce.cjs.map +1 -1
  8. package/dist/cjs/ScrollRestoration.cjs +39 -0
  9. package/dist/cjs/ScrollRestoration.cjs.map +1 -0
  10. package/dist/cjs/ScrollRestoration.d.cts +15 -0
  11. package/dist/cjs/Transitioner.cjs +3 -33
  12. package/dist/cjs/Transitioner.cjs.map +1 -1
  13. package/dist/cjs/index.cjs +3 -4
  14. package/dist/cjs/index.cjs.map +1 -1
  15. package/dist/cjs/index.d.cts +2 -3
  16. package/dist/cjs/route.cjs.map +1 -1
  17. package/dist/cjs/route.d.cts +10 -2
  18. package/dist/cjs/router.cjs +37 -23
  19. package/dist/cjs/router.cjs.map +1 -1
  20. package/dist/cjs/router.d.cts +28 -27
  21. package/dist/cjs/scroll-restoration.cjs +168 -165
  22. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  23. package/dist/cjs/scroll-restoration.d.cts +25 -15
  24. package/dist/esm/Match.js +80 -30
  25. package/dist/esm/Match.js.map +1 -1
  26. package/dist/esm/Matches.d.ts +2 -0
  27. package/dist/esm/Matches.js +4 -1
  28. package/dist/esm/Matches.js.map +1 -1
  29. package/dist/esm/ScriptOnce.js +1 -1
  30. package/dist/esm/ScriptOnce.js.map +1 -1
  31. package/dist/esm/ScrollRestoration.d.ts +15 -0
  32. package/dist/esm/ScrollRestoration.js +39 -0
  33. package/dist/esm/ScrollRestoration.js.map +1 -0
  34. package/dist/esm/Transitioner.js +4 -34
  35. package/dist/esm/Transitioner.js.map +1 -1
  36. package/dist/esm/index.d.ts +2 -3
  37. package/dist/esm/index.js +1 -2
  38. package/dist/esm/route.d.ts +10 -2
  39. package/dist/esm/route.js.map +1 -1
  40. package/dist/esm/router.d.ts +28 -27
  41. package/dist/esm/router.js +38 -24
  42. package/dist/esm/router.js.map +1 -1
  43. package/dist/esm/scroll-restoration.d.ts +25 -15
  44. package/dist/esm/scroll-restoration.js +168 -148
  45. package/dist/esm/scroll-restoration.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/Match.tsx +101 -54
  48. package/src/Matches.tsx +3 -1
  49. package/src/ScriptOnce.tsx +1 -1
  50. package/src/ScrollRestoration.tsx +65 -0
  51. package/src/Transitioner.tsx +4 -40
  52. package/src/index.tsx +3 -3
  53. package/src/route.ts +38 -1
  54. package/src/router.ts +75 -49
  55. package/src/scroll-restoration.tsx +271 -183
package/src/router.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  defaultParseSearch,
13
13
  defaultStringifySearch,
14
14
  functionalUpdate,
15
+ getLocationChangeInfo,
15
16
  interpolatePath,
16
17
  joinPaths,
17
18
  last,
@@ -27,6 +28,8 @@ import {
27
28
  } from '@tanstack/router-core'
28
29
  import { isRedirect, isResolvedRedirect } from './redirects'
29
30
  import { isNotFound } from './not-found'
31
+
32
+ import { setupScrollRestoration } from './scroll-restoration'
30
33
  import type * as React from 'react'
31
34
  import type {
32
35
  HistoryLocation,
@@ -61,6 +64,7 @@ import type {
61
64
  BeforeLoadContextOptions,
62
65
  ErrorRouteComponent,
63
66
  LoaderFnContext,
67
+ MakeRemountDepsOptionsUnion,
64
68
  NotFoundRouteComponent,
65
69
  RootRoute,
66
70
  RouteComponent,
@@ -451,6 +455,17 @@ export interface RouterOptions<
451
455
  pathParamsAllowedCharacters?: Array<
452
456
  ';' | ':' | '@' | '&' | '=' | '+' | '$' | ','
453
457
  >
458
+
459
+ defaultRemountDeps?: (opts: MakeRemountDepsOptionsUnion<TRouteTree>) => any
460
+
461
+ /**
462
+ * If `false`, scroll restoration will be disabled
463
+ *
464
+ * @default true
465
+ */
466
+ scrollRestoration?: boolean
467
+ getScrollRestorationKey?: (location: ParsedLocation) => string
468
+ scrollRestorationBehavior?: ScrollBehavior
454
469
  }
455
470
 
456
471
  export interface RouterErrorSerializer<TSerializedError> {
@@ -470,7 +485,7 @@ export interface RouterState<
470
485
  pendingMatches?: Array<TRouteMatch>
471
486
  cachedMatches: Array<TRouteMatch>
472
487
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
473
- resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
488
+ resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
474
489
  statusCode: number
475
490
  redirect?: ResolvedRedirect
476
491
  }
@@ -563,46 +578,37 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
563
578
  return {}
564
579
  }
565
580
 
581
+ type NavigationEventInfo = {
582
+ fromLocation?: ParsedLocation
583
+ toLocation: ParsedLocation
584
+ pathChanged: boolean
585
+ hrefChanged: boolean
586
+ hashChanged: boolean
587
+ }
588
+
566
589
  export type RouterEvents = {
567
590
  onBeforeNavigate: {
568
591
  type: 'onBeforeNavigate'
569
- fromLocation: ParsedLocation
570
- toLocation: ParsedLocation
571
- pathChanged: boolean
572
- hrefChanged: boolean
573
- }
592
+ } & NavigationEventInfo
574
593
  onBeforeLoad: {
575
594
  type: 'onBeforeLoad'
576
- fromLocation: ParsedLocation
577
- toLocation: ParsedLocation
578
- pathChanged: boolean
579
- hrefChanged: boolean
580
- }
595
+ } & NavigationEventInfo
581
596
  onLoad: {
582
597
  type: 'onLoad'
583
- fromLocation: ParsedLocation
584
- toLocation: ParsedLocation
585
- pathChanged: boolean
586
- hrefChanged: boolean
587
- }
598
+ } & NavigationEventInfo
588
599
  onResolved: {
589
600
  type: 'onResolved'
590
- fromLocation: ParsedLocation
591
- toLocation: ParsedLocation
592
- pathChanged: boolean
593
- hrefChanged: boolean
594
- }
601
+ } & NavigationEventInfo
595
602
  onBeforeRouteMount: {
596
603
  type: 'onBeforeRouteMount'
597
- fromLocation: ParsedLocation
598
- toLocation: ParsedLocation
599
- pathChanged: boolean
600
- hrefChanged: boolean
601
- }
604
+ } & NavigationEventInfo
602
605
  onInjectedHtml: {
603
606
  type: 'onInjectedHtml'
604
607
  promise: Promise<string>
605
608
  }
609
+ onRendered: {
610
+ type: 'onRendered'
611
+ } & NavigationEventInfo
606
612
  }
607
613
 
608
614
  export type RouterEvent = RouterEvents[keyof RouterEvents]
@@ -664,6 +670,8 @@ export class Router<
664
670
  isViewTransitionTypesSupported?: boolean = undefined
665
671
  subscribers = new Set<RouterListener<RouterEvent>>()
666
672
  viewTransitionPromise?: ControlledPromise<true>
673
+ isScrollRestoring = false
674
+ isScrollRestorationSetup = false
667
675
 
668
676
  // Must build in constructor
669
677
  __store!: Store<RouterState<TRouteTree>>
@@ -800,6 +808,8 @@ export class Router<
800
808
  }
801
809
  },
802
810
  })
811
+
812
+ setupScrollRestoration(this)
803
813
  }
804
814
 
805
815
  if (
@@ -1151,19 +1161,26 @@ export class Router<
1151
1161
 
1152
1162
  const parentMatch = matches[index - 1]
1153
1163
 
1154
- const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
1164
+ const [preMatchSearch, strictMatchSearch, searchError]: [
1165
+ Record<string, any>,
1166
+ Record<string, any>,
1167
+ any,
1168
+ ] = (() => {
1155
1169
  // Validate the search params and stabilize them
1156
1170
  const parentSearch = parentMatch?.search ?? next.search
1171
+ const parentStrictSearch = parentMatch?._strictSearch ?? {}
1157
1172
 
1158
1173
  try {
1159
- const search =
1160
- validateSearch(route.options.validateSearch, parentSearch) ?? {}
1174
+ const strictSearch =
1175
+ validateSearch(route.options.validateSearch, { ...parentSearch }) ??
1176
+ {}
1161
1177
 
1162
1178
  return [
1163
1179
  {
1164
1180
  ...parentSearch,
1165
- ...search,
1181
+ ...strictSearch,
1166
1182
  },
1183
+ { ...parentStrictSearch, ...strictSearch },
1167
1184
  undefined,
1168
1185
  ]
1169
1186
  } catch (err: any) {
@@ -1178,7 +1195,7 @@ export class Router<
1178
1195
  throw searchParamError
1179
1196
  }
1180
1197
 
1181
- return [parentSearch, searchParamError]
1198
+ return [parentSearch, {}, searchParamError]
1182
1199
  }
1183
1200
  })()
1184
1201
 
@@ -1194,7 +1211,7 @@ export class Router<
1194
1211
 
1195
1212
  const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
1196
1213
 
1197
- const interpolatedPath = interpolatePath({
1214
+ const { usedParams, interpolatedPath } = interpolatePath({
1198
1215
  path: route.fullPath,
1199
1216
  params: routeParams,
1200
1217
  decodeCharMap: this.pathParamsDecodeCharMap,
@@ -1206,7 +1223,7 @@ export class Router<
1206
1223
  params: routeParams,
1207
1224
  leaveWildcards: true,
1208
1225
  decodeCharMap: this.pathParamsDecodeCharMap,
1209
- }) + loaderDepsHash
1226
+ }).interpolatedPath + loaderDepsHash
1210
1227
 
1211
1228
  // Waste not, want not. If we already have a match for this route,
1212
1229
  // reuse it. This is important for layout routes, which might stick
@@ -1231,9 +1248,11 @@ export class Router<
1231
1248
  params: previousMatch
1232
1249
  ? replaceEqualDeep(previousMatch.params, routeParams)
1233
1250
  : routeParams,
1251
+ _strictParams: usedParams,
1234
1252
  search: previousMatch
1235
1253
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1236
1254
  : replaceEqualDeep(existingMatch.search, preMatchSearch),
1255
+ _strictSearch: strictMatchSearch,
1237
1256
  }
1238
1257
  } else {
1239
1258
  const status =
@@ -1251,11 +1270,13 @@ export class Router<
1251
1270
  params: previousMatch
1252
1271
  ? replaceEqualDeep(previousMatch.params, routeParams)
1253
1272
  : routeParams,
1273
+ _strictParams: usedParams,
1254
1274
  pathname: joinPaths([this.basepath, interpolatedPath]),
1255
1275
  updatedAt: Date.now(),
1256
1276
  search: previousMatch
1257
1277
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1258
1278
  : preMatchSearch,
1279
+ _strictSearch: strictMatchSearch,
1259
1280
  searchError: undefined,
1260
1281
  status,
1261
1282
  isFetching: false,
@@ -1463,7 +1484,7 @@ export class Router<
1463
1484
  path: route.fullPath,
1464
1485
  params: matchedRoutesResult?.routeParams ?? {},
1465
1486
  decodeCharMap: this.pathParamsDecodeCharMap,
1466
- })
1487
+ }).interpolatedPath
1467
1488
  const pathname = joinPaths([this.basepath, interpolatedPath])
1468
1489
  return pathname === fromPath
1469
1490
  })?.id as keyof this['routesById']
@@ -1503,7 +1524,7 @@ export class Router<
1503
1524
  leaveWildcards: false,
1504
1525
  leaveParams: opts.leaveParams,
1505
1526
  decodeCharMap: this.pathParamsDecodeCharMap,
1506
- })
1527
+ }).interpolatedPath
1507
1528
 
1508
1529
  let search = fromSearch
1509
1530
  if (opts._includeValidateSearch && this.options.search?.strict) {
@@ -1867,8 +1888,6 @@ export class Router<
1867
1888
  try {
1868
1889
  const next = this.latestLocation
1869
1890
  const prevLocation = this.state.resolvedLocation
1870
- const hrefChanged = prevLocation.href !== next.href
1871
- const pathChanged = prevLocation.pathname !== next.pathname
1872
1891
 
1873
1892
  // Cancel any pending matches
1874
1893
  this.cancelMatches()
@@ -1900,19 +1919,19 @@ export class Router<
1900
1919
  if (!this.state.redirect) {
1901
1920
  this.emit({
1902
1921
  type: 'onBeforeNavigate',
1903
- fromLocation: prevLocation,
1904
- toLocation: next,
1905
- pathChanged,
1906
- hrefChanged,
1922
+ ...getLocationChangeInfo({
1923
+ resolvedLocation: prevLocation,
1924
+ location: next,
1925
+ }),
1907
1926
  })
1908
1927
  }
1909
1928
 
1910
1929
  this.emit({
1911
1930
  type: 'onBeforeLoad',
1912
- fromLocation: prevLocation,
1913
- toLocation: next,
1914
- pathChanged,
1915
- hrefChanged,
1931
+ ...getLocationChangeInfo({
1932
+ resolvedLocation: prevLocation,
1933
+ location: next,
1934
+ }),
1916
1935
  })
1917
1936
 
1918
1937
  await this.loadMatches({
@@ -2173,6 +2192,10 @@ export class Router<
2173
2192
  this._handleNotFound(matches, err, {
2174
2193
  updateMatch,
2175
2194
  })
2195
+ this.serverSsr?.onMatchSettled({
2196
+ router: this,
2197
+ match: this.getMatch(match.id)!,
2198
+ })
2176
2199
  throw err
2177
2200
  }
2178
2201
  }
@@ -2637,6 +2660,7 @@ export class Router<
2637
2660
  if (isNotFound(err) && !allPreload) {
2638
2661
  await triggerOnReady()
2639
2662
  }
2663
+
2640
2664
  throw err
2641
2665
  }
2642
2666
  }
@@ -2835,8 +2859,10 @@ export class Router<
2835
2859
  _fromLocation: next,
2836
2860
  })
2837
2861
  }
2838
- // Preload errors are not fatal, but we should still log them
2839
- console.error(err)
2862
+ if (!isNotFound(err)) {
2863
+ // Preload errors are not fatal, but we should still log them
2864
+ console.error(err)
2865
+ }
2840
2866
  return undefined
2841
2867
  }
2842
2868
  }
@@ -2882,7 +2908,7 @@ export class Router<
2882
2908
 
2883
2909
  const baseLocation = pending
2884
2910
  ? this.latestLocation
2885
- : this.state.resolvedLocation
2911
+ : this.state.resolvedLocation || this.state.location
2886
2912
 
2887
2913
  const match = matchPathname(this.basepath, baseLocation.pathname, {
2888
2914
  ...opts,
@@ -3020,7 +3046,7 @@ export function getInitialRouterState(
3020
3046
  isLoading: false,
3021
3047
  isTransitioning: false,
3022
3048
  status: 'idle',
3023
- resolvedLocation: { ...location },
3049
+ resolvedLocation: undefined,
3024
3050
  location,
3025
3051
  matches: [],
3026
3052
  pendingMatches: [],