@tanstack/react-router 1.97.25 → 1.98.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.97.25",
3
+ "version": "1.98.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -53,8 +53,8 @@
53
53
  "jsesc": "^3.1.0",
54
54
  "tiny-invariant": "^1.3.3",
55
55
  "tiny-warning": "^1.0.3",
56
- "@tanstack/history": "1.97.8",
57
- "@tanstack/router-core": "^1.97.25"
56
+ "@tanstack/history": "1.98.0",
57
+ "@tanstack/router-core": "^1.98.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@testing-library/jest-dom": "^6.6.3",
package/src/Match.tsx CHANGED
@@ -114,26 +114,41 @@ export const MatchInner = React.memo(function MatchInnerImpl({
114
114
  }): any {
115
115
  const router = useRouter()
116
116
 
117
- const { match, matchIndex, routeId } = useRouterState({
117
+ const { match, key, routeId } = useRouterState({
118
118
  select: (s) => {
119
119
  const matchIndex = s.matches.findIndex((d) => d.id === matchId)
120
120
  const match = s.matches[matchIndex]!
121
121
  const routeId = match.routeId as string
122
+
123
+ const remountFn =
124
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
125
+ router.options.defaultRemountDeps
126
+ const remountDeps = remountFn?.({
127
+ routeId,
128
+ loaderDeps: match.loaderDeps,
129
+ params: match._strictParams,
130
+ search: match._strictSearch,
131
+ })
132
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
133
+
122
134
  return {
135
+ key,
123
136
  routeId,
124
- matchIndex,
125
137
  match: pick(match, ['id', 'status', 'error']),
126
138
  }
127
139
  },
128
140
  structuralSharing: true as any,
129
141
  })
130
142
 
131
- const route = router.routesById[routeId]!
143
+ const route = router.routesById[routeId] as AnyRoute
132
144
 
133
145
  const out = React.useMemo(() => {
134
146
  const Comp = route.options.component ?? router.options.defaultComponent
135
- return Comp ? <Comp /> : <Outlet />
136
- }, [route.options.component, router.options.defaultComponent])
147
+ if (Comp) {
148
+ return <Comp key={key} />
149
+ }
150
+ return <Outlet />
151
+ }, [key, route.options.component, router.options.defaultComponent])
137
152
 
138
153
  // function useChangedDiff(value: any) {
139
154
  // const ref = React.useRef(value)
@@ -184,7 +199,8 @@ export const MatchInner = React.memo(function MatchInnerImpl({
184
199
  if (router.isServer) {
185
200
  return (
186
201
  <RouteErrorComponent
187
- error={match.error}
202
+ error={match.error as any}
203
+ reset={undefined as any}
188
204
  info={{
189
205
  componentStack: '',
190
206
  }}
package/src/Matches.tsx CHANGED
@@ -63,6 +63,7 @@ export interface RouteMatch<
63
63
  index: number
64
64
  pathname: string
65
65
  params: TAllParams
66
+ _strictParams: TAllParams
66
67
  status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
67
68
  isFetching: false | 'beforeLoad' | 'loader'
68
69
  error: unknown
@@ -77,6 +78,7 @@ export interface RouteMatch<
77
78
  __beforeLoadContext: Record<string, unknown>
78
79
  context: TAllContext
79
80
  search: TFullSearchSchema
81
+ _strictSearch: TFullSearchSchema
80
82
  fetchCount: number
81
83
  abortController: AbortController
82
84
  cause: 'preload' | 'enter' | 'stay'
package/src/index.tsx CHANGED
@@ -259,6 +259,8 @@ export type {
259
259
  BeforeLoadContextParameter,
260
260
  ResolveAllContext,
261
261
  ResolveAllParamsFromParent,
262
+ MakeRemountDepsOptionsUnion,
263
+ RemountDepsOptions,
262
264
  } from './route'
263
265
 
264
266
  export type {
@@ -8,9 +8,14 @@ import type { AsyncRouteComponent } from './route'
8
8
  // URL to the lazy module.
9
9
  // In that case, we want to attempt one window refresh to get the latest.
10
10
  function isModuleNotFoundError(error: any): boolean {
11
+ // chrome: "Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
12
+ // firefox: "error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
13
+ // safari: "Importing a module script failed."
14
+ if (typeof error?.message !== 'string') return false
11
15
  return (
12
- typeof error?.message === 'string' &&
13
- /Failed to fetch dynamically imported module/.test(error.message)
16
+ error.message.startsWith('Failed to fetch dynamically imported module') ||
17
+ error.message.startsWith('error loading dynamically imported module') ||
18
+ error.message.startsWith('Importing a module script failed')
14
19
  )
15
20
  }
16
21
 
@@ -46,6 +51,7 @@ export function lazyRouteComponent<
46
51
  let loadPromise: Promise<any> | undefined
47
52
  let comp: T[TKey] | T['default']
48
53
  let error: any
54
+ let reload: boolean
49
55
 
50
56
  const load = () => {
51
57
  if (typeof document === 'undefined' && ssr?.() === false) {
@@ -59,7 +65,27 @@ export function lazyRouteComponent<
59
65
  comp = res[exportName ?? 'default']
60
66
  })
61
67
  .catch((err) => {
68
+ // We don't want an error thrown from preload in this case, because
69
+ // there's nothing we want to do about module not found during preload.
70
+ // Record the error, the rest is handled during the render path.
62
71
  error = err
72
+ if (isModuleNotFoundError(error)) {
73
+ if (
74
+ error instanceof Error &&
75
+ typeof window !== 'undefined' &&
76
+ typeof sessionStorage !== 'undefined'
77
+ ) {
78
+ // Again, we want to reload one time on module not found error and not enter
79
+ // a reload loop if there is some other issue besides an old deploy.
80
+ // That's why we store our reload attempt in sessionStorage.
81
+ // Use error.message as key because it contains the module path that failed.
82
+ const storageKey = `tanstack_router_reload:${error.message}`
83
+ if (!sessionStorage.getItem(storageKey)) {
84
+ sessionStorage.setItem(storageKey, '1')
85
+ reload = true
86
+ }
87
+ }
88
+ }
63
89
  })
64
90
  }
65
91
 
@@ -68,36 +94,13 @@ export function lazyRouteComponent<
68
94
 
69
95
  const lazyComp = function Lazy(props: any) {
70
96
  // Now that we're out of preload and into actual render path,
71
- // throw the error if it was a module not found error during preload
97
+ if (reload) {
98
+ // If it was a module loading error,
99
+ // throw eternal suspense while we wait for window to reload
100
+ window.location.reload()
101
+ throw new Promise(() => {})
102
+ }
72
103
  if (error) {
73
- if (isModuleNotFoundError(error)) {
74
- // We don't want an error thrown from preload in this case, because
75
- // there's nothing we want to do about module not found during preload.
76
- // Record the error, recover the promise with a null return,
77
- // and we will attempt module not found resolution during the render path.
78
-
79
- if (
80
- error instanceof Error &&
81
- typeof window !== 'undefined' &&
82
- typeof sessionStorage !== 'undefined'
83
- ) {
84
- // Again, we want to reload one time on module not found error and not enter
85
- // a reload loop if there is some other issue besides an old deploy.
86
- // That's why we store our reload attempt in sessionStorage.
87
- // Use error.message as key because it contains the module path that failed.
88
- const storageKey = `tanstack_router_reload:${error.message}`
89
- if (!sessionStorage.getItem(storageKey)) {
90
- sessionStorage.setItem(storageKey, '1')
91
- window.location.reload()
92
-
93
- // Return empty component while we wait for window to reload
94
- return {
95
- default: () => null,
96
- }
97
- }
98
- }
99
- }
100
-
101
104
  // Otherwise, just throw the error
102
105
  throw error
103
106
  }
package/src/route.ts CHANGED
@@ -59,7 +59,7 @@ import type {
59
59
  RouteMatch,
60
60
  } from './Matches'
61
61
  import type { NavigateOptions, ToMaskOptions } from './link'
62
- import type { RouteById, RouteIds, RoutePaths } from './routeInfo'
62
+ import type { ParseRoute, RouteById, RouteIds, RoutePaths } from './routeInfo'
63
63
  import type { AnyRouter, RegisteredRouter, Router } from './router'
64
64
  import type { BuildLocationFn, NavigateFn } from './RouterProvider'
65
65
  import type { NotFoundError } from './not-found'
@@ -154,6 +154,7 @@ export type FileBaseRouteOptions<
154
154
  TRouterContext = {},
155
155
  TRouteContextFn = AnyContext,
156
156
  TBeforeLoadFn = AnyContext,
157
+ TRemountDepsFn = AnyContext,
157
158
  > = ParamsOptions<TPath, TParams> & {
158
159
  validateSearch?: Constrain<TSearchValidator, AnyValidator, DefaultValidator>
159
160
 
@@ -204,6 +205,18 @@ export type FileBaseRouteOptions<
204
205
  opts: FullSearchSchemaOption<TParentRoute, TSearchValidator>,
205
206
  ) => TLoaderDeps
206
207
 
208
+ remountDeps?: Constrain<
209
+ TRemountDepsFn,
210
+ (
211
+ opt: RemountDepsOptions<
212
+ TId,
213
+ FullSearchSchemaOption<TParentRoute, TSearchValidator>,
214
+ Expand<ResolveAllParamsFromParent<TParentRoute, TParams>>,
215
+ TLoaderDeps
216
+ >,
217
+ ) => any
218
+ >
219
+
207
220
  loader?: Constrain<
208
221
  TLoaderFn,
209
222
  (
@@ -275,6 +288,30 @@ export interface RouteContextOptions<
275
288
  context: Expand<RouteContextParameter<TParentRoute, TRouterContext>>
276
289
  }
277
290
 
291
+ export interface RemountDepsOptions<
292
+ in out TRouteId,
293
+ in out TFullSearchSchema,
294
+ in out TAllParams,
295
+ in out TLoaderDeps,
296
+ > {
297
+ routeId: TRouteId
298
+ search: TFullSearchSchema
299
+ params: TAllParams
300
+ loaderDeps: TLoaderDeps
301
+ }
302
+
303
+ export type MakeRemountDepsOptionsUnion<
304
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
305
+ TRoute extends AnyRoute = ParseRoute<TRouteTree>,
306
+ > = TRoute extends any
307
+ ? RemountDepsOptions<
308
+ TRoute['id'],
309
+ TRoute['types']['fullSearchSchema'],
310
+ TRoute['types']['allParams'],
311
+ TRoute['types']['loaderDeps']
312
+ >
313
+ : never
314
+
278
315
  export interface BeforeLoadContextOptions<
279
316
  in out TParentRoute extends AnyRoute,
280
317
  in out TSearchValidator,
package/src/router.ts CHANGED
@@ -61,6 +61,7 @@ import type {
61
61
  BeforeLoadContextOptions,
62
62
  ErrorRouteComponent,
63
63
  LoaderFnContext,
64
+ MakeRemountDepsOptionsUnion,
64
65
  NotFoundRouteComponent,
65
66
  RootRoute,
66
67
  RouteComponent,
@@ -451,6 +452,8 @@ export interface RouterOptions<
451
452
  pathParamsAllowedCharacters?: Array<
452
453
  ';' | ':' | '@' | '&' | '=' | '+' | '$' | ','
453
454
  >
455
+
456
+ defaultRemountDeps?: (opts: MakeRemountDepsOptionsUnion<TRouteTree>) => any
454
457
  }
455
458
 
456
459
  export interface RouterErrorSerializer<TSerializedError> {
@@ -1151,19 +1154,26 @@ export class Router<
1151
1154
 
1152
1155
  const parentMatch = matches[index - 1]
1153
1156
 
1154
- const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
1157
+ const [preMatchSearch, strictMatchSearch, searchError]: [
1158
+ Record<string, any>,
1159
+ Record<string, any>,
1160
+ any,
1161
+ ] = (() => {
1155
1162
  // Validate the search params and stabilize them
1156
1163
  const parentSearch = parentMatch?.search ?? next.search
1164
+ const parentStrictSearch = parentMatch?._strictSearch ?? {}
1157
1165
 
1158
1166
  try {
1159
- const search =
1160
- validateSearch(route.options.validateSearch, parentSearch) ?? {}
1167
+ const strictSearch =
1168
+ validateSearch(route.options.validateSearch, { ...parentSearch }) ??
1169
+ {}
1161
1170
 
1162
1171
  return [
1163
1172
  {
1164
1173
  ...parentSearch,
1165
- ...search,
1174
+ ...strictSearch,
1166
1175
  },
1176
+ { ...parentStrictSearch, ...strictSearch },
1167
1177
  undefined,
1168
1178
  ]
1169
1179
  } catch (err: any) {
@@ -1178,7 +1188,7 @@ export class Router<
1178
1188
  throw searchParamError
1179
1189
  }
1180
1190
 
1181
- return [parentSearch, searchParamError]
1191
+ return [parentSearch, {}, searchParamError]
1182
1192
  }
1183
1193
  })()
1184
1194
 
@@ -1194,7 +1204,7 @@ export class Router<
1194
1204
 
1195
1205
  const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
1196
1206
 
1197
- const interpolatedPath = interpolatePath({
1207
+ const { usedParams, interpolatedPath } = interpolatePath({
1198
1208
  path: route.fullPath,
1199
1209
  params: routeParams,
1200
1210
  decodeCharMap: this.pathParamsDecodeCharMap,
@@ -1206,7 +1216,7 @@ export class Router<
1206
1216
  params: routeParams,
1207
1217
  leaveWildcards: true,
1208
1218
  decodeCharMap: this.pathParamsDecodeCharMap,
1209
- }) + loaderDepsHash
1219
+ }).interpolatedPath + loaderDepsHash
1210
1220
 
1211
1221
  // Waste not, want not. If we already have a match for this route,
1212
1222
  // reuse it. This is important for layout routes, which might stick
@@ -1231,9 +1241,11 @@ export class Router<
1231
1241
  params: previousMatch
1232
1242
  ? replaceEqualDeep(previousMatch.params, routeParams)
1233
1243
  : routeParams,
1244
+ _strictParams: usedParams,
1234
1245
  search: previousMatch
1235
1246
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1236
1247
  : replaceEqualDeep(existingMatch.search, preMatchSearch),
1248
+ _strictSearch: strictMatchSearch,
1237
1249
  }
1238
1250
  } else {
1239
1251
  const status =
@@ -1251,11 +1263,13 @@ export class Router<
1251
1263
  params: previousMatch
1252
1264
  ? replaceEqualDeep(previousMatch.params, routeParams)
1253
1265
  : routeParams,
1266
+ _strictParams: usedParams,
1254
1267
  pathname: joinPaths([this.basepath, interpolatedPath]),
1255
1268
  updatedAt: Date.now(),
1256
1269
  search: previousMatch
1257
1270
  ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1258
1271
  : preMatchSearch,
1272
+ _strictSearch: strictMatchSearch,
1259
1273
  searchError: undefined,
1260
1274
  status,
1261
1275
  isFetching: false,
@@ -1463,7 +1477,7 @@ export class Router<
1463
1477
  path: route.fullPath,
1464
1478
  params: matchedRoutesResult?.routeParams ?? {},
1465
1479
  decodeCharMap: this.pathParamsDecodeCharMap,
1466
- })
1480
+ }).interpolatedPath
1467
1481
  const pathname = joinPaths([this.basepath, interpolatedPath])
1468
1482
  return pathname === fromPath
1469
1483
  })?.id as keyof this['routesById']
@@ -1503,7 +1517,7 @@ export class Router<
1503
1517
  leaveWildcards: false,
1504
1518
  leaveParams: opts.leaveParams,
1505
1519
  decodeCharMap: this.pathParamsDecodeCharMap,
1506
- })
1520
+ }).interpolatedPath
1507
1521
 
1508
1522
  let search = fromSearch
1509
1523
  if (opts._includeValidateSearch && this.options.search?.strict) {
@@ -2173,6 +2187,10 @@ export class Router<
2173
2187
  this._handleNotFound(matches, err, {
2174
2188
  updateMatch,
2175
2189
  })
2190
+ this.serverSsr?.onMatchSettled({
2191
+ router: this,
2192
+ match: this.getMatch(match.id)!,
2193
+ })
2176
2194
  throw err
2177
2195
  }
2178
2196
  }
@@ -2637,6 +2655,7 @@ export class Router<
2637
2655
  if (isNotFound(err) && !allPreload) {
2638
2656
  await triggerOnReady()
2639
2657
  }
2658
+
2640
2659
  throw err
2641
2660
  }
2642
2661
  }
@@ -2835,8 +2854,10 @@ export class Router<
2835
2854
  _fromLocation: next,
2836
2855
  })
2837
2856
  }
2838
- // Preload errors are not fatal, but we should still log them
2839
- console.error(err)
2857
+ if (!isNotFound(err)) {
2858
+ // Preload errors are not fatal, but we should still log them
2859
+ console.error(err)
2860
+ }
2840
2861
  return undefined
2841
2862
  }
2842
2863
  }