You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Layout routes cause optional params to fail to match #5912
Taking the example route from the documentation of posts/{-$category}, this is documented to match both /posts and /posts/category.
While this normally works as expected, if a "route" component exists, such as this:
posts/
{-$category}.tsx
route.tsx
/posts now fails to match anything. The route.tsx is rendered but the outlet is just left empty. If route.tsx is removed, the category route is rendered as expected.
This looks related to things like #5799 and #5635, but the fix attached to both those issues seems to have already landed and did not resolve this issue.
Trying to write my thoughts down so I can debug this later:
If we go to /posts should we match /posts/{-$category} or /posts?
current behavior is /posts, so just the layout with an empty outlet
if route.tsx is not there, then /posts will match /posts/{-$category}
I feel like this issue is kinda correct. If I were to try and formulate a rule it would be
if /posts is an exact match and is an index then favor that over the optional child
if /posts does not exist or is a layout route then favor the optional child
it is implementable but
our possibilities of early return are even smaller
some of the unit tests fail (but these are edge-cases that are hard to have a strong opinion on)
Both of these would now fail, and match the other route instead
it('/optional/static/static vs /static/static',()=>{consttree=makeTree(['/{-$other}/posts/new','/posts/new'])expect(findRouteMatch('/posts/new',tree)?.route.id).toBe('/posts/new')})it('edge-case: ???',()=>{// This test comes from the previous processRouteTree tests.// > This demonstrates that `/foo/{-$p}.tsx` will be matched, not `/foo/{-$p}/{-$x}.tsx`// > This route has 1 optional parameter, making it more specific than the route with 2consttree=makeTree(['/foo/{-$p}.tsx','/foo/{-$p}/{-$x}.tsx'])expect(findRouteMatch('/foo',tree)?.route.id).toBe('/foo/{-$p}.tsx')})
Either way could be argued, I'm not sure what is the correct match here. Would be interesting to see what other routers do.
To implement this, change
perfect match early return condition
// perfect match, no need to continueif(statics===partsLength&&node.isIndex)returnbestMatch
stack frame comparison function
functionisFrameMoreSpecific(// the stack frame previously saved as "best match"prev: MatchStackFrame<any>|null,// the candidate stack framenext: MatchStackFrame<any>,): boolean{if(!prev)returntruereturn(next.statics>prev.statics||(next.statics===prev.statics&&(next.dynamics>prev.dynamics||(next.dynamics===prev.dynamics&&((next.node.isIndex&&!prev.node.isIndex)||next.depth>prev.depth)))))}
Activity
Sheraff commentedon Nov 20, 2025
Trying to write my thoughts down so I can debug this later:
If we go to
/postsshould we match/posts/{-$category}or/posts?/posts, so just the layout with an empty outletroute.tsxis not there, then/postswill match/posts/{-$category}I feel like this issue is kinda correct. If I were to try and formulate a rule it would be
/postsis an exact match and is an index then favor that over the optional child/postsdoes not exist or is a layout route then favor the optional childit is implementable but
Both of these would now fail, and match the other route instead
Either way could be argued, I'm not sure what is the correct match here. Would be interesting to see what other routers do.
To implement this, change