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.
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
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 :
Allons-y pas à pas !
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.
ErrorBoundary
par niveauDans app/routes/_usersLayout.tsx
, on définit la route parent pour /users
. Elle contient :
loader
pour charger la liste des utilisateurs.NavLink
pour ajouter un nouvel utilisateur.<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}
Pense à créer un fichier users.server.ts
pour simuler ton back-end.
Ce suffixe .server.ts
n’est pas bundlé côté client.
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;
La déclaration manuelle est claire, mais tu peux aussi explorer le file-based routing (fs-routes).
Le fichier app/routes/users.$userSlug.tsx
gère deux cas :
userSlug === 'new'
⇒ formulaire de créationuserSlug
existant ⇒ formulaire d’édition1import {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}
En réservant new
comme slug, tu empêches un utilisateur de s’appeler new
.
<Outlet>
dans le layout pour afficher les enfants.users.$userSlug.tsx
.ErrorBoundary
isolent les erreurs dans chaque niveau de route.loader
parent et enfant s’exécutent en parallèle pour optimiser le SSR.Pour en savoir plus sur les routes dynamiques, consulte la leçon dédiée.
action
, détecte formData.get('_method') === 'delete'
et implémente la suppression.error.tsx
globale avec un ErrorBoundary
qui affiche un message sympa./users/settings
./users/settings/profile
et /users/settings/security
.Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?