A powerful file-based routing plugin for React Router v7 that enables automatic route generation based on your file system structure, with full support for layouts, loaders, actions, and error boundaries.
react-router
7.0.0 or highervite
6.0.0 or higherglob
package (dev dependency)
npm install -D glob
# or
pnpm add -D glob
# or
yarn add -D glob
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactRoutePlugin from './vite-react-routing-plugin';
export default defineConfig({
plugins: [
reactRoutePlugin({ root: './src/app/pages' }),
react()
]
});
// src/app/index.tsx
import { createBrowserRouter, RouterProvider } from "react-router";
import React from "react";
import ReactDOM from "react-dom/client";
import { routes } from 'virtual:react-routing-plugin/client';
const router = createBrowserRouter(routes);
const root = document.getElementById("root");
ReactDOM.createRoot(root).render(
<RouterProvider router={router} />
);
// src/server/index.tsx
import { renderToString } from "react-dom/server";
import {
createStaticHandler,
createStaticRouter,
StaticRouterProvider,
} from "react-router";
import { routes } from 'virtual:react-routing-plugin/server';
const { query, dataRoutes } = createStaticHandler(routes);
export async function handler(request: Request) {
const context = await query(request);
const router = createStaticRouter(dataRoutes, context);
return new Response(renderToString(
<StaticRouterProvider router={router} context={context} />
), {
status: context.statusCode,
headers: { 'Content-Type': 'text/html' }
});
}
Add the type definitions to your project:
// types/vite-react-routing-plugin.d.ts
declare module 'virtual:react-routing-plugin/client' {
import { RouteObject } from 'react-router';
export const routes: RouteObject[];
}
declare module 'virtual:react-routing-plugin/server' {
import { RouteObject } from 'react-router';
export const routes: RouteObject[];
}
function reactRoutePlugin(opts?: DataRoutePluginOptions): Plugin[]
root?: string
- Base directory for file scanning. Default:'./src/app/pages'
clientScan?: ScanOptions | false
- Client-side virtual module scanning options. Set tofalse
to disable or return minimal route structure for path checking only.serverScan?: ScanOptions | false
- Server-side virtual module scanning options. Set tofalse
to disable or return minimal route structure for path checking only.lazy?: boolean
- Whether to use lazy imports for page components and routing modules. Default:true
. Set tofalse
for SSR/SSG optimization using static imports.
interface ScanOptions {
page?: boolean; // Scan page components (default: true)
route?: boolean; // Scan route modules (default: true)
layout?: boolean; // Scan layout components (default: true)
error?: boolean; // Scan error components (default: true)
fallback?: boolean; // Scan fallback components (default: true)
}
At least one of these files must exist in a directory to define a route:
page.{js,jsx,ts,tsx}
- Page component that exports a React component as default. Can optionally exportloader
andaction
functions.route.{js,ts}
- Route module that can exportloader
andaction
functions for both client and server.
layout.{js,jsx,ts,tsx}
- Layout component that must export a React component as default and include<Outlet />
for child route rendering.error.{js,jsx,ts,tsx}
- Error boundary component for handling route errors.fallback.{js,jsx,ts,tsx}
- Fallback component for hydration fallback (HydrateFallback
).
Important: Pages inherit layouts based on their actual file directory path, not the routing path.
- Current directory layout takes priority
- If no layout exists, searches parent directories (nearest first)
- Flat routing also follows actual file path-based inheritance
- Layout components must include
<Outlet />
to render child routes
- Client-side: If both
page.js
androute.js
exist, onlyroute.js
loader/action functions are used - Server-side: Only
route.js
loader/action functions are executed;page.js
functions are ignored
Standard directory/file names create static routes:
FILESYSTEM URL
==================== ======
pages/page.js /
pages/about/page.js /about
pages/jobs/page.js /jobs
pages/$$email/page.js /$email
Use $
prefix for dynamic segments:
FILESYSTEM URL
======================== =======================
pages/movie/$id/page.js /movie/1, /movie/2, ...
pages/actor/$gender/$name/page.js /actor/male/john, /actor/female/jane
Use $
suffix for optional segments:
FILESYSTEM URL
======================== =======================
pages/theatre/$id$/page.js /theatre/1, /theatre/2, /theatre
pages/seats/$id/get$/page.js /seats/1, /seats/1/get, /seats/2/get
Use parentheses to group routes without adding URL segments:
FILESYSTEM URL
================================ ==================
pages/(front)/page.js /
pages/(front)/about/page.js /about
pages/admin-panel/page.js /admin-panel
Use dots (.
) in directory names to create flat routing structure:
FILESYSTEM URL
==================== ======
pages/about.ours/page.js /about/ours
pages/movie.$id/page.js /movie/1, /movie/2
pages/actor.$gender.$name/page.js /actor/male/john, /actor/female/jane
Use $
as the segment name:
FILESYSTEM URL
======================== =======================
pages/address/$/page.js /address/seoul, /address/kyeonggi/anyang
Access the catch-all parameter with params['*']
in your components.
Routes are resolved in the following priority order:
- Static routes - Exact string matches
- Dynamic routes - Routes with
:param
segments - Flat routes - Routes defined with dot notation
- Group routes - Routes with parentheses grouping
- Folder catch-all routes -
$/page.js
- Flat catch-all routes -
address.$/page.js
Given this file structure:
src/app/pages/
├── (main)/
│ ├── layout.tsx
│ ├── page.tsx
│ └── system/
│ ├── member/
│ │ ├── page.tsx
│ │ └── new/
│ │ └── page.tsx
│ └── role/
│ └── page.tsx
└── auth/
└── login/
└── page.tsx
The plugin generates:
export const routes = [
{
path: "/",
Component: Layout0,
children: [
{
index: true,
lazy: () => import("/src/app/pages/(main)/page.tsx")
.then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
},
{
path: "system/member",
lazy: () => import("/src/app/pages/(main)/system/member/page.tsx")
.then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
},
{
path: "system/member/new",
lazy: () => import("/src/app/pages/(main)/system/member/new/page.tsx")
.then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
},
{
path: "system/role",
lazy: () => import("/src/app/pages/(main)/system/role/page.tsx")
.then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
}
]
},
{
path: "/auth/login",
lazy: () => import("/src/app/pages/auth/login/page.tsx")
.then(m => ({ Component: m.default, loader: m.loader, action: m.action }))
}
];
The plugin throws these error types during build time and development:
InvalidRouteError
- Invalid route configuration (e.g., flat routes with grouping)DuplicateRouteError
- Conflicting route paths
When errors occur, the plugin provides fallback empty routes and logs detailed error information to keep the development server running.
The plugin supports HMR with intelligent update strategies:
- File content changes: Attempts partial route reconstruction, falls back to full reconstruction if needed
- File add/delete/rename: Full route reconstruction
- Debounced updates: Multiple rapid changes are batched with a 100ms debounce
- Map-based lookups: O(1) file and directory resolution
- Single-pass scanning: Files are processed once per update
- Efficient hierarchy building: Bottom-up directory tree construction
- Lazy loading support: Code splitting with dynamic imports by default
- Eager loading option: Static imports for SSR/SSG optimization
- Move your route components to the
pages
directory following the file conventions - Extract loaders and actions to separate
route.js
files or keep them inpage.js
files - Convert layout components to use
<Outlet />
instead of manual routing - Replace manual
createBrowserRouter
calls with the virtual module import
app/page.tsx
→pages/page.tsx
app/layout.tsx
→pages/layout.tsx
app/loading.tsx
→pages/fallback.tsx
app/error.tsx
→pages/error.tsx
app/[slug]/page.tsx
→pages/$slug/page.tsx
- Check that files are within the configured
root
directory - Verify file extensions match the supported patterns
- Check browser console for route generation errors
- Ensure layout components include
<Outlet />
- Verify the layout is in the correct directory relative to the page
- Check that layout scanning is enabled in options
- Add the provided type definitions to your project
- Ensure TypeScript can resolve the virtual module paths
- Check that
vite/client
types are included in yourtsconfig.json
The plugin currently supports these file patterns:
**/route.{js,ts}
**/{page,layout,error,fallback}.{js,jsx,ts,tsx}
Use different scan options for different environments:
export default defineConfig(({ mode }) => ({
plugins: [
reactRoutePlugin({
lazy: mode === 'development', // Eager loading in production
clientScan: mode === 'development' ? undefined : { layout: false }, // Disable layouts in production client
}),
react()
]
}));
This plugin provides a powerful, flexible file-based routing solution that scales from simple static sites to complex applications with nested layouts and dynamic routes.