Generated file-based routes for Vite
Motivation
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.
How does it work?
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.
- π Client-side file-based routing
- β‘ Powered by Vite
- β¨ React support with
react-routeror@tanstack/routerπ§ͺ or@tanstack/react-locationπ¨ - β¨ Solid support with
@solidjs/router - β¨ File-based MDX routes with React or Solid, requires
@mdx-js/rollupinstallation and setup - π Type-safe navigation
- π Type-safe global modals
- π€ Route-based code-splitting and lazy-loading
- π Route-based data loaders and actions
- π£ Route-based error boundary
- π Nested layouts
- π« Pathless layout groups
- β Optional static and dynamic routes
- π Ignored routes per file or directory
- β‘ Visit
generouted's interactive playground via StackBlitz - π§© Explore file-based routing patterns and conventions
- π Visualize the routes layouts and the resolved routes paths
- π Update
src/pages/files and see your changes reflecting
React Router
In 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
// 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.
- Type-safe navigation + global modals
- Custom integration
- Custom integration with custom path
- MDX routes
Solid Router
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.
TanStack React Router β In-progress experimental support π§ͺ
React Location β Deprecated π¨
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> }
- Routes declaration at
src/pages - Supports
.tsx,.jsxand.mdxfile extensions - Optional
src/pages/_app.tsxfor an app level layout - Optional
src/pages/404.tsxfor a custom not-found page
src/pages/index.tsxβ/src/pages/posts/index.tsxβ/posts
src/pages/posts/2022/index.tsxβ/posts/2022src/pages/posts/2022/resolutions.tsxβ/posts/2022/resolutions
src/pages/posts/[slug].tsxβ/posts/:slugsrc/pages/posts/[slug]/tags.tsxβ/posts/:slug/tagssrc/pages/posts/[...all].tsxβ/posts/*
- By defining
_layout.tsxin any nested directory βsrc/pages/posts/_layout.tsx - Requires using an
<Outlet />component to render layout children - All the files within the
src/pages/posts/directory in this case, will be wrapped with that layout
- Route file should be outside of the nested layout directory
- Include dots
.between the segments to be converted to forward slashes src/pages/posts.nested.as.url.not.layout.tsxβ/posts/nested/as/url/not/layout
- Similar to nested layouts for layout definition
- By wrapping the parent directory with parentheses
() src/pages/(auth)/_layout.tsxsrc/pages/(auth)/login.tsxβ/login- Layout parent directory name is not included in the routes paths
- By prefixing the file name with a plus sign
+(thinking the modal is an extra route overlaying the current route) - Modals navigation available via the
useModals()hook src/pages/+info.tsxβ/infosrc/pages/+checkout/+details.tsxβ/checkout/detailssrc/pages/+checkout/+payment.tsxβ/checkout/payment
- By prefixing a route segment with a minus sign
-(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
- Any directory or file starts with an underscore
_will be ignored src/pages/_ignored.tsxsrc/pages/posts/_components/button.tsxsrc/pages/posts/_components/link.tsx
- Required page component
export default Component() {...} - Optional page loader function
export const Loader = () => {...} - Optional page action function
export const Action = () => {...} - Optional suspense-based pending component
export const Pending = () => {...} - Optional error boundary component
export const Catch = () => {...}
Directory structure
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
Overview
| 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 />component
Via @generouted/react-router/lazy or @generouted/solid-router/lazy
- Used instead of
@generouted/react-routeror@generouted/solid-routerto enable lazy-loading - Make sure to replace all imports to lazy imports β namely at app entry and
src/pages/_app.tsx - Provides the same
<Routes />androutesexports
Via @generouted/react-router/plugin or @generouted/solid-router/plugin
- Vite plugin for type generation and initializing type-safe components/hooks/utils
- Generates
src/router.tsfile - Exports type-safe
<Link>,<Navigate>,useModals(),useNavigate(),useParams(),redirect(), etc. - Check out
@generouted/react-routerdocs or@generouted/solid-routerdocs for more details
Via @generouted/react-router/core or @generouted/solid-router/core
- Available for customization, however it's recommended to use the available integrations directory via the
<Routes/>component - Check out the custom integration example
How to implement protected or guarded routes?
There 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' 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' 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
How to use with Hash or Memory Routers?
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' import { routes } from '@generouted/react-router' const router = createHashRouter(routes) const Routes = () => <RouterProvider router={router} /> createRoot(document.getElementById('root')!).render(<Routes />)
MIT