Repository navigation
Description
Which project does this relate to?
Router
Describe the bug
tldr: When mixing virtual routing and file based routing using the physical virtual route type, the root of the mounted folder cannot have both an index.tsx and a route.tsx. This setup is supported in file-based routing, and is useful for adding layout or context to all child routes.
I'm working on a project with many mostly independent features, each of with has its own set of routes. In my repository, I'd like to have the routes for each feature isolated into the subfolder scoped to to that feature. For example:
/src
├── app/
│ └── routes/ # global routes not related to a feature
└── features
├── feature-a
│ └── routes/ # routes for feature-a
└── feature-b
└── routes/ # routes for feature-b
Virtual file routes seem like a great fit for this:
export default rootRoute('app/routes/root.tsx', [
// ... global routes
physical('feature-a', 'features/feature-a/routes'),
physical('feature-b', 'features/feature-b/routes'),
]);However, when i do this, i can no longer have both a route.tsx and an index.tsx in the feature/*/routes folders. When I do, i get an error like:
Error: Conflicting configuration paths were found for the following routes: "feature", "feature".
Please ensure each Route has a unique full path.
Conflicting files:
/home/projects/tanstack-router-xuwk6tn2/src/routes/feature/index.tsx
/home/projects/tanstack-router-xuwk6tn2/src/routes/feature/route.tsx
This feels broken, when the exact same folder structure works with file based routing.
As a workaround, I can use a pathless route instead of a route.tsx. ie, _layout.tsx and _layout.index.tsx. But this feels like extra verbosity that shouldn't be needed. Adding a shared layout, loader, or context to all children seems like it should be easier.
Your Example Website or App
Steps to Reproduce the Bug or Issue
The attached stackblitz can be swapped between virtual file routes and file based routing by commenting out the virtualRouteConfig option in vite.config.js, and then restarting vite. Vite will automatically scroll the terminal past the router plugin errors, so it will look successful, but if you scroll up yo can see the conflicting path error.
In src/routes.ts, you can swap between mounting the feature folder using physical() and mounting the feature routes directly using route() and feature().
Note that when disabling file based routing, routeTree.gen.ts is identical (ignoring casing) to the generated version when using route()/index(). Also note that when using physical() and getting the error, routeTree.gen.ts is not actually updated, so the app still works (confusing).
Expected behavior
When mounting a directory into a virtual route tree using physical('path', 'folder'), and folder contains both an index.tsx and a route.tsx, the route tree should still generate correctly, with the route.tsx being treated as the parent route for index.tsx, and all other routes in the folder.
Screenshots or Videos
No response
Platform
- @tanstack/react-router: 1.132.47
- @tanstack/router-plugin: 1.132.51
- @tanstack/virtual-file-routes: 1.131.2
- OS: OSX 15.5 ARM
- Browser: Chrome
- Browser Version: 140.0.7339.209 ARM
- Bundler: Vite
- Bundler Version: 7.1.7
Additional context
No response
Activity
SeanCassiere commentedon Oct 26, 2025
@adharris please attach an example of this with the vitrtual routing config. Using this unit tests can be built out to fix this.
adharris commentedon Oct 31, 2025
Sorry @SeanCassiere, what are you looking for that's not in the stack blitz?
bhscer commentedon Nov 1, 2025
I meet the same problem, and what should we provide now?
fmo688428-a11y commentedon Jul 8, 2026
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
Outlet,
Link,
createRootRouteWithContext,
useRouter,
HeadContent,
Scripts,
} from "@tanstack/react-router";
import { useEffect, type ReactNode } from "react";
import appCss from "../styles.css?url";
import { reportLovableError } from "../lib/lovable-error-reporting";
function NotFoundComponent() {
return (
404
Page not found
The page you're looking for doesn't exist or has been moved.
Go home
);
}
function ErrorComponent({ error, reset }: { error: Error; reset: () => void }) {
console.error(error);
const router = useRouter();
useEffect(() => {
reportLovableError(error, { boundary: "tanstack_root_error_component" });
}, [error]);
return (
This page didn't load
Something went wrong on our end. You can try refreshing or head back home.
<button
onClick={() => {
router.invalidate();
reset();
}}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Try again
Go home
);
}
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "فاطمه ❤️ امیر — ۱۱ ماه با هم" },
{ name: "description", content: "یادگاری یازده ماه عاشقانه — از ۱۸ شهریور ۱۴۰۴ تا ۱۸ تیر ۱۴۰۵" },
{ property: "og:title", content: "فاطمه ❤️ امیر — ۱۱ ماه با هم" },
{ property: "og:description", content: "یادگاری یازده ماه عاشقانه — از ۱۸ شهریور ۱۴۰۴ تا ۱۸ تیر ۱۴۰۵" },
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "theme-color", content: "#0a0f2a" },
{ name: "twitter:title", content: "فاطمه ❤️ امیر — ۱۱ ماه با هم" },
{ name: "twitter:description", content: "یادگاری یازده ماه عاشقانه — از ۱۸ شهریور ۱۴۰۴ تا ۱۸ تیر ۱۴۰۵" },
{ property: "og:image", content: "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/97004faf-678b-4033-b625-b794d026b15b/id-preview-8e9c8ef9--70f294e5-7d86-460a-88db-927275e805d2.lovable.app-1783548455916.png" },
{ name: "twitter:image", content: "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/97004faf-678b-4033-b625-b794d026b15b/id-preview-8e9c8ef9--70f294e5-7d86-460a-88db-927275e805d2.lovable.app-1783548455916.png" },
],
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", href: "/favicon.ico", type: "image/x-icon" },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&family=Gulzar&display=swap" },
],
}),
shellComponent: RootShell,
component: RootComponent,
notFoundComponent: NotFoundComponent,
errorComponent: ErrorComponent,
});
function RootShell({ children }: { children: ReactNode }) {
return (
{children}
);
}
function RootComponent() {
const { queryClient } = Route.useRouteContext();
return (
{/* Required: nested routes render here. Removing breaks all child routes. */}
);
}
jantic2 commentedon Aug 2, 2026
https://deriv-analytics-pro-sua6a8j5.blinkusercontent.com