Aller au contenu principal

Baldr Dashboard - Routing System Documentation

Framework: React Router v7 (SSR-enabled)
Last Updated: November 5, 2025
Version: 1.0.0


Table of Contents

  1. Overview
  2. Routing Architecture
  3. Route Configuration
  4. File-Based Routing
  5. Layout System
  6. Route Groups
  7. Navigation
  8. Server-Side Rendering (SSR)
  9. Protected Routes
  10. Query Parameters
  11. Error Handling
  12. 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

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>;
}
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

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 });
// 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

v6v7
File-system routingDeclarative route config
createBrowserRouterBuilt-in with SSR
No SSR by defaultSSR enabled by default
useLoaderData()loaderData prop
json() responseDirect object return
Manual lazy loadingAutomatic code splitting

Migration Steps

  1. Update dependencies:
npm install react-router@7 @react-router/dev@7 @react-router/serve@7
  1. 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;
  1. Update loaders:
// v6
export async function loader() {
return json({ data: await fetchData() });
}

// v7
export async function loader() {
return { data: await fetchData() }; // Direct return
}
  1. 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


Last Updated: November 5, 2025
Maintained By: InLeed Development Team