@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/dist/cjs/Match.cjs +18 -6
- package/dist/cjs/Match.cjs.map +1 -1
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +2 -0
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/lazyRouteComponent.cjs +17 -13
- package/dist/cjs/lazyRouteComponent.cjs.map +1 -1
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +10 -2
- package/dist/cjs/router.cjs +23 -11
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +2 -1
- package/dist/esm/Match.js +18 -6
- package/dist/esm/Match.js.map +1 -1
- package/dist/esm/Matches.d.ts +2 -0
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/lazyRouteComponent.js +17 -13
- package/dist/esm/lazyRouteComponent.js.map +1 -1
- package/dist/esm/route.d.ts +10 -2
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +2 -1
- package/dist/esm/router.js +23 -11
- package/dist/esm/router.js.map +1 -1
- package/package.json +3 -3
- package/src/Match.tsx +22 -6
- package/src/Matches.tsx +2 -0
- package/src/index.tsx +2 -0
- package/src/lazyRouteComponent.tsx +34 -31
- package/src/route.ts +38 -1
- package/src/router.ts +32 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-router",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
57
|
-
"@tanstack/router-core": "^1.
|
|
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,
|
|
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
|
-
|
|
136
|
-
|
|
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
|
@@ -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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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]: [
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
2839
|
-
|
|
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
|
}
|