@epicabdou/linkr
v0.2.0
Published
linkr.js — File-based routing for React Router (v7) with Vite
Downloads
1,090
Maintainers
Readme
@epicabdou/linkr
File-based routing for React Router v7 in Vite + React SPA apps. Generates a route tree from a src/pages/ directory using import.meta.glob — the consumer passes the glob result; the library does not call it.
Install
pnpm add @epicabdou/linkr react react-dom react-routerPeer dependencies: react (≥18), react-dom (≥18), react-router (≥7).
Usage
Basic setup
import { createRoutes } from "@epicabdou/linkr";
import { createBrowserRouter, RouterProvider } from "react-router";
const pages = import.meta.glob("./pages/**/*.tsx");
const routes = createRoutes({ pagesGlob: pages, pagesDir: "pages" });
const router = createBrowserRouter(routes);
// Then render <RouterProvider router={router} />One-line app bootstrap
Use createRootWithLinkr to create the root, build routes, and render in one call:
import { createRootWithLinkr } from "@epicabdou/linkr";
createRootWithLinkr(document.getElementById("root")!, {
pagesGlob: import.meta.glob("./pages/**/*.tsx"),
pagesDir: "pages",
});This uses createBrowserRouter, StrictMode, and RouterProvider under the hood. No need to import React Router or call createRoutes yourself.
Using <LinkrApp> (with custom providers)
When you need to wrap the app with your own providers, use the <LinkrApp> component and pass the same options as createRoutes:
import { LinkrApp } from "@epicabdou/linkr";
const pages = import.meta.glob("./pages/**/*.tsx");
function App() {
return (
<YourProvider>
<LinkrApp
pagesGlob={pages}
pagesDir="pages"
defaultRedirectTo="/login"
/>
</YourProvider>
);
}
createRoot(document.getElementById("root")!).render(<App />);Options are read once on mount; the router is created with useMemo.
createRoutes options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| pagesGlob | Record<string, () => Promise<unknown>> | required | Result of import.meta.glob("...") from your app. |
| pagesDir | string | "src/pages" | Base path used to normalize file paths. Must match the prefix you strip from glob keys (e.g. "pages" when glob is ./pages/**/*.tsx from src/). |
| layoutFileName | string | "_layout" | Filename (without extension) for layout routes. |
| notFoundFileName | string | "404" | Filename for the catch-all not-found route. |
| routeExtensions | string[] | [".tsx", ".ts", ".jsx", ".js"] | Extensions that define route files. |
| defaultRedirectTo | string | "/" | Default redirect path when a route's protect check fails and no redirectTo is set. |
| layoutsGlob | Record<string, () => Promise<unknown>> | — | When set with layoutMap, layouts are loaded from this glob and _layout files in pages are ignored. Example: import.meta.glob("./layouts/**/*.tsx"). |
| layoutsDir | string | "src/layouts" | Base path for layout files when using layoutsGlob. |
| layoutMap | Record<string, string> | — | Path prefix → layout name. Use "" or "/" for root; segment name (e.g. blog) for nested. Requires layoutsGlob. |
File conventions
| File / pattern | Route |
|----------------|-------|
| index.tsx | / (index route) |
| about.tsx | /about |
| blog/index.tsx | /blog (index under blog) |
| blog/[id].tsx | /blog/:id |
| blog/[id]/index.tsx | /blog/:id (index under dynamic segment) |
| blog/[id]/post.tsx | /blog/:id/post |
| docs/[...slug].tsx | /docs/* (splat) |
| 404.tsx | path: "*" (catch-all, last) |
| _layout.tsx (in any folder) | Layout route; children render in <Outlet />. Ignored when using layoutsGlob + layoutMap. |
- Nested routes: folder nesting = nested routes. Dynamic segments work in both file names (e.g.
[id].tsx) and folder names (e.g.[id]/index.tsx,[id]/post.tsx). - Layouts: Either put
_layout.tsxin a folder (folder-based), or use a separate layouts folder with layoutsGlob and layoutMap so layouts are reusable components. - Sort order: among siblings — static segments first, then dynamic (
:id), then splat (*). - Lazy loading: every route uses React Router's
lazy()for code splitting.
Page module exports
| Export | Description |
|--------|-------------|
| default | React component used as the route element (required). |
| ErrorBoundary | Optional; used as the route's errorElement. |
| handle | Optional; attached to the route's handle. |
| protect | Optional; runs before rendering. If it returns false, the user is redirected. See Route protection. |
Layouts in a separate folder
Keep layouts as reusable components in a dedicated folder and wire them by path with layoutMap (no _layout.tsx in pages).
Example layouts:
// src/layouts/Root.tsx
import { Link, Outlet } from "react-router";
export default function RootLayout() {
return (
<div>
<nav><Link to="/">Home</Link> <Link to="/blog">Blog</Link></nav>
<Outlet />
</div>
);
}// src/layouts/Blog.tsx
import { Outlet } from "react-router";
export default function BlogLayout() {
return <div><h2>Blog</h2><Outlet /></div>;
}Wire in router setup:
const pages = import.meta.glob("./pages/**/*.tsx");
const layouts = import.meta.glob("./layouts/**/*.tsx");
const routes = createRoutes({
pagesGlob: pages,
pagesDir: "pages",
layoutsGlob: layouts,
layoutsDir: "layouts",
layoutMap: { "/": "Root", "": "Root", blog: "Blog" },
});- layoutMap keys:
""or"/"= root layout;"blog"= layout for/blogand its children. Value = layout filename without extension (e.g.Root,Blog). - The same layout component can be reused by mapping multiple keys to the same name.
Route protection
You can guard a route or an entire layout in two ways.
1. protect export on a page or layout
Export protect from any page or _layout module. The check runs in the route loader; if it returns false, the user is redirected before the page renders.
Shorthand (function): redirect uses defaultRedirectTo from createRoutes options (or "/").
// pages/dashboard.tsx
export const protect = () => !!getAuthToken();
export default function Dashboard() {
return <div>Dashboard</div>;
}Full form (object): specify redirect and optional fallback for async checks.
// pages/settings/_layout.tsx
export const protect = {
check: async () => (await fetchUser())?.role === "admin",
redirectTo: "/login",
};
export default function SettingsLayout() {
return (
<div>
<nav>...</nav>
<Outlet />
</div>
);
}Set a default redirect when creating routes:
createRoutes({
pagesGlob: pages,
pagesDir: "pages",
defaultRedirectTo: "/login",
});2. <Protect> component
Use <Protect> when you want to guard content inside a layout (e.g. wrap <Outlet />) or need a stable condition with a fallback UI.
Predefined config (recommended): define condition, redirectTo, and fallback once, then reuse with <Protect {...config}>.
// src/config/protect.tsx
import type { ProtectConfig } from "@epicabdou/linkr";
export const authProtect: ProtectConfig = {
condition: () => !!localStorage.getItem("token"),
redirectTo: "/login",
fallback: <div>Checking access…</div>,
};// In any layout
import { Protect } from "@epicabdou/linkr";
import { Outlet } from "react-router";
import { authProtect } from "../config/protect";
export default function DashboardLayout() {
return (
<Protect {...authProtect}>
<Outlet />
</Protect>
);
}Props:
condition: sync or async function; returntrueto allow,falseto redirect.redirectTo: path to redirect to when the condition fails.fallback: optional React node shown while an async condition is pending.children: content to render when the condition is true.
You can define multiple configs (e.g. authProtect, adminProtect) and import the one you need.
API exports
| Export | Description |
|--------|-------------|
| createRoutes | Builds React Router route array from pages glob and options. |
| LinkrApp | Component that renders RouterProvider with routes from options. |
| createRootWithLinkr | One-shot: createRoot, createRoutes, StrictMode + RouterProvider. |
| Protect | Component for conditional redirect with fallback UI. |
| normalizePath, parseSegment, compareSegments | Path utilities (for advanced use). |
Types: CreateRoutesOptions, LayoutMap, LayoutsGlob, PagesGlob, RouteObject, RouteProtect, ProtectProps, ProtectConfig, LinkrAppOptions, ParsedSegment, SegmentKind.
Quick start with CLI
npx create-linkrjs-app my-app
cd my-app && pnpm devBuild & test
From the monorepo root:
pnpm --filter @epicabdou/linkr build
pnpm --filter @epicabdou/linkr testOr from packages/linkr:
pnpm build
pnpm test