Generated file-based routes for Vite
I enjoyed using file-based routing since I tried Next.js (pages directory). After applying the same concept with Vite and client-side applications, I started writing blog posts covering the implementation of client-side file-based routing with React Router which was packaged later as generouted.
Today generouted brings many features, supports multiple frameworks and routers, and inspires ideas and conventions from Next.js, Remix, Expo, Docusaurus, SvelteKit and more.
generouted uses Vite's glob import API to list the routes within the src/pages directory and generates the routes tree and modals based on generouted's conventions.
There are also Vite plugins available for some integrations to provide type-safe components/hooks/utils through code-generation.
react-router-dom or @tanstack/router ? or @tanstack/react-location @solidjs/router@mdx-js/rollup installation and setupgenerouted's interactive playground via StackBlitzsrc/pages/ files and see your changes reflectingIn case you don't have a Vite project with React and TypeScript, check Vite documentation to start a new project.
pnpm add @generouted/react-router react-router-dom// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import generouted from '@generouted/react-router/plugin'
export default defineConfig({ plugins: [react(), generouted()] })// src/main.tsx
import { createRoot } from 'react-dom/client'
import { Routes } from '@generouted/react-router'
createRoot(document.getElementById('root')!).render(<Routes />)Add the home page by creating a new file src/pages/index.tsx → /, then export a default component:
export default function Home() {
return <h1>Home</h1>
}Check the routing conventions section below.
You can find more details about type-safe navigation and global modals at @generouted/react-router docs.
In case you don't have a Vite project with Solid and TypeScript, check Vite documentation to start a new project.
pnpm add @generouted/solid-router @solidjs/router// vite.config.ts
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
import generouted from '@generouted/solid-router/plugin'
export default defineConfig({ plugins: [solid(), generouted()] })// src/main.tsx
import { render } from 'solid-js/web'
import { Routes } from '@generouted/solid-router'
render(Routes, document.getElementById('root')!)Add the home page by creating a new file src/pages/index.tsx → /, then export a default component:
export default function Home() {
return <h1>Home</h1>
}See more about generouted routing conventions below.
You can find more details about type-safe navigation and global modals at @generouted/solid-router docs.
Check out the docs here
In case you don't have a Vite project with React and TypeScript, check Vite documentation to start a new project.
pnpm add generouted @tanstack/react-location// src/main.tsx
import { createRoot } from 'react-dom/client'
import { Routes } from 'generouted/react-location'
createRoot(document.getElementById('root')!).render(<Routes />)Add the home page by creating a new file src/pages/index.tsx → /, then export a default component:
export default function Home() {
return <h1>Home</h1>
}src/pages.tsx, .jsx and .mdx file extensionssrc/pages/_app.tsx for an app level layoutsrc/pages/404.tsx for a custom not-found pagesrc/pages/index.tsx → /src/pages/posts/index.tsx → /postssrc/pages/posts/2022/index.tsx → /posts/2022src/pages/posts/2022/resolutions.tsx → /posts/2022/resolutionssrc/pages/posts/[slug].tsx → /posts/:slugsrc/pages/posts/[slug]/tags.tsx → /posts/:slug/tagssrc/pages/posts/[...all].tsx → /posts/*_layout.tsx in any nested directory → src/pages/posts/_layout.tsx<Outlet /> component to render layout childrensrc/pages/posts/ directory in this case, will be wrapped with that layout. between the segments to be converted to forward slashessrc/pages/posts.nested.as.url.not.layout.tsx → /posts/nested/as/url/not/layout()src/pages/(auth)/_layout.tsxsrc/pages/(auth)/login.tsx → /login+ (thinking the modal is an extra route overlaying the current route)useModals() hooksrc/pages/+info.tsx → /infosrc/pages/+checkout/+details.tsx → /checkout/detailssrc/pages/+checkout/+payment.tsx → /checkout/payment- (thinking the segment can be subtracted or removed from the route path)src/pages/-en/about.tsx → /en?/about → /en/about, /aboutsrc/pages/-[lang]/about.tsx → /:lang?/about → /en/about, /fr/about, /about_ will be ignoredsrc/pages/_ignored.tsxsrc/pages/posts/_components/button.tsxsrc/pages/posts/_components/link.tsxexport default Component() {...}export const Loader = () => {...}export const Action = () => {...}export const Pending = () => {...}export const Catch = () => {...}src/pages
├── (auth)
│ ├── _layout.tsx
│ ├── login.tsx
│ └── register.tsx
├── blog
│ ├── _components
│ │ ├── button.tsx
│ │ └── comments.tsx
│ ├── [...all].tsx
│ ├── [slug].tsx
│ ├── _layout.tsx
│ ├── index.tsx
│ └── tags.tsx
├── docs
│ ├── -[lang]
│ │ ├── index.tsx
│ │ └── resources.tsx
│ └── -en
│ └── contributors.tsx
├── +info.tsx
├── 404.tsx
├── _app.tsx
├── _ignored.tsx
├── about.tsx
├── blog.w.o.layout.tsx
└── index.tsx| File | Path | Convention |
|---|---|---|
(auth)/_layout.tsx |
Pathless Layout group | |
(auth)/login.tsx |
/login |
Regular route |
(auth)/register.tsx |
/register |
Regular route |
blog/_components/button.tsx |
Ignored route by an ignored directory | |
blog/_components/comments.tsx |
Ignored route by an ignored directory | |
blog/[...all].tsx |
/blog/* |
Dynamic catch-all route |
blog/[slug].tsx |
/blog/:slug |
Dynamic route |
blog/_layout.tsx |
Layout for /blog routes |
|
blog/index.tsx |
/blog |
Index route |
blog/tags.tsx |
/blog/tags |
Regular route |
docs/-[lang]/index.tsx |
/docs/:lang?/index |
Optional dynamic route segment |
docs/-[lang]/resources.tsx |
/docs/:lang?/resources |
Optional dynamic route segment |
docs/-en/contributors.tsx |
/docs/en?/contributors |
Optional static route segment |
+info.tsx |
/info |
Modal route |
404.tsx |
* |
Custom 404 (optional) |
_app.tsx |
Custom app layout (optional) |
|
_ignored.tsx |
Ignored route | |
about.tsx |
/about |
Regular route |
blog.w.o.layout.tsx |
/blog/w/o/layout |
Route without /blog layout |
index.tsx |
/ |
Index route |
Via @generouted/react-router or @generouted/solid-router
<Routes /> — file-based routing component to be render in the app entryroutes — file-based routes tree/object used by default at <Routes /> componentVia @generouted/react-router/lazy or @generouted/solid-router/lazy
@generouted/react-router or @generouted/solid-router to enable lazy-loadingsrc/pages/_app.tsx<Routes /> and routes exportsVia @generouted/react-router/plugin or @generouted/solid-router/plugin
src/router.ts file<Link>, <Navigate>, useModals(), useNavigate(), useParams(), redirect(), etc.@generouted/react-router docs or @generouted/solid-router docs for more detailsVia @generouted/react-router/core or @generouted/solid-router/core
<Routes/> componentThere are multiple approaches to achieve that. If you prefer handling the logic in one place, you can define the protected routes with redirection handling within a component:
// src/config/redirects.tsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '../context/auth'
import { Path } from '../router'
const PRIVATE: Path[] = ['/logout']
const PUBLIC: Path[] = ['/login']
export const Redirects = ({ children }: { children: React.ReactNode }) => {
const auth = useAuth()
const location = useLocation()
const authenticatedOnPublicPath = auth.active && PUBLIC.includes(location.pathname as Path)
const unAuthenticatedOnPrivatePath = !auth.active && PRIVATE.includes(location.pathname as Path)
if (authenticatedOnPublicPath) return <Navigate to="/" replace />
if (unAuthenticatedOnPrivatePath) return <Navigate to="/login" replace />
return children
}Then use that component (<Redirects> ) at the root-level layout src/pages/_app.tsx to wrap the <Outlet> component:
// src/pages/_app.tsx
import { Outlet } from 'react-router-dom'
import { Redirects } from '../config/redirects'
export default function App() {
return (
<section>
<header>
<nav>...</nav>
</header>
<main>
<Redirects>
<Outlet />
</Redirects>
</main>
</section>
)
}You can find a full example of this approach at Render template
You can use the exported routes object to customize the router or to use hash/memory routers:
import { createRoot } from 'react-dom/client'
import { RouterProvider, createHashRouter } from 'react-router-dom'
import { routes } from '@generouted/react-router'
const router = createHashRouter(routes)
const Routes = () => <RouterProvider router={router} />
createRoot(document.getElementById('root')!).render(<Routes />)MIT