Skip to content

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

Description

@pacexy

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions