Skip to content

Pathless layout child without params.parse disables the parent's params.parse → false skip #8690

Description

@Floriferous

Which project does this relate to?

Router

Describe the bug

If a route's params.parse returns false (the experimental "skip this route during matching"), giving that route a pathless layout child without its own params.parse turns the check off. URLs the parent's parse rejects then match the parent and its whole subtree.

Without the pathless child, /abc doesn't match /$postId. With it, /abc matches /$postId, and the loader runs with the raw, unparsed postId: 'abc'. The documented fallback pattern breaks the same way: once /posts/$postId (parse, priority 10) has a pathless child, /posts/hello matches /posts/$postId/_layout/ instead of /posts/$slug.

A transforming or throwing params.parse and params.stringify still behave correctly. Only the skip during matching is lost.

The pathless child seems to end on its parent's segment node in parseSegments. With no parse of its own, no pathless node is created for it, and node.parse = parseParams ?? null / node.priority = … ?? 0 overwrite the parent's values:

// create pathless node
if (
parseParams &&
route.children &&
!route.isRoot &&
route.id &&
route.id.charCodeAt(route.id.lastIndexOf('/') + 1) === 95 /* '_' */
) {
const pathlessNode = createSegmentNode(node, SEGMENT_TYPE_PATHLESS)
;(node.pathless ??= []).push(pathlessNode)
node = pathlessNode
}
const isLeaf = (route.path || !route.children) && !route.isRoot
// create index node
if (isLeaf && literalEnd < length) {
const indexNode = createSegmentNode(node, SEGMENT_TYPE_INDEX)
node.index = indexNode
node = indexNode
}
node.parse = parseParams ?? null
node.priority = options?.params?.priority ?? 0

Workaround: repeat the parent's params.parse on the pathless child.

Complete minimal reproducer

https://gist.github.com/Floriferous/ad98e6fa0dc7950d6a09d5e56fe9f4ec

Steps to Reproduce the Bug

  1. Clone the gist, then npm install && npm start (Node 24).
  2. Compare the output with and without the pathless _layout child: /abc and /abc/details match only when the child exists.
import { createMemoryHistory, createRootRoute, createRoute, createRouter } from '@tanstack/react-router'

function buildRouter({ withPathlessChild }) {
  const rootRoute = createRootRoute()
  const postRoute = createRoute({
    getParentRoute: () => rootRoute,
    path: '$postId',
    params: { parse: ({ postId }) => (/^\d+$/.test(postId) ? { postId } : false) },
  })
  if (withPathlessChild) {
    const layoutRoute = createRoute({ getParentRoute: () => postRoute, id: '_layout' }) // no params.parse
    layoutRoute.addChildren([createRoute({ getParentRoute: () => layoutRoute, path: 'details' })])
    postRoute.addChildren([layoutRoute])
  }
  return createRouter({ routeTree: rootRoute.addChildren([postRoute]), history: createMemoryHistory() })
}

for (const withPathlessChild of [false, true]) {
  const router = buildRouter({ withPathlessChild })
  for (const path of ['/123', '/abc', '/abc/details']) {
    console.log(`withPathlessChild=${withPathlessChild} ${path} ->`, router.matchRoutes(path, {}).map((m) => m.routeId))
  }
}

Actual output:

withPathlessChild=false /123 -> [ '__root__', '/$postId' ]
withPathlessChild=false /abc -> [ '__root__' ]
withPathlessChild=false /abc/details -> [ '__root__' ]
withPathlessChild=true /123 -> [ '__root__', '/$postId' ]
withPathlessChild=true /abc -> [ '__root__', '/$postId' ]
withPathlessChild=true /abc/details -> [ '__root__', '/$postId', '/$postId/_layout', '/$postId/_layout/details' ]

Expected behavior

Adding a pathless layout should not change which URLs match. /abc and /abc/details should match nothing (not found) whether or not _layout exists, just as they do without it.

Screenshots or Videos

No response

Platform

  • Router Version: @tanstack/react-router 1.170.42 / @tanstack/router-core 1.171.35 (latest). The same code is on main at 57bd1a9.
  • OS: macOS 26.6
  • Browser: n/a (reproduced in Node 24 with router.matchRoutes; first seen in a Vite 8.2 app with file-based routes)
  • Bundler: n/a / Vite 8.2.2

Additional context

  • Related: a code-based pathless route whose id doesn't start with _ (e.g. id: 'layout', as in the code-based routing docs) and that has its own params.parse gets no pathless node either, so its parse replaces the parent's.
  • A local fix that passes a regression test plus the existing matching tests: only assign node.parse / node.priority when the route added a node of its own, and treat a route that added no segment as pathless when creating the pathless node. Happy to open a PR with the test.
  • Investigated and drafted with an AI assistant (Claude). The repro and fix were run and checked locally.

Activity

  1. self-assigned this
    on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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