Comment utiliser une route imbriquée
Dans cette leçon on va apprendre à construire un layout Outlet : routes imbriquées index/détail, formulaire partagé création-édition et NavLink actif.
Dans cette leçon, tu vas découvrir comment tirer parti des routes imbriquées (nested routes) avec React Router 7. Grâce à ce pattern, tu pourras :
- Créer des layouts partagés (sidebars, headers) qui enveloppent plusieurs sous-pages.
- Charger data en parallèle pour le parent et l’enfant.
- Gérer la navigation et l’état actif de manière déclarative.
Allons-y pas à pas !
Qu’est-ce qu’une route imbriquée ?
Une route imbriquée permet à une route « parent » de rendre un composant partagé, tout en déléguant une partie de l’interface à une route enfant. Le composant <Outlet> indique où s’affiche le contenu de l’enfant.
Avantages
- Réutilisation de layout (barre latérale, en-tête)
- Chargements parallèles (loader parent + loader enfant)
- Erreurs isolées avec
ErrorBoundarypar niveau
Créer un layout route
Dans app/routes/_usersLayout.tsx, on définit la route parent pour /users. Elle contient :
- Un
loaderpour charger la liste des utilisateurs. - Un
NavLinkpour ajouter un nouvel utilisateur. - Un
<Outlet>où s’afficheront les pages enfants.
1import {2href,3NavLink,4Outlet,5useLoaderData,6isRouteErrorResponse,7useRouteError,8type ActionFunctionArgs,9} from 'react-router';10import { getUsers, addUser } from '~/users.server';1112export async function loader() {13return { users: await getUsers() }; // ^? Chargement serveur14}1516export default function UsersLayout() {17const { users } = useLoaderData<typeof loader>();18return (19<div className="flex gap-4 p-4">20<ul className="w-80 space-y-2">21<div className="flex justify-between items-center">22<h1 className="text-xl font-bold">Utilisateurs</h1>23<NavLink24to={href('/users/:userSlug', { userSlug: 'new' })}25className="px-3 py-1 bg-green-600 text-white rounded"26>27Ajouter28</NavLink>29</div>30{users.map((u) => (31<li key={u.id}>32<NavLink33to={href('/users/:userSlug', { userSlug: u.slug })}34className={({ isActive }) =>35isActive36? 'block p-2 bg-blue-600 text-white rounded'37: 'block p-2 hover:bg-gray-100 rounded'38}39>40{u.name}41</NavLink>42</li>43))}44</ul>45<div className="flex-1 bg-gray-50 p-4 rounded shadow">46<Outlet /> // ^? Zone enfant47</div>48</div>49);50}5152export async function action({ request }: ActionFunctionArgs) {53const form = await request.formData();54await addUser({ name: form.get('name') as string });55return null;56}5758export function ErrorBoundary() {59const error = useRouteError();60if (isRouteErrorResponse(error)) {61return <p className="text-red-500">{error.statusText}</p>;62}63return <p className="text-red-500">Erreur inattendue</p>;64}
Tip
Pense à créer un fichier users.server.ts pour simuler ton back-end.
Ce suffixe .server.ts n’est pas bundlé côté client.
Définir la route index
Quand tu accèdes à /users, tu veux un message par défaut.
Crée app/routes/users.index.tsx :
1export default function UsersIndex() {2return (3<h2 className="text-center text-gray-600">4Sélectionne un profil5</h2>6);7}
Puis, dans app/routes.ts, imbrique l’index sous /users :
1import { index, route, type RouteConfig } from '@react-router/dev/routes';23export default [4index('routes/home.tsx'),5route('users', 'routes/_usersLayout.tsx', [6index('routes/users.index.tsx'),7route(':userSlug', 'routes/users.$userSlug.tsx'),8]),9] satisfies RouteConfig;
File-based routing
La déclaration manuelle est claire, mais tu peux aussi explorer le file-based routing (fs-routes).
Créer la route dynamique enfant
Le fichier app/routes/users.$userSlug.tsx gère deux cas :
userSlug === 'new'⇒ formulaire de créationuserSlugexistant ⇒ formulaire d’édition
1import {2Form,3useLoaderData,4useParams,5isRouteErrorResponse,6useRouteError,7redirect,8href,9} from 'react-router';10import { getUserBySlug, addUser, updateUser } from '~/users.server';1112export async function loader({ params }) {13const slug = params.userSlug!;14if (slug === 'new') return { user: null };15const user = await getUserBySlug({ slug });16if (!user) throw new Response('Non trouvé', { status: 404 });17return { user };18}1920export async function action({ request, params }) {21const form = await request.formData();22const name = form.get('name') as string;23const slug = params.userSlug!;24if (slug === 'new') {25const u = await addUser({ name });26return redirect(href('/users/:userSlug', { userSlug: u.slug }));27}28const u = await updateUser({ slug, name });29return redirect(href('/users/:userSlug', { userSlug: u.slug }));30}3132export default function UserForm() {33const { user } = useLoaderData<typeof loader>();34const { userSlug } = useParams();35const isNew = userSlug === 'new';36return (37<Form method="post" className="space-y-2">38<h2 className="text-lg font-semibold">39{isNew ? 'Nouveau profil' : 'Modifier profil'}40</h2>41<input42name="name"43defaultValue={user?.name ?? ''}44className="w-full border px-2 py-1 rounded"45/>46<button47type="submit"48className={`px-4 py-1 text-white rounded ${49isNew ? 'bg-green-600' : 'bg-blue-600'50}`}51>52{isNew ? 'Créer' : 'Enregistrer'}53</button>54</Form>55);56}5758export function ErrorBoundary() {59const error = useRouteError();60if (isRouteErrorResponse(error)) {61return <p className="text-red-600">{error.status} {error.statusText}</p>;62}63return <p className="text-red-600">Erreur inconnue</p>;64}
Slug réservé
En réservant new comme slug, tu empêches un utilisateur de s’appeler new.
Points clés
- Utilise
<Outlet>dans le layout pour afficher les enfants. - Déclare un index route pour le contenu par défaut.
- Gère les cas creation/édition dans un fichier
users.$userSlug.tsx. - Les
ErrorBoundaryisolent les erreurs dans chaque niveau de route. - Le
loaderparent et enfant s’exécutent en parallèle pour optimiser le SSR.
Tip
Pour en savoir plus sur les routes dynamiques, consulte la leçon dédiée.
Exercices rapides
-
Supprimer un utilisateur
- Ajoute un bouton « Supprimer » dans le formulaire.
- Dans
action, détecteformData.get('_method') === 'delete'et implémente la suppression.
-
Page 404 personnalisée
- Crée une route
error.tsxglobale avec unErrorBoundaryqui affiche un message sympa. - Redirige toute erreur non capturée vers cette page.
- Crée une route
-
Layout secondaire
- Crée un layout pour
/users/settings. - Imbrique-y deux pages
/users/settings/profileet/users/settings/security.
- Crée un layout pour
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?