aaex-file-router
v2.5.0
Published
A file-based routing system for React projects that automatically generates routes from your file structure. Similar to Next.js App Router or Remix file conventions.
Downloads
231
Readme
AAEX File Router
A file-based routing system for React projects that automatically generates routes from your file structure. Similar to Next.js App Router or Remix file conventions.
V. 2.0.4
Added support for root layout and 404 pages
Table of Contents
- Features
- Installation
- Quick Start
- File Conventions
- Route Resolution Examples
- Import strategy
- Layouts
- FileLink component
- useScroll hook
- Usage
- Generated files
- API Reference
- How It Works
- Performance Considerations
- Common Patterns
- Troubleshooting
- License
Features
- Automatic Route Generation: Routes are generated based on your file and folder structure
- Layout Support: Create
layout.tsxfiles to wrap nested routes - Slug Support: Creates dynamic routes for [slug] files
- Static & Lazy Loading: Top-level routes use static imports, nested routes use lazy loading
- Hot Reload: Vite plugin watches for file changes and regenerates routes automatically
- TypeScript Support: Full TypeScript support with generated route types
- Type safe link component: Link component that knows what routes are available
Installation
npm install aaex-file-routerQuick Start
1. Create your pages structure
src/pages/
└── dashboard/
├── loading.tsx ← Used as fallback for ALL lazy imports below
├── index.tsx
├── stats/
│ ├── loading.tsx ← Overrides parent
│ └── weekly.tsx
└── users/
└── [id].tsx ← used for dynamic routes ex :users/123
2. Configure Vite
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { aaexFileRouter } from "aaex-file-router/plugin";
export default defineConfig({
plugins: [
react(),
aaexFileRouter({
pagesDir: "./src/pages", //page files location(optional: default ./src/pages)
outputFile: "./src/routes.ts", //generated routes (default: ./src/routes.ts)
}),
],
});3. Use in your app
Note: Since v1.4.0 every lazy-loaded route is automatically wrapped in a Suspense boundary, using the nearest loading.tsx file as the fallback.
1. Using createBrowserRouter (recommended for most users)
// src/App.tsx
import "./App.css";
import routes from "./routes";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Suspense } from "react";
function App() {
const router = createBrowserRouter(routes);
return (
<Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={router} />
</Suspense>
);
}
export default App;2. Using nested Route elements
Note I will probably create a custom route provider using this version later since this is the only solution that works with VITE-SSR if you wrap client in <BrowserRouter/> and server in <StaticRouter/>
//src/App.tsx
import {
BrowserRouter,
Routes,
Route,
type RouteObject,
} from "react-router-dom";
import routes from "./routes";
import { Suspense } from "react";
import "./App.css";
//recursivly creates nested routes
function createRoutes(route: RouteObject) {
return (
<Route key={route.path} path={route.path} element={route.element}>
{route.children?.map((child) => createRoutes(child))}
</Route>
);
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>{routes.map((route) => createRoutes(route))}</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;File Conventions
index.tsx
Renders at the parent route path.
pages/index.tsx → "/"
pages/about/index.tsx → "/about"layout.tsx
Wraps all sibling and nested routes. Children are rendered in an <Outlet />. Also works for root layout if placed directly in /pages
// pages/admin/layout.tsx
import { Outlet } from "react-router-dom";
export default function AdminLayout() {
return (
<div>
<nav>Admin Navigation</nav>
<Outlet /> {/* Nested routes render here */}
</div>
);
}404.tsx
Root & folder level fallback. Creating a file named 404.tsx inside the page root or inside a subfolder creates a 404 page for that segment. root => /nonexisting subfolder ex: blog => blog/nonexisting
will render your 404
//src/pages/404.tsx
export default function NotFound() {
return <>404 not found!</>;
}Slug files
Filenames wrapper in square brackets [filename] will resolve to a dynamic route
src/pages/test/[<filename>].tsx → "/test/:<filename>"// src/pages/test/[slug].tsx
import { useParams } from "react-router-dom";
export default function TestWithSlug() {
// replace slug with what the file is called
const { slug } = useParams();
return <div>{slug}</div>;
}Named files
Any other .tsx file becomes a route based on its filename.
pages/about.tsx → "/about"
pages/blog/post.tsx → "/blog/post"Route Resolution Examples
| File Structure | Route Path |
| --------------------------------------- | -------------------- |
| src/pages/index.tsx | / |
| src/pages/about.tsx | /about |
| src/pages/blog/index.tsx | /blog |
| src/pages/blog/post.tsx | /blog/post |
| src/pages/admin/layout.tsx + children | /admin/* (grouped) |
Layouts
Layouts wrap their child routes and provide shared UI:
// src/pages/dashboard/layout.tsx
import { Outlet } from "react-router-dom";
export default function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<main>
<Outlet />
</main>
</div>
);
}All routes in src/pages/dashboard/* will render inside this layout.
Import Strategy
| File type | | | Import style | | | ------------------------------------- | --- | --- | ------------- | --- | | pages/*.tsx (top level) | | | static import | | | Files inside a folder with layout.tsx | | | Lazy loaded | | | Files inside a folder without layout | | | Lazy loaded | | | layout.tsx | | | Static import | | loading.tsx | | | Static import |
FileLink component
The FileLink component is a type safe wrapper for the Link component in react router that uses an autogenerated type to check which routes are available.
Notice!
At the moment it can only do the basic routing where the "to" prop is a string. React Router's normal Link still works in cases where type safety is less important.
Usage
If reades the type file that is automatically generated
users/{string} is what users/:slug gets translated to this means users/ allows any string after even if the route dosnt exist. Will look into better solution
// src/routeTypes.ts
// * AUTO GENERATED: DO NOT EDIT
/
export type FileRoutes = "/" | "test" | "users/{string}";// src/pages/index.tsx
import { FileLink } from "aaex-file-router";
import type { FileRoutes } from "../routeTypes"; //import type
export default function Home() {
return (
<>
Hello Home!
<FileLink<FileRoutes> to="test">Test safe</FileLink>
{/* or without type safety */}
<FileLink to="some-route">Non safe</FileLink>
</>
);
}useScroll
Custom React hook that scrolls to the top after navigation. Designed to work seamlessly with client-side routing while remaining SSR-safe.
Features
Automatically scrolls on route change
Supports smooth scrolling
Can scroll either the window or a specific container
Safe to use in SSR environments
Usage
import { useScroll } from "aaex-file-router";
export default function PageWithNavigation() {
useScroll();
return <>{/* rest of content */}</>;
}Options
useScroll accepts an optional config object.
useScroll({
behavior?: ScrollBehavior;
container?: HTMLElement | null;
});| Option | Type | Default | Description |
| ----------- | --------------------- | ---------- | ---------------------------------------- | ------------------------- |
| behavior | "auto" | "smooth" | "auto" | Scroll animation behavior |
| container | HTMLElement \| null | null | Scrolls this element instead of window |
Example: smooth scroll
useScroll({
behavior: "smooth",
});Example: scroll a container
const ref = useRef<HTMLDivElement>(null);
useScroll({
container: ref.current,
});
return <div ref={ref}>...</div>;Generated files
routes.ts
Generated route definition file
// src/routes.ts
...imports
export default routes = [
{
path: "/",
element: React.createElement(Index)},
{
path: "test",
element: React.createElement(TestLayout),
children:
[
{
path: "",
element: React.createElement(
React.Suspense,
{ fallback: React.createElement(TestLoading) },
React.createElement(React.lazy(() => import("./pages/test/index.tsx")))
)
},
...
]
}
]
routeTypes.ts
Exports TypeScript union type of existing routes
export type FileRoutes = `/` | `test`;API Reference
FileScanner
Scans the file system and converts files into a structured format.
import { FileScanner } from "aaex-file-router/core";
const scanner = new FileScanner("./src/pages");
const fileData = await scanner.get_file_data();RouteGenerator
Converts file structure into React Router route configuration.
import { RouteGenerator } from "aaex-file-router/core";
const generator = new RouteGenerator();
const routesCode = await generator.generateRoutesFile(fileData);aaexFileRouter (Vite Plugin)
Automatically watches for file changes and regenerates routes.
import { aaexFileRouter } from "aaex-file-router/plugin";
export default defineConfig({
plugins: [
aaexFileRouter({
pagesDir: "./src/pages",
outputFile: "./src/routes.ts",
}),
],
});Server routing
If you are using vite SSR you want to configure your vite config with the serverRouter plugin instead of the normal one.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { aaexServerRouter } from "aaex-file-router/plugin";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), aaexServerRouter()],
});this plugin generates 2 route files
1. server-routes.ts
Includes absolute path to the file modulePath
route example:
{
"path": "",
"element": React.createElement(Index),
"modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/index.tsx"
},2. client-routes.ts
Just the normal route file.
How It Works
- File Scanning: Recursively scans your pages directory and builds a file tree
- Route Generation: Converts the file structure into React Router
RouteObjectformat - Smart Importing:
- Top-level files use static imports for faster initial load
- Nested/grouped routes use lazy loading for code splitting
- Layout files are statically imported as route wrappers
- Auto-Regeneration: Vite plugin watches for changes and automatically regenerates
routes.ts
Performance Considerations
- Static Imports: Top-level routes are statically imported, included in the main bundle
- Code Splitting: Routes nested in layout groups are lazy-loaded, improving initial bundle size
- Watch Mode: File watching only runs in development (
vite serve), not in production builds
Common Patterns
Shared Layout
pages/
├── layout.tsx # Wraps entire app
├── index.tsx
└── about.tsxNested Layouts
pages/
├── layout.tsx # Root layout
├── admin/
│ ├── layout.tsx # Admin layout (inherits from root)
│ ├── index.tsx
│ └── users.tsxRoute Groups Without Layout
pages/
├── blog/
│ ├── post.tsx # Routes as /blog/post (no grouping)
│ └── author.tsx # Routes as /blog/authorTroubleshooting
Routes not updating on file change
- Ensure Vite dev server is running (
npm run dev) - Check that
pagesDirin vite config matches your actual pages directory
Duplicate imports in generated file
- This shouldn't happen, but if it does, try restarting the dev server
- Check for files with the same name in different directories
Unexpected route paths
- Remember:
index.tsxfiles inherit their parent's path - Directories without
layout.tsxflatten their children into absolute routes - File names are converted to lowercase for routes
License
MIT
