diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx
index 918fa2b8e08..eca4f92a5cf 100644
--- a/packages/react-router/tests/useNavigate.test.tsx
+++ b/packages/react-router/tests/useNavigate.test.tsx
@@ -2795,3 +2795,59 @@ describe('encoded and unicode paths', () => {
},
)
})
+
+test('when navigating to /auth/sign-in with literal path (no params)', async () => {
+ const rootRoute = createRootRoute()
+
+ const IndexComponent = () => {
+ const navigate = useNavigate()
+ return (
+ <>
+
Index
+
+ >
+ )
+ }
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+ })
+
+ const authRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/auth/$path',
+ component: () => {
+ const params = authRoute.useParams()
+ return (
+
+
Auth Route
+ {params.path}
+
+ )
+ },
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, authRoute]),
+ history,
+ })
+
+ render()
+
+ const btn = await screen.findByTestId('navigate-btn')
+
+ // First click should navigate successfully
+ await act(() => fireEvent.click(btn))
+
+ // Should be at /auth/sign-in with correct params
+ expect(window.location.pathname).toBe('/auth/sign-in')
+ expect(await screen.findByTestId('auth-heading')).toBeInTheDocument()
+ expect((await screen.findByTestId('path-param')).textContent).toBe('sign-in')
+})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 6815bcdf322..f0c20f4170b 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -594,6 +594,12 @@ export interface MatchRoutesOpts {
snapshot?: MatchSnapshot
}
+export interface MatchRoutesResult {
+ matches: Array
+ /** Raw string params extracted from path (before parsing) */
+ rawParams: Record
+}
+
export type InferRouterContext =
TRouteTree['types']['routerContext']
@@ -1265,16 +1271,17 @@ export class RouterCore<
search: locationSearchOrOpts,
} as ParsedLocation,
opts,
- )
+ ).matches
}
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
+ .matches
}
private matchRoutesInternal(
next: ParsedLocation,
opts?: MatchRoutesOpts,
- ): Array {
+ ): MatchRoutesResult {
// Fast-path: use snapshot hint if valid
const snapshot = opts?.snapshot
const snapshotValid =
@@ -1284,6 +1291,7 @@ export class RouterCore<
let matchedRoutes: ReadonlyArray
let routeParams: Record
+ let rawParams: Record
let globalNotFoundRouteId: string | undefined
let parsedParams: Record
@@ -1291,6 +1299,7 @@ export class RouterCore<
// Rebuild matched routes from snapshot
matchedRoutes = snapshot.routeIds.map((id) => this.routesById[id]!)
routeParams = { ...snapshot.params }
+ rawParams = { ...snapshot.params }
globalNotFoundRouteId = snapshot.globalNotFoundRouteId
parsedParams = snapshot.parsedParams
} else {
@@ -1298,6 +1307,7 @@ export class RouterCore<
const matchedRoutesResult = this.getMatchedRoutes(next.pathname)
const { foundRoute, routeParams: rp } = matchedRoutesResult
routeParams = rp
+ rawParams = { ...rp } // Capture before routeParams gets modified
matchedRoutes = matchedRoutesResult.matchedRoutes
parsedParams = matchedRoutesResult.parsedParams
@@ -1642,7 +1652,7 @@ export class RouterCore<
}
})
- return matches
+ return { matches, rawParams }
}
getMatchedRoutes: GetMatchRoutesFn = (pathname) => {
@@ -1765,9 +1775,10 @@ export class RouterCore<
params: nextParams,
}).interpolatedPath
- const destMatches = this.matchRoutes(interpolatedNextTo, undefined, {
- _buildLocation: true,
- })
+ const { matches: destMatches, rawParams } = this.matchRoutesInternal(
+ { pathname: interpolatedNextTo } as ParsedLocation,
+ { _buildLocation: true },
+ )
const destRoutes = destMatches.map(
(d) => this.looseRoutesById[d.routeId]!,
)
@@ -1856,10 +1867,15 @@ export class RouterCore<
nextState = replaceEqualDeep(currentLocation.state, nextState)
// Build match snapshot for fast-path on back/forward navigation
- // Use destRoutes and nextParams directly (after stringify)
+ // Use raw params captured during matchRoutesInternal (needed for literal path navigation
+ // where nextParams may be empty but path contains param values)
+ const snapshotParams = {
+ ...rawParams,
+ ...nextParams,
+ }
const matchSnapshot = buildMatchSnapshotFromRoutes({
routes: destRoutes,
- params: nextParams,
+ params: snapshotParams,
searchStr,
globalNotFoundRouteId: globalNotFoundMatch?.routeId,
})