We'd like to thank the Remix team for pioneering the concept of virtual file routes. We've taken inspiration from their work and adapted it to work with TanStack Router's existing file-based route-tree generation.
Virtual file routes are a powerful concept that allows you to build a route tree programmatically using code that references real files in your project. This can be useful if:
Here's a quick example of using virtual file routes to map a route tree to a set of real files in your project:
import {
rootRoute,
route,
index,
layout,
physical,
} from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
index('index.tsx'),
layout('layout.tsx', [
route('/dashboard', 'app/dashboard.tsx', [
index('app/dashboard-index.tsx'),
route('/invoices', 'app/dashboard-invoices.tsx', [
index('app/invoices-index.tsx'),
route('$id', 'app/invoice-detail.tsx'),
]),
]),
physical('/posts', 'posts'),
]),
])
import {
rootRoute,
route,
index,
layout,
physical,
} from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
index('index.tsx'),
layout('layout.tsx', [
route('/dashboard', 'app/dashboard.tsx', [
index('app/dashboard-index.tsx'),
route('/invoices', 'app/dashboard-invoices.tsx', [
index('app/invoices-index.tsx'),
route('$id', 'app/invoice-detail.tsx'),
]),
]),
physical('/posts', 'posts'),
]),
])
Virtual file routes can be configured either via:
If you're using the TanStackRouter plugin for Vite/Rspack/Webpack, you can configure virtual file routes by passing a virtualRoutesConfig option to the plugin:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { rootRoute } from '@tanstack/virtual-file-routes'
const routes = rootRoute('root.tsx', [
// ... the rest of your virtual route tree
])
export default defineConfig({
plugins: [TanStackRouterVite({ virtualRouteConfig: routes }), react()],
})
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { rootRoute } from '@tanstack/virtual-file-routes'
const routes = rootRoute('root.tsx', [
// ... the rest of your virtual route tree
])
export default defineConfig({
plugins: [TanStackRouterVite({ virtualRouteConfig: routes }), react()],
})
To create virtual file routes, you'll need to import the @tanstack/virtual-file-routes package. This package provides a set of functions that allow you to create virtual routes that reference real files in your project. A few utility functions are exported from the package:
The rootRoute function is used to create a virtual root route. It takes a file name and an array of children routes. Here's an example of a virtual root route:
import { rootRoute } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
// ... children routes
])
import { rootRoute } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
// ... children routes
])
The route function is used to create a virtual route. It takes a path, a file name, and an array of children routes. Here's an example of a virtual route:
import { route } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
route('/about', 'about.tsx', [
// ... children routes
]),
])
import { route } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
route('/about', 'about.tsx', [
// ... children routes
]),
])
You can also define a virtual route without a file name. This allows to set a common path prefix for its children:
import { route } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
route('/hello', [
route('/world', 'world.tsx'), // full path will be "/hello/world"
route('/universe', 'universe.tsx'), // full path will be "/hello/universe"
]),
])
import { route } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
route('/hello', [
route('/world', 'world.tsx'), // full path will be "/hello/world"
route('/universe', 'universe.tsx'), // full path will be "/hello/universe"
]),
])
The index function is used to create a virtual index route. It takes a file name. Here's an example of a virtual index route:
import { index } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [index('index.tsx')])
import { index } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [index('index.tsx')])
The layout function is used to create a virtual layout route. It takes a file name, an array of children routes, and an optional layout ID. Here's an example of a virtual layout route:
import { layout } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
layout('layout.tsx', [
// ... children routes
]),
])
import { layout } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
layout('layout.tsx', [
// ... children routes
]),
])
You can also specify a layout ID to give the layout a unique identifier that is different from the filename:
import { layout } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
layout('my-layout-id', 'layout.tsx', [
// ... children routes
]),
])
import { layout } from '@tanstack/virtual-file-routes'
const virtualRouteConfig = rootRoute('root.tsx', [
layout('my-layout-id', 'layout.tsx', [
// ... children routes
]),
])
Physical virtual routes are a way to "mount" a directory of good ol' TanStack Router File Based routing convention under a specific URL path. This can be useful if you are using virtual routes to customize a small portion of your route tree high up in the hierarchy, but want to use the standard file-based routing convention for sub-routes and directories.
Consider the following file structure:
/routes
├── root.tsx
├── index.tsx
├── layout.tsx
├── app
│ ├── dashboard.tsx
│ ├── dashboard-index.tsx
│ ├── dashboard-invoices.tsx
│ ├── invoices-index.tsx
│ ├── invoice-detail.tsx
└── posts
├── index.tsx
├── $postId.tsx
├── $postId.edit.tsx
├── comments/
│ ├── index.tsx
│ ├── $commentId.tsx
└─ ─ likes/
├── index.tsx
├── $likeId.tsx
/routes
├── root.tsx
├── index.tsx
├── layout.tsx
├── app
│ ├── dashboard.tsx
│ ├── dashboard-index.tsx
│ ├── dashboard-invoices.tsx
│ ├── invoices-index.tsx
│ ├── invoice-detail.tsx
└── posts
├── index.tsx
├── $postId.tsx
├── $postId.edit.tsx
├── comments/
│ ├── index.tsx
│ ├── $commentId.tsx
└── likes/
├── index.tsx
├── $likeId.tsx
Let's use virtual routes to customize our route tree for everything but posts, then use physical virtual routes to mount the posts directory under the /posts path:
const virtualRouteConfig = rootRoute('root.tsx', [
// Set up your virtual routes as normal
index('index.tsx'),
layout('layout.tsx', [
route('/dashboard', 'app/dashboard.tsx', [
index('app/dashboard-index.tsx'),
route('/invoices', 'app/dashboard-invoices.tsx', [
index('app/invoices-index.tsx'),
route('$id', 'app/invoice-detail.tsx'),
]),
]),
// Mount the `posts` directory under the `/posts` path
physical('/posts', 'posts'),
]),
])
const virtualRouteConfig = rootRoute('root.tsx', [
// Set up your virtual routes as normal
index('index.tsx'),
layout('layout.tsx', [
route('/dashboard', 'app/dashboard.tsx', [
index('app/dashboard-index.tsx'),
route('/invoices', 'app/dashboard-invoices.tsx', [
index('app/invoices-index.tsx'),
route('$id', 'app/invoice-detail.tsx'),
]),
]),
// Mount the `posts` directory under the `/posts` path
physical('/posts', 'posts'),
]),
])
The previous section showed you how you can use TanStack Router's File Based routing convention inside of a virtual route configuration.
However, the opposite is possible as well.
You can configure the main part of your app's route tree using TanStack Router's File Based routing convention and opt into virtual route configuration for specific subtrees.
Consider the following file structure:
/routes
├── __root.tsx
├── foo
│ ├── bar
│ │ ├── __virtual.ts
│ │ ├── details.tsx
│ │ ├── home.tsx
│ │ └── route.ts
│ └── bar.tsx
└── index.tsx
/routes
├── __root.tsx
├── foo
│ ├── bar
│ │ ├── __virtual.ts
│ │ ├── details.tsx
│ │ ├── home.tsx
│ │ └── route.ts
│ └── bar.tsx
└── index.tsx
Let's look at the bar directory which contains a special file named __virtual.ts. This file instructs the generator to switch over to virtual file route configuration for this directory (and its child directories).
__virtual.ts configures the virtual routes for that particular subtree of the route tree. It uses the same API as explained above, with the only difference being that no rootRoute is defined for that subtree:
import {
defineVirtualSubtreeConfig,
index,
route,
} from '@tanstack/virtual-file-routes'
export default defineVirtualSubtreeConfig([
index('home.tsx'),
route('$id', 'details.tsx'),
])
import {
defineVirtualSubtreeConfig,
index,
route,
} from '@tanstack/virtual-file-routes'
export default defineVirtualSubtreeConfig([
index('home.tsx'),
route('$id', 'details.tsx'),
])
The helper function defineVirtualSubtreeConfig is closely modeled after vite's defineConfig and allows you to define a subtree configuration via a default export. The default export can either be
You can mix and match TanStack Router's File Based routing convention and virtual route configuration however you like.
Let's go deeper!
Check out the following example that starts off using File Based routing convention, switches over to virtual route configuration for /posts, switches back to File Based routing convention for /posts/lets-go only to switch over to virtual route configuration again for /posts/lets-go/deeper.
├── __root.tsx
├── index.tsx
├── posts
│ ├── __virtual.ts
│ ├── details.tsx
│ ├── home.tsx
│ └── lets-go
│ ├── deeper
│ │ ├── __virtual.ts
│ │ └── home.tsx
│ └── index.tsx
└── posts.tsx
├── __root.tsx
├── index.tsx
├── posts
│ ├── __virtual.ts
│ ├── details.tsx
│ ├── home.tsx
│ └── lets-go
│ ├── deeper
│ │ ├── __virtual.ts
│ │ └── home.tsx
│ └── index.tsx
└── posts.tsx
While much less common, you can also configure virtual file routes via the TanStack Router CLI by adding a virtualRouteConfig object to your tsr.config.json file and defining your virtual routes and passing the resulting JSON that is generated by calling the actual rootRoute/route/index/etc functions from the @tanstack/virtual-file-routes package:
// tsr.config.json
{
"virtualRouteConfig": {
"type": "root",
"file": "root.tsx",
"children": [
{
"type": "index",
"file": "home.tsx"
},
{
"type": "route",
"file": "posts/posts.tsx",
"path": "/posts",
"children": [
{
"type": "index",
"file": "posts/posts-home.tsx"
},
{
"type": "route",
"file": "posts/posts-detail.tsx",
"path": "$postId"
}
]
},
{
"type": "layout",
"id": "first",
"file": "layout/first-layout.tsx",
"children": [
{
"type": "layout",
"id": "second",
"file": "layout/second-layout.tsx",
"children": [
{
"type": "route",
"file": "a.tsx",
"path": "/layout-a"
},
{
"type": "route",
"file": "b.tsx",
"path": "/layout-b"
}
]
}
]
}
]
}
}
// tsr.config.json
{
"virtualRouteConfig": {
"type": "root",
"file": "root.tsx",
"children": [
{
"type": "index",
"file": "home.tsx"
},
{
"type": "route",
"file": "posts/posts.tsx",
"path": "/posts",
"children": [
{
"type": "index",
"file": "posts/posts-home.tsx"
},
{
"type": "route",
"file": "posts/posts-detail.tsx",
"path": "$postId"
}
]
},
{
"type": "layout",
"id": "first",
"file": "layout/first-layout.tsx",
"children": [
{
"type": "layout",
"id": "second",
"file": "layout/second-layout.tsx",
"children": [
{
"type": "route",
"file": "a.tsx",
"path": "/layout-a"
},
{
"type": "route",
"file": "b.tsx",
"path": "/layout-b"
}
]
}
]
}
]
}
}
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.