Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save trevor-hackett/788ddc844cc284153c5ec9d19e03355b to your computer and use it in GitHub Desktop.
Save trevor-hackett/788ddc844cc284153c5ec9d19e03355b to your computer and use it in GitHub Desktop.
File based routes with react-router-dom (v6.4 and above) and Vite
└───src
| main.tsx
│ routes.tsx
└───pages
│ _app.tsx
│ 404.tsx
│ index.tsx
│ posts.tsx
└───posts
│ index.tsx
│ 404.tsx
└───[id]
index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Routes } from "./routes";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Routes />
</React.StrictMode>
);
import React from 'react'
import {
ActionFunction,
DataBrowserRouter,
LoaderFunction,
Outlet,
RouteObject,
} from 'react-router-dom'
type Element = React.ReactNode
type Module = {
default: React.ComponentType<any>
loader: LoaderFunction
action: ActionFunction
errorElement?: React.ComponentType<any>
}
const PRESERVED = import.meta.globEager<Module>(`/src/pages/(_app|404).tsx`)
const ROUTES = import.meta.globEager<Module>(`/src/pages/**/[a-z[404]*.tsx`)
const preservedRoutes: Partial<Record<string, RouteObject>> = Object.keys(PRESERVED).reduce(
(routes, key) => {
const path = key.replace(/\/src\/pages\/|\.tsx$/g, '')
const module = PRESERVED[key]
const Element = module.default ?? React.Fragment
const ErrorElement = module.errorElement
const route: RouteObject = {
element: <Element />,
loader: module.loader || undefined,
action: module.action || undefined,
errorElement: ErrorElement ? <ErrorElement /> : undefined,
id: key.replace(/\/src\/pages|\.tsx$/g, '').replace(/^\//, ''),
}
return { ...routes, [path]: route }
},
{},
)
const regularRoutes = Object.keys(ROUTES).reduce<RouteObject[]>((routes, key) => {
const module = ROUTES[key]
const Element = module.default ?? React.Fragment
const ErrorElement = module.errorElement
const route: RouteObject = {
element: <Element />,
loader: module.loader || undefined,
action: module.action || undefined,
errorElement: ErrorElement ? <ErrorElement /> : undefined,
id: key.replace(/\/src\/pages|\.tsx$/g, '').replace(/^\//, ''),
}
const segments = key
.replace(/\/src\/pages|\.tsx$/g, '')
.replace(/404$/g, '*')
.replace(/\[\.{3}.+\]/, '*')
.replace(/\[([^\]]+)\]/g, ':$1')
.split('/')
.filter(Boolean)
segments.reduce((parent, segment, index) => {
const path = segment.replace(/index|\./g, '/').replace(/^\//, '')
const root = index === 0
const leaf = index === segments.length - 1 && segments.length > 1
const node = !root && !leaf
const insert = /^\w|\//.test(path) ? 'unshift' : 'push'
if (root) {
const dynamic = path.startsWith(':') || path === '*'
if (dynamic) return parent
const last = segments.length === 1
if (last) {
routes.push({ path, ...route })
return parent
}
}
if (root || node) {
const current = root ? routes : parent.children
const found = current?.find((route: RouteObject) => route.path === path)
if (found) found.children ??= []
else current?.[insert]({ path, children: [] })
return found || (current?.[insert === 'unshift' ? 0 : current.length - 1] as RouteObject)
}
if (leaf) {
parent?.children?.[insert]({ path, ...route })
}
return parent
}, {} as RouteObject)
return routes
}, [])
const DefaultApp: React.ComponentType<any> = () => <Outlet />
const DefaultNotFound: React.ComponentType<any> = () => <>404: Route not found</>
let defaultAppRoute = { element: <DefaultApp />, children: regularRoutes }
let defaultNotFoundRoute = { element: <DefaultNotFound /> }
const appRoute = preservedRoutes?.['_app'] ?? defaultAppRoute
const notFoundRoute = preservedRoutes?.['404'] ?? defaultNotFoundRoute
const routes: RouteObject[] = [
{ ...appRoute, id: 'root', children: regularRoutes, path: '' },
{ ...notFoundRoute, id: 'rootnotfound', path: '*' },
]
export default function Routes() {
return <DataBrowserRouter fallbackElement={<>Loading...</>} routes={routes} />
}
@trevor-hackett
Copy link
Author

trevor-hackett commented May 27, 2022

This is an example of how to get file-based routes using react-router-dom (v6.4) and Vitejs.

The goal was to get as close to remix as possible without SSR.

Routes are placed in the /src/pages directory.

It supports:

  • Nested layouts
  • Dynamic routes
  • loaders/mutations
  • Special routes like _app.tsx (root layout) and 404.tsx (Not found page)
  • Error boundaries (errorElement)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment