Link to a server-only route skips parent params.stringify on the client
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Start and Router
Describe the bug
When a parent route uses params.parse and params.stringify, a <Link reloadDocument> to a server-only child route can generate different hrefs during SSR and hydration.
The server applies the parent params.stringify, while the client does not.
Reproduction
Create a parent route that maps the public articles segment to the internal article value:
// routes/$postType/route.tsx
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/$postType')({
params: {
parse: ({ postType }) =>
postType === 'articles'
? { postType: 'article' as const }
: false,
stringify: ({ postType }) => ({
postType: postType === 'article' ? 'articles' : postType,
}),
},
component: Outlet,
})
Render a link from its index route:
// routes/$postType/index.tsx
import { Link, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/$postType/')({
component: IndexComponent,
})
function IndexComponent() {
return (
<Link
to="/$postType/$postId/download"
params={{
postType: 'article',
postId: '1',
}}
reloadDocument
>
Download
</Link>
)
}
Define the destination as a server-only route:
// routes/$postType/$postId/download.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute(
'/$postType/$postId/download',
)({
server: {
handlers: {
GET: () => new Response('file'),
},
},
})
Then:
- Start the application.
- Visit
/articles. - Check the hydration warning.
- Click the download link.
Actual behavior
SSR renders:
/articles/1/download
During hydration, the client generates:
/article/1/download
React reports an href hydration mismatch.
The DOM may continue to show /articles/1/download, but clicking the Link uses the client-generated /article/1/download, resulting in a 404.
The server-only route is available in the generated route types, so the Link passes TypeScript validation. However, it is absent from the client runtime route tree, so client-side URL generation does not apply the parent params.stringify.
This was discovered in the same application as #7731, but appears to be a separate issue involving server-only route pruning.
Expected behavior
SSR and client-side URL generation should produce the same canonical href:
/articles/1/download
A type-safe Link to a server-only route should apply the same parent params.stringify chain on both sides.
If linking to server-only routes is intentionally unsupported, the generated route types should prevent this usage or the limitation should be documented.
Current workaround
Adding an empty component keeps the destination in the client route tree:
export const Route = createFileRoute(
'/$postType/$postId/download',
)({
component: () => null,
server: {
handlers: {
GET: () => new Response('file'),
},
},
})
After adding the component, both SSR and the client generate /articles/1/download.
Version
@tanstack/react-router: 1.170.18
@tanstack/react-start: 1.168.32
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the reproduction files routes/$postType/route.tsx, routes/$postType/index.tsx, and routes/$postType/$postId/download.ts, then run the listed steps and compare the SSR and hydration hrefs. Done means a type-safe Link to the server-only route produces /articles/1/download on both sides, or the limitation is prevented by generated types or documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- backend, frontend, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100