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.

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
45 leçons
Accès à vie
299.00
-50%

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.

Créer un layout route

Dans app/routes/_usersLayout.tsx, on définit la route parent pour /users. Elle contient :

  • Un loader pour charger la liste des utilisateurs.
  • Un NavLink pour ajouter un nouvel utilisateur.
  • Un <Outlet> où s’afficheront les pages enfants.
app/routes/_usersLayout.tsx
1
import {
2
href,
3
NavLink,
4
Outlet,
5
useLoaderData,
6
isRouteErrorResponse,
7
useRouteError,
8
type ActionFunctionArgs,
9
} from 'react-router';
10
import { getUsers, addUser } from '~/users.server';
11
12
export async function loader() {
13
return { users: await getUsers() }; // ^? Chargement serveur
14
}
15
16
export default function UsersLayout() {
17
const { users } = useLoaderData<typeof loader>();
18
return (
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
<NavLink
24
to={href('/users/:userSlug', { userSlug: 'new' })}
25
className="px-3 py-1 bg-green-600 text-white rounded"
26
>
27
Ajouter
28
</NavLink>
29
</div>
30
{users.map((u) => (
31
<li key={u.id}>
32
<NavLink
33
to={href('/users/:userSlug', { userSlug: u.slug })}
34
className={({ isActive }) =>
35
isActive
36
? '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 enfant
47
</div>
48
</div>
49
);
50
}
51
52
export async function action({ request }: ActionFunctionArgs) {
53
const form = await request.formData();
54
await addUser({ name: form.get('name') as string });
55
return null;
56
}
57
58
export function ErrorBoundary() {
59
const error = useRouteError();
60
if (isRouteErrorResponse(error)) {
61
return <p className="text-red-500">{error.statusText}</p>;
62
}
63
return <p className="text-red-500">Erreur inattendue</p>;
64
}

Définir la route index

Quand tu accèdes à /users, tu veux un message par défaut.
Crée app/routes/users.index.tsx :

app/routes/users.index.tsx
1
export default function UsersIndex() {
2
return (
3
<h2 className="text-center text-gray-600">
4
Sélectionne un profil
5
</h2>
6
);
7
}

Puis, dans app/routes.ts, imbrique l’index sous /users :

app/routes.ts
1
import { index, route, type RouteConfig } from '@react-router/dev/routes';
2
3
export default [
4
index('routes/home.tsx'),
5
route('users', 'routes/_usersLayout.tsx', [
6
index('routes/users.index.tsx'),
7
route(':userSlug', 'routes/users.$userSlug.tsx'),
8
]),
9
] satisfies RouteConfig;

Créer la route dynamique enfant

Le fichier app/routes/users.$userSlug.tsx gère deux cas :

  1. userSlug === 'new' ⇒ formulaire de création
  2. userSlug existant ⇒ formulaire d’édition
app/routes/users.$userSlug.tsx
1
import {
2
Form,
3
useLoaderData,
4
useParams,
5
isRouteErrorResponse,
6
useRouteError,
7
redirect,
8
href,
9
} from 'react-router';
10
import { getUserBySlug, addUser, updateUser } from '~/users.server';
11
12
export async function loader({ params }) {
13
const slug = params.userSlug!;
14
if (slug === 'new') return { user: null };
15
const user = await getUserBySlug({ slug });
16
if (!user) throw new Response('Non trouvé', { status: 404 });
17
return { user };
18
}
19
20
export async function action({ request, params }) {
21
const form = await request.formData();
22
const name = form.get('name') as string;
23
const slug = params.userSlug!;
24
if (slug === 'new') {
25
const u = await addUser({ name });
26
return redirect(href('/users/:userSlug', { userSlug: u.slug }));
27
}
28
const u = await updateUser({ slug, name });
29
return redirect(href('/users/:userSlug', { userSlug: u.slug }));
30
}
31
32
export default function UserForm() {
33
const { user } = useLoaderData<typeof loader>();
34
const { userSlug } = useParams();
35
const isNew = userSlug === 'new';
36
return (
37
<Form method="post" className="space-y-2">
38
<h2 className="text-lg font-semibold">
39
{isNew ? 'Nouveau profil' : 'Modifier profil'}
40
</h2>
41
<input
42
name="name"
43
defaultValue={user?.name ?? ''}
44
className="w-full border px-2 py-1 rounded"
45
/>
46
<button
47
type="submit"
48
className={`px-4 py-1 text-white rounded ${
49
isNew ? 'bg-green-600' : 'bg-blue-600'
50
}`}
51
>
52
{isNew ? 'Créer' : 'Enregistrer'}
53
</button>
54
</Form>
55
);
56
}
57
58
export function ErrorBoundary() {
59
const error = useRouteError();
60
if (isRouteErrorResponse(error)) {
61
return <p className="text-red-600">{error.status} {error.statusText}</p>;
62
}
63
return <p className="text-red-600">Erreur inconnue</p>;
64
}

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 ErrorBoundary isolent les erreurs dans chaque niveau de route.
  • Le loader parent et enfant s’exécutent en parallèle pour optimiser le SSR.

Exercices rapides

  1. Supprimer un utilisateur

    • Ajoute un bouton « Supprimer » dans le formulaire.
    • Dans action, détecte formData.get('_method') === 'delete' et implémente la suppression.
  2. Page 404 personnalisée

    • Crée une route error.tsx globale avec un ErrorBoundary qui affiche un message sympa.
    • Redirige toute erreur non capturée vers cette page.
  3. Layout secondaire

    • Crée un layout pour /users/settings.
    • Imbrique-y deux pages /users/settings/profile et /users/settings/security.
Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours