Baldr Dashboard - Routing System Documentation
Framework: React Router v7 (SSR-enabled)
Last Updated: November 5, 2025
Version: 1.0.0
Table of Contents
- Overview
- Routing Architecture
- Route Configuration
- File-Based Routing
- Layout System
- Route Groups
- Navigation
- Server-Side Rendering (SSR)
- Protected Routes
- Query Parameters
- Error Handling
- Best Practices
Overview
Baldr Dashboard uses React Router v7, the latest evolution of React Router featuring:
- Server-Side Rendering (SSR) by default
- Type-safe routing with TypeScript
- File-based route configuration (not file-system routing)
- Nested layouts with persistent UI
- Automatic code splitting per route
- Type-safe loaders and actions
- Enhanced meta/link management
Key Difference from v6: React Router v7 shifts from file-system routing to a declarative route configuration while maintaining SSR capabilities previously only in Remix.
Routing Architecture
Project Structure
app/
├── routes.ts # Central route configuration
├── root.tsx # Root layout component
├── routes/ # Route group definitions
│ ├── auth.route.ts # Authentication routes
│ ├── address.route.ts # Address management routes
│ ├── modules.route.ts # Dynamic module routes
│ ├── seo.route.ts # SEO configuration routes
│ ├── websiteManagement.route.ts
│ └── ...
├── pages/ # Page components
│ ├── index.page.tsx # Dashboard homepage
│ ├── errors.page.tsx # Error boundary page
│ ├── addresses.page.tsx # Address list page
│ └── ...
└── components/
└── template/
└── layout.template.tsx # Main dashboard layout
Route Configuration Flow
react-router.config.ts (SSR settings)
↓
app/routes.ts (route definitions)
↓
app/root.tsx (root layout + QueryClient)
↓
components/template/layout.template.tsx (dashboard layout)
↓
app/pages/*.page.tsx (page components)
Route Configuration
Central Configuration (app/routes.ts)
import { type RouteConfig, index, layout } from "@react-router/dev/routes";
export default [
// Dashboard layout wrapper for all authenticated routes
layout("components/template/layout.template.tsx", [
index("pages/index.page.tsx"), // Dashboard home: /
...AuthRoutes, // /connexion, /mot-de-passe-oublie, etc.
...AddressRoutes, // /adresses/*
...WebsiteManagementRoutes, // /gestion-site/*
...SeoRoutes, // /seo/*
...ModulesRoutes, // /modules/:moduleSlug/*
]),
] satisfies RouteConfig;
Route Group Example (app/routes/auth.route.ts)
import { type RouteConfig, route } from "@react-router/dev/routes";
export const AuthRoutes: RouteConfig = [
route("connexion", "pages/login.page.tsx"),
route("mot-de-passe-oublie", "pages/forgotPassword.page.tsx"),
route("reinitialiser-mot-de-passe", "pages/resetPassword.page.tsx"),
route("deconnexion", "pages/logout.page.tsx"),
];
Dynamic Routes Example (app/routes/modules.route.ts)
import { type RouteConfig, route } from "@react-router/dev/routes";
export const ModulesRoutes: RouteConfig = [
// Module list page
route("modules", "pages/modules.page.tsx"),
// Dynamic module detail pages
route("modules/:moduleSlug", "pages/moduleDetail.page.tsx", [
// Nested routes within module
route("nouveau", "pages/moduleCreation.page.tsx"),
route(":itemSlug", "pages/moduleEdit.page.tsx"),
]),
];
Generated URLs:
/modules- Module list/modules/news- News module homepage/modules/news/nouveau- Create new news article/modules/news/summer-sale-2025- Edit specific article
File-Based Routing
Naming Conventions
Page Files: {name}.page.tsx
// app/pages/addresses.page.tsx
export default function AddressesPage() {
return <div>Addresses List</div>;
}
Route Files: {group}.route.ts
// app/routes/address.route.ts
export const AddressRoutes: RouteConfig = [
route("adresses", "pages/addresses.page.tsx"),
route("adresses/nouveau", "pages/addressCreation.page.tsx"),
route("adresses/:slug", "pages/addressDetail.page.tsx"),
];
Route Parameters
Path Parameters:
// Route definition
route("modules/:moduleSlug/:itemSlug", "pages/moduleEdit.page.tsx")
// In component
import { useParams } from "react-router";
function ModuleEditPage() {
const { moduleSlug, itemSlug } = useParams();
// moduleSlug: "news"
// itemSlug: "summer-sale-2025"
return <div>Editing {itemSlug} in {moduleSlug}</div>;
}
Optional Parameters:
route("search/:query?", "pages/search.page.tsx")
// Matches both:
// /search
// /search/articles
Wildcard Routes:
route("docs/*", "pages/docs.page.tsx")
// Matches all:
// /docs/api
// /docs/api/authentication
// /docs/getting-started/installation
Layout System
Root Layout (app/root.tsx)
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Outlet /> {/* Child routes render here */}
</QueryClientProvider>
);
}
Dashboard Layout (components/template/layout.template.tsx)
import { Outlet } from "react-router";
import Menu from "~/components/organisms/menu.organism";
import { AppProvider } from "~/context/appProvider";
export default function DashboardLayout() {
return (
<AppProvider>
<div className="flex h-screen">
{/* Persistent sidebar navigation */}
<Menu />
{/* Main content area - changes per route */}
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
</AppProvider>
);
}
Nested Layouts
// Route configuration
layout("components/template/layout.template.tsx", [
layout("components/template/settingsLayout.template.tsx", [
route("parametres/profil", "pages/settings/profile.page.tsx"),
route("parametres/securite", "pages/settings/security.page.tsx"),
]),
])
// settingsLayout.template.tsx
export default function SettingsLayout() {
return (
<div>
<SettingsSidebar />
<Outlet /> {/* Settings pages render here */}
</div>
);
}
Layout Hierarchy:
Root Layout (HTML, QueryClient)
└─ Dashboard Layout (Menu, AppProvider)
└─ Settings Layout (SettingsSidebar)
└─ Profile Page
Route Groups
Authentication Routes
// app/routes/auth.route.ts
export const AuthRoutes: RouteConfig = [
route("connexion", "pages/login.page.tsx"),
route("mot-de-passe-oublie", "pages/forgotPassword.page.tsx"),
route("reinitialiser-mot-de-passe", "pages/resetPassword.page.tsx"),
route("deconnexion", "pages/logout.page.tsx"),
];
URLs:
/connexion- Login page/mot-de-passe-oublie- Forgot password/reinitialiser-mot-de-passe?t={token}- Reset password/deconnexion- Logout
Address Management Routes
// app/routes/address.route.ts
export const AddressRoutes: RouteConfig = [
route("adresses", "pages/addresses.page.tsx"),
route("adresses/nouveau", "pages/addressCreation.page.tsx"),
route("adresses/:slug", "pages/addressDetail.page.tsx", [
route("informations", "pages/addressInformation.page.tsx"),
route("horaires", "pages/addressHours.page.tsx"),
route("reseaux-sociaux", "pages/addressSocials.page.tsx"),
]),
];
URLs:
/adresses- Address list/adresses/nouveau- Create address/adresses/paris-office- Address detail/adresses/paris-office/informations- Edit basic info/adresses/paris-office/horaires- Edit opening hours/adresses/paris-office/reseaux-sociaux- Edit social links
Dynamic Module Routes
// app/routes/modules.route.ts
export const ModulesRoutes: RouteConfig = [
route("modules", "pages/modules.page.tsx"),
route("modules/:moduleSlug", "pages/moduleList.page.tsx"),
route("modules/:moduleSlug/nouveau", "pages/moduleCreation.page.tsx"),
route("modules/:moduleSlug/:itemSlug", "pages/moduleEdit.page.tsx"),
];
Example URLs:
/modules- Module management/modules/news- News article list/modules/news/nouveau- Create news article/modules/news/summer-sale-2025- Edit specific article/modules/events/christmas-market- Edit event/modules/products/widget-deluxe- Edit product
Navigation
Programmatic Navigation
import { useNavigate } from "react-router";
function MyComponent() {
const navigate = useNavigate();
// Simple navigation
navigate("/adresses");
// With state
navigate("/modules/news/nouveau", {
state: { referenceId: "123" }
});
// Replace (no history entry)
navigate("/connexion", { replace: true });
// Relative navigation
navigate("../"); // Go up one level
navigate("./nouveau"); // Sibling route
// With query params
navigate("/modules/news?lang=en&ref=123");
return <button onClick={() => navigate("/adresses")}>Go to Addresses</button>;
}
Link Components
import { Link, NavLink } from "react-router";
// Standard Link
<Link to="/adresses">Addresses</Link>
// NavLink with active styling
<NavLink
to="/modules/news"
className={({ isActive }) => isActive ? "text-primary font-bold" : "text-gray-600"}
>
News
</NavLink>
// Custom Button component (from atoms)
import Button from "~/components/atoms/button.atom";
<Button to="/adresses/nouveau" variant="primary">
New Address
</Button>
Form Navigation (Actions)
import { Form } from "react-router";
// Form submission triggers navigation on success
<Form method="post" action="/modules/news/nouveau">
<input name="title" />
<button type="submit">Create</button>
</Form>
Server-Side Rendering (SSR)
SSR Configuration
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: true, // Enable SSR (default in v7)
} satisfies Config;
Data Loading (Loaders)
// app/pages/addresses.page.tsx
import type { Route } from "./+types/addresses";
import addressApi from "~/api/address.api";
// Server-side data fetching
export async function loader({ request }: Route.LoaderArgs) {
const addresses = await addressApi.getAll();
return {
addresses,
timestamp: new Date().toISOString(),
};
}
// Component receives typed loader data
export default function AddressesPage({ loaderData }: Route.ComponentProps) {
const { addresses, timestamp } = loaderData;
return (
<div>
<h1>Addresses</h1>
<p>Loaded at: {timestamp}</p>
{addresses.map(address => (
<div key={address.id}>{address.name}</div>
))}
</div>
);
}
Type Safety
// Automatic type generation from loaders
import type { Route } from "./+types/addresses";
// LoaderData type is inferred from loader return
export default function AddressesPage({ loaderData }: Route.ComponentProps) {
// TypeScript knows loaderData.addresses is IAddress[]
loaderData.addresses.forEach(address => {
console.log(address.name); // ✅ Type-safe
});
}
Protected Routes
Authentication Guard
// app/utils/authGuard.ts
import { redirect } from "react-router";
import Credential from "~/api/credential.api";
export async function requireAuth(request: Request) {
try {
const user = await Credential.getProfile();
if (!user) {
const url = new URL(request.url);
throw redirect(`/connexion?redirect=${url.pathname}`);
}
return user;
} catch (error) {
throw redirect("/connexion");
}
}
Using in Loaders
// app/pages/dashboard.page.tsx
import { requireAuth } from "~/utils/authGuard";
export async function loader({ request }: Route.LoaderArgs) {
// Redirects to login if not authenticated
const user = await requireAuth(request);
// Fetch dashboard data (user is authenticated)
const stats = await statApi.getAll();
return { user, stats };
}
export default function DashboardPage({ loaderData }: Route.ComponentProps) {
const { user, stats } = loaderData;
return (
<div>
<h1>Welcome, {user.username}!</h1>
{/* Dashboard content */}
</div>
);
}
Role-Based Access
export async function requireRole(request: Request, allowedRoles: string[]) {
const user = await requireAuth(request);
if (!allowedRoles.includes(user.role)) {
throw redirect("/non-autorise");
}
return user;
}
// Usage
export async function loader({ request }: Route.LoaderArgs) {
await requireRole(request, ["admin", "editor"]);
// Only admins and editors reach here
}
Query Parameters
Reading Query Params
import { useSearchParams } from "react-router";
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
// Read params
const query = searchParams.get("q");
const page = searchParams.get("page") || "1";
const lang = searchParams.get("lang") || "fr";
// Update params
const handleSearch = (newQuery: string) => {
setSearchParams({ q: newQuery, page: "1" });
// URL becomes: /search?q=newQuery&page=1
};
return (
<div>
<input
value={query || ""}
onChange={(e) => handleSearch(e.target.value)}
/>
<p>Page: {page}</p>
</div>
);
}
Translation Query Params
// Translation system uses query parameters
// URL: /modules/news/article-123?lang=en&ref=456
import { useSearchParams } from "react-router";
function TranslationEditor() {
const [searchParams] = useSearchParams();
const currentLang = searchParams.get("lang") || "fr";
const referenceId = searchParams.get("ref");
// Switch language
const switchLanguage = (newLang: string) => {
setSearchParams({ lang: newLang, ref: referenceId });
};
return (
<div>
<LanguageTabs currentLang={currentLang} onSwitch={switchLanguage} />
<TranslationForm lang={currentLang} referenceId={referenceId} />
</div>
);
}
Error Handling
Error Boundary (app/root.tsx)
import { isRouteErrorResponse } from "react-router";
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
// HTTP errors (404, 500, etc.)
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
// JavaScript errors
return (
<div>
<h1>Oops! Something went wrong</h1>
<p>{error.message}</p>
</div>
);
}
Custom Error Pages
// app/pages/errors.page.tsx
import { useRouteError, isRouteErrorResponse } from "react-router";
export default function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
switch (error.status) {
case 404:
return <NotFoundPage />;
case 403:
return <UnauthorizedPage />;
case 500:
return <ServerErrorPage />;
default:
return <GenericErrorPage error={error} />;
}
}
return <UnexpectedErrorPage error={error} />;
}
Throwing Errors in Loaders
export async function loader({ params }: Route.LoaderArgs) {
const address = await addressApi.getOneBySlug(params.slug);
if (!address) {
// Triggers ErrorBoundary with 404
throw new Response("Address not found", { status: 404 });
}
return { address };
}
Best Practices
1. Route Organization
✅ DO:
// Group related routes in separate files
app/routes/
├── auth.route.ts # All auth routes
├── address.route.ts # All address routes
└── modules.route.ts # All module routes
❌ DON'T:
// Mix unrelated routes in one file
app/routes/
└── allRoutes.route.ts # Everything mixed together
2. Page Component Naming
✅ DO:
// Clear, descriptive names with .page.tsx suffix
app/pages/
├── addresses.page.tsx
├── addressDetail.page.tsx
└── addressCreation.page.tsx
❌ DON'T:
// Generic names without context
app/pages/
├── list.tsx
├── detail.tsx
└── create.tsx
3. Use Layouts for Shared UI
✅ DO:
// Persistent navigation in layout
layout("components/template/layout.template.tsx", [
route("adresses", "pages/addresses.page.tsx"),
route("modules", "pages/modules.page.tsx"),
])
❌ DON'T:
// Duplicate navigation in every page
export default function AddressesPage() {
return (
<>
<Menu /> {/* Duplicated on every page */}
<AddressesList />
</>
);
}
4. Type-Safe Navigation
✅ DO:
// Use constants for routes
export const ROUTES = {
ADDRESSES: "/adresses",
ADDRESS_DETAIL: (slug: string) => `/adresses/${slug}`,
ADDRESS_CREATE: "/adresses/nouveau",
} as const;
navigate(ROUTES.ADDRESS_DETAIL("paris-office"));
❌ DON'T:
// Magic strings everywhere
navigate("/adresses/paris-office");
navigate("/addresses/paris-office"); // Typo! Different URL
5. Use Loaders for Data Fetching
✅ DO:
// Server-side data loading
export async function loader() {
return await addressApi.getAll();
}
export default function AddressesPage({ loaderData }: Route.ComponentProps) {
return <AddressesList addresses={loaderData} />;
}
❌ DON'T:
// Client-side only fetching
export default function AddressesPage() {
const [addresses, setAddresses] = useState([]);
useEffect(() => {
addressApi.getAll().then(setAddresses);
}, []);
// Delays render, no SSR benefits
}
6. Protect Sensitive Routes
✅ DO:
export async function loader({ request }: Route.LoaderArgs) {
await requireAuth(request);
return await getAdminData();
}
❌ DON'T:
export default function AdminPage() {
const { user } = useUser();
if (!user) {
// Client-side check only, page briefly visible
return <Navigate to="/connexion" />;
}
}
7. Use Query Params for Filters
✅ DO:
// URL: /modules/news?search=summer&page=2&sort=-date
const [searchParams] = useSearchParams();
const search = searchParams.get("search");
const page = searchParams.get("page");
const sort = searchParams.get("sort");
// Bookmarkable, shareable URLs
❌ DON'T:
// State-only filters
const [filters, setFilters] = useState({ search: "", page: 1 });
// Not bookmarkable, lost on refresh
Common Patterns
Breadcrumb Integration
import { useBreadcrumbs } from "~/hooks/breadcrumbs/useBreadcrumbs.hook";
export default function AddressDetailPage({ loaderData }: Route.ComponentProps) {
const { addTemplatesToBreadcrumbs } = useBreadcrumbs();
const breadcrumbs = addTemplatesToBreadcrumbs([
{ label: "Accueil", url: "/" },
{ label: "Adresses", url: "/adresses" },
{ label: loaderData.address.name },
]);
return (
<div>
<Breadcrumb model={breadcrumbs} />
{/* Page content */}
</div>
);
}
Multi-Step Forms
// Route structure
route("adresses/nouveau", "pages/addressCreation.page.tsx", [
route("etape-1", "pages/addressCreation/step1.page.tsx"),
route("etape-2", "pages/addressCreation/step2.page.tsx"),
route("etape-3", "pages/addressCreation/step3.page.tsx"),
])
// Navigation between steps
const navigate = useNavigate();
const goToNextStep = () => navigate("../etape-2", { state: formData });
Modal Routes
// Show modal over current page
navigate("/modules/news/article-123/delete", {
state: { background: location }
});
// In route component
export default function DeleteConfirmModal() {
const location = useLocation();
const background = location.state?.background;
const handleClose = () => {
if (background) {
navigate(-1); // Go back to background page
} else {
navigate("/modules/news");
}
};
return <Dialog visible onHide={handleClose}>...</Dialog>;
}
Migration from React Router v6
Key Changes
| v6 | v7 |
|---|---|
| File-system routing | Declarative route config |
createBrowserRouter | Built-in with SSR |
| No SSR by default | SSR enabled by default |
useLoaderData() | loaderData prop |
json() response | Direct object return |
| Manual lazy loading | Automatic code splitting |
Migration Steps
- Update dependencies:
npm install react-router@7 @react-router/dev@7 @react-router/serve@7
- Create route configuration:
// routes.ts (new)
export default [
layout("components/template/layout.template.tsx", [
index("pages/index.page.tsx"),
route("about", "pages/about.page.tsx"),
]),
] satisfies RouteConfig;
- Update loaders:
// v6
export async function loader() {
return json({ data: await fetchData() });
}
// v7
export async function loader() {
return { data: await fetchData() }; // Direct return
}
- Update components:
// v6
export default function Page() {
const data = useLoaderData();
}
// v7
export default function Page({ loaderData }: Route.ComponentProps) {
// loaderData is typed automatically
}
Troubleshooting
Issue: Routes not found (404)
Solution: Check route configuration matches URL exactly.
// ❌ Wrong
route("address", "pages/addresses.page.tsx") // /address
navigate("/addresses") // 404!
// ✅ Correct
route("addresses", "pages/addresses.page.tsx") // /addresses
navigate("/addresses") // Works!
Issue: Layout not persisting
Solution: Ensure <Outlet /> is in layout component.
export default function DashboardLayout() {
return (
<div>
<Menu />
<Outlet /> {/* Required for child routes */}
</div>
);
}
Issue: Query params not updating
Solution: Use setSearchParams instead of manual URL manipulation.
// ❌ Wrong
navigate(`/search?q=${query}`);
// ✅ Correct
setSearchParams({ q: query });
Resources
- Official Docs: https://reactrouter.com/
- Migration Guide: https://reactrouter.com/upgrading/v6
- GitHub: https://github.com/remix-run/react-router
- Examples:
/docsfolder in this project
Last Updated: November 5, 2025
Maintained By: InLeed Development Team