Skip to content

fix(router-core): Pathless Layout Route Renders Empty HTML #4003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

leesb971204
Copy link
Contributor

fixes #3843

Copy link

nx-cloud bot commented Apr 16, 2025

View your CI Pipeline Execution ↗ for commit 9fa49ff.

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 4m 50s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 10s View ↗

☁️ Nx Cloud last updated this comment at 2025-06-23 05:43:11 UTC

Copy link

pkg-pr-new bot commented Apr 16, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@4003

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@4003

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@4003

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@4003

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@4003

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@4003

@tanstack/react-router-with-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-with-query@4003

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@4003

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@4003

@tanstack/react-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@4003

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@4003

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@4003

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@4003

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@4003

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@4003

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@4003

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@4003

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@4003

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@4003

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@4003

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@4003

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@4003

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@4003

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@4003

@tanstack/solid-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@4003

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@4003

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@4003

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@4003

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@4003

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@4003

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@4003

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@4003

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@4003

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@4003

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@4003

commit: 9fa49ff

@schiller-manuel
Copy link
Contributor

can you add a test for this?

@schiller-manuel
Copy link
Contributor

also wondering if this maybe needs a fix in the router generator instead? what's the idea of your fix here?

@leesb971204
Copy link
Contributor Author

leesb971204 commented Apr 16, 2025

also wondering if this maybe needs a fix in the router generator instead? what's the idea of your fix here?

Just because only a pathless layout file exists doesn’t mean there’s no route.
Do you think it’s more appropriate to handle this in the generator instead?

@schiller-manuel
Copy link
Contributor

yes I think so. the generator currently generates a "virtual route" in this situation. this is then matched at runtime

@leesb971204 leesb971204 changed the title fix(router-core): Pathless Layout Route Renders Empty HTML fix(router-generator): Pathless Layout Route Renders Empty HTML Apr 17, 2025
@leesb971204
Copy link
Contributor Author

yes I think so. the generator currently generates a "virtual route" in this situation. this is then matched at runtime

I believe the issue can be resolved by updating the isNonPath value when a node’s only child is a pathless_layout.
What do you think about this approach?

@schiller-manuel
Copy link
Contributor

maybe, I did not look into it deeply yet. can you add an e2e test into one of the existing e2e test setups for react router for this please? then we can check whether the fix solves it

@leesb971204
Copy link
Contributor Author

leesb971204 commented Apr 17, 2025

maybe, I did not look into it deeply yet. can you add an e2e test into one of the existing e2e test setups for react router for this please? then we can check whether the fix solves it

Got it. But I think additional changes might be needed.
For example, if a node has both a pathless_layout and static children, should we still treat it as isNonPath: true?
In this situation, accessing /app would still result in a empty HTML being returned.
What do you think?

L app  👈 isNonPath ??
  L _appLayout.tsx  
  L test.tsx  

@SeanCassiere
Copy link
Member

I think we'll also need to update the test snapshots on this one.

Looking at the current changes on the reproduction sandbox in the reported issue, using the pr-pkg-new packages, the diff shows that a path is being assigned to a pathless layout route.

image

@SeanCassiere
Copy link
Member

Been thinking about this more and we'd need some comprehensive sandbox (end-to-end) testing for this.

Plus, my point earlier about pathless layout routes being assigned a path isn't correct.

@leesb971204
Copy link
Contributor Author

Been thinking about this more and we'd need some comprehensive sandbox (end-to-end) testing for this.

Plus, my point earlier about pathless layout routes being assigned a path isn't correct.

Then, do you think we should approach this in a different way rather than relying on isNonPath?

@leesb971204
Copy link
Contributor Author

leesb971204 commented May 2, 2025

@SeanCassiere

Could you review the last commit?
I only modified the logic related to the path part, so the e2e tests may not pass, but based on my local testing, it seems to be working fine.
Additional changes might be needed, but I’d appreciate it if you could check whether the issue is resolved.

Copy link
Member

@SeanCassiere SeanCassiere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leesb971204 its partially resolved.

With your changes, these scenarios now work.

src/routes/
	app/
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
src/routes/
	app/
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
		_foo.tsx
		_foo.index.tsx

However, these two scenarios break the route tree.

  1. For this scenario, the routeTree file gets put into a state where it tries to create an import from './routes/app' which of-course does not exist on the file-system.
src/routes/
	app/
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
		_foo.tsx
		_foo.something.tsx
  1. For this scenario, the routeTree file adds an additional /app into the routes. So, instead of it being /app/dashboard, it instead becomes /app/app/dashboard.
src/routes/
	app/
		route.tsx
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
		_foo.tsx
		_foo.index.tsx

I wish I had better feedback to give, as to how you can approach this, but this is something @schiller-manuel and I have discussed earlier at length with the current system being 'best-effort' compromise that we came to.

The shortcut, to get your original sandbox working, would be to throw a redirect in src/routes/app/index.tsx to a different route or to set a notFoundComponent and immediately throw a notFound.

src/routes/
	app/
+		index.tsx
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx

@leesb971204
Copy link
Contributor Author

@leesb971204 its partially resolved.

With your changes, these scenarios now work.

src/routes/
	app/
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
src/routes/
	app/
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
		_foo.tsx
		_foo.index.tsx

However, these two scenarios break the route tree.

  1. For this scenario, the routeTree file gets put into a state where it tries to create an import from './routes/app' which of-course does not exist on the file-system.
src/routes/
	app/
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
		_foo.tsx
		_foo.something.tsx
  1. For this scenario, the routeTree file adds an additional /app into the routes. So, instead of it being /app/dashboard, it instead becomes /app/app/dashboard.
src/routes/
	app/
		route.tsx
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx
		_foo.tsx
		_foo.index.tsx

I wish I had better feedback to give, as to how you can approach this, but this is something @schiller-manuel and I have discussed earlier at length with the current system being 'best-effort' compromise that we came to.

The shortcut, to get your original sandbox working, would be to throw a redirect in src/routes/app/index.tsx to a different route or to set a notFoundComponent and immediately throw a notFound.

src/routes/
	app/
+		index.tsx
		_appLayout.tsx
		_appLayout.dashboard.tsx
		_appLayout.something.tsx

Thanks for the review.
I’m really sorry to bother you again, but I need some clarification on the last part.
Do you see this scenario not as something that requires changes to the generator or other internal code, but rather as an issue where the user is expected to add an index.tsx themselves to handle the redirect?
If so, I’ll go ahead and close this PR.

@SeanCassiere
Copy link
Member

... Do you see this scenario not as something that requires changes to the generator or other internal code, but rather as an issue where the user is expected to add an index.tsx themselves to handle the redirect? If so, I’ll go ahead and close this PR.

It is kind of two-fold.

Router works in a tree-like structure, which is important since pathless layout routes (which do not have a 'path') requires it having to know what its parent is, for everything to work at runtime.

As such, in this scenario, it creates a route in the tree which doesn't have a component and/or any logic attached to it. Its purely there, just for the pathless layout route to use for it refer to it as its parent.

So, I say this problem is two-fold, since we could either tackle this problem by.

  1. Adding code in the generator that detects this "empty" route scenario, setup a virtual index for the given route which throws a redirect to / if the parent layout route's children do not contain an existing index route.
  2. Or move that stuff into a user-space and ask them to set it up themselves (using an index.tsx).

If you are open to tackling option 1, that'd be great! If not, no worries.

@leesb971204
Copy link
Contributor Author

... Do you see this scenario not as something that requires changes to the generator or other internal code, but rather as an issue where the user is expected to add an index.tsx themselves to handle the redirect? If so, I’ll go ahead and close this PR.

It is kind of two-fold.

Router works in a tree-like structure, which is important since pathless layout routes (which do not have a 'path') requires it having to know what its parent is, for everything to work at runtime.

As such, in this scenario, it creates a route in the tree which doesn't have a component and/or any logic attached to it. Its purely there, just for the pathless layout route to use for it refer to it as its parent.

So, I say this problem is two-fold, since we could either tackle this problem by.

  1. Adding code in the generator that detects this "empty" route scenario, setup a virtual index for the given route which throws a redirect to / if the parent layout route's children do not contain an existing index route.
  2. Or move that stuff into a user-space and ask them to set it up themselves (using an index.tsx).

If you are open to tackling option 1, that'd be great! If not, no worries.

I’ve implemented the first approach — it now redirects to / when there’s no index route in the children.
Would it make sense to update the test cases to reflect this change?

@SeanCassiere
Copy link
Member

cc @schiller-manuel on please making some allowances to be able to add this into the generator reworks later on.

@schiller-manuel
Copy link
Contributor

I am currently rewriting the generator at a larger scale. please bear with me, I hope to get this done in the next few days. then we can revisit this topic

@leesb971204 leesb971204 force-pushed the fix/pathless-layout-notfound branch from c495f09 to 35d88fb Compare June 20, 2025 01:07
Comment on lines 521 to 540
// Detect layout routes that have children but lack an explicit index ("/") child
// and mark them to redirect to the root path.
acc.routeNodes.forEach((node) => {
if (node._fsRouteType !== 'layout') {
return
}

if (!node.isVirtualParentRoute) {
return
}

if (node.children && node.children.length > 0) {
if (node.children.some((child) => child.cleanedPath === '/')) {
return // already has index child → skip
}
}

node.isVirtualRedirectIndex = true
node.redirectTo = '/'
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We iterate through acc.routeNodes to find virtual layout routes that do not have an index route as a children, and redirect them to '/'.

Copy link
Contributor Author

@leesb971204 leesb971204 Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the following directory structure, accessing /foo/bar currently results in a redirect to '/'.
If this behavior is incorrect and /foo/bar should be accessible, additional changes are needed.
Please share your thoughts.

src/routes/
	foo/
		_fooLayout.tsx
		bar/	
			index.tsx

Copy link
Contributor

@schiller-manuel schiller-manuel Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surely this is a valid case where /foo/bar should be accesible
don't see where the pathless layout comes into play here?

@schiller-manuel
Copy link
Contributor

schiller-manuel commented Jun 20, 2025

I am wondering what the actual issue is that we need to solve. it seems the problem is that at runtime router is not aware that those routes should not be used for matching. if my understanding is correct, shouldn't we rather fix this when processing the route tree in the router?
we probably can't detect this at that point in time (or can we?). if we cannot, we could issue a marker to the route in the generator to set a prop using Route.update which would then later get read when processing the route tree upon router initialization

@leesb971204
Copy link
Contributor Author

I am wondering what the actual issue is that we need to solve. it seems the problem is that at runtime router is not aware that those routes should not be used for matching. if my understanding is correct, shouldn't we rather fix this when processing the route tree in the router? we probably can't detect this at that point in time (or can we?). if we cannot, we could issue a marker to the route in the generator to set a prop using Route.update which would then later get read when processing the route tree upon router initialization

Are you suggesting that redirecting pathless layout routes to '/' may not be the appropriate solution?

@schiller-manuel
Copy link
Contributor

yes. I think ideally the route matching would just fail and the not found handling would kick in.

@leesb971204
Copy link
Contributor Author

yes. I think ideally the route matching would just fail and the not found handling would kick in.

In that case, it sounds more appropriate to handle those specific paths as notFound within the router itself.
I’ll try that approach instead.

@leesb971204 leesb971204 changed the title fix(router-generator): Pathless Layout Route Renders Empty HTML fix(router-core): Pathless Layout Route Renders Empty HTML Jun 20, 2025
@leesb971204 leesb971204 force-pushed the fix/pathless-layout-notfound branch from 971741a to 35d88fb Compare June 20, 2025 05:42
Comment on lines 3228 to 3234
// If there are child routes, check if any of them have the same path as the current route
if (route.children && route.children.length > 0) {
if (!route.children.find((child) => child.path === '/')) {
return false
}
}

Copy link
Contributor Author

@leesb971204 leesb971204 Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a route does not have a parent index route among its children, it is excluded from matching.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's "parent index route" when it comes to children?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's "parent index route" when it comes to children?

This refers to a route with an index path among the children under a parent route.

src/routes/
	foo/
		_fooLayout.tsx
		index.tsx 👈
		bar/	
			index.tsx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if you have foo.tsx or route.tsx inside /foo ?

Copy link
Contributor Author

@leesb971204 leesb971204 Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if you have foo.tsx or route.tsx inside /foo ?

I’m not sure if this can be fully solved within the router core alone.
As you mentioned, we need to correctly match both cases:
– when a route file with the same name exists in another directory (foo.tsx)
– when a route.tsx file exists inside the directory

But it seems difficult to handle both cases purely in the router core.
I initially tried to determine this based on the presence of options.component,
but this approach fails in cases where the component is lazy.

Should we consider adding a specific flag in the generator to help the router core handle it properly or using update?

@schiller-manuel
Copy link
Contributor

we need to add tests for this, ideally in router core.

@@ -583,6 +600,7 @@ export class Generator {
`id: '${node.path}'`,
!node.isNonPath ? `path: '${node.cleanedPath}'` : undefined,
`getParentRoute: () => ${findParent(node, exportName)}`,
node.isVirtualLayout ? `isVirtualLayout: true` : undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A value is set via update() in the router core to determine whether a route should be matched.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schiller-manuel
I wanted to implement this in a way that keeps the logic hidden from the user’s code, but I couldn’t come up with a better solution. 😕
I’d appreciate any further suggestions you might have.
If you need a test case, I’d be happy to write one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pathless Layout Route Renders Empty HTML
3 participants