TanStack / TanStack/router

Link to a server-only route skips parent params.stringify on the client

Open
#7,877 1 comment 0 reactions 0 assignees View on GitHub

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:

  1. Start the application.
  2. Visit /articles.
  3. Check the hydration warning.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.