Ajout d’un back-office admin avec React Router 7
Crée un back-office admin efficace en React Router 7 : routing imbriqué, gestion produits, validation Zod et interface épurée.
Ajoute un back-office admin sans douleur
1 Préparer un layout dédié
Nous isolons l’interface d’administration dans un dossier admin+.
L’underscore final + permet au file-based routing d’ajouter automatiquement le
préfixe /admin à chaque page.
1import { SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar"2import { AdminSidebar } from "./_adminLayout"34export default function AdminLayout() {5return (6<SidebarProvider>7<AdminSidebar /> {/* sidebar Shadcn – 100 % responsive */}8<SidebarTrigger className="-ml-1" />910{/* contenu injecté par les routes enfants */}11<main className="flex flex-1 flex-col gap-4 p-4">12<Outlet />13</main>14</SidebarProvider>15)16}
Pourquoi ?
- La barre latérale reste visible sur toutes les pages admin.
- Le composant
<Outlet />affiche la page active (routing imbriqué).
2 Déclarer la liste des produits
Une page index affiche le tableau :
1export async function loader() {2return { products: await adminGetProducts() }3}45export default function ProductsPage() {6const { products } = useLoaderData<typeof loader>()7return (8<DataTable9columns={adminProductsColumns}10data={products}11searchPlaceholder="Rechercher..."12/>13)14}
loader()récupère tous les produits en base via Prisma.DataTablevient de shadcn/ui : pagination, tri et filtre déjà inclus.
3 Colonnes dynamiques et actions
1export const adminProductsColumns: ColumnDef<Product>[] = [2{ accessorKey: "name", header: "Nom", cell: NameCell },3{ accessorKey: "priceCents", header: "Prix", cell: PriceCell },4{ accessorKey: "stock", header: "Stock", cell: StockBadge },5{6accessorKey: "isActive",7header: "Statut",8cell: ({ row }) => <ProductStatusToggle product={row.original} />,9},10{ id: "actions", header: "Actions", cell: ActionsCell },11]
Chaque colonne est typée – l’autocomplétion évite les fautes de clé.
3.1 Activer / désactiver un produit
1const ProductStatusToggle = ({ product }: { product: Product }) => {2const fetcher = useFetcher()3const [form, fields] = useForm({4constraint: getZodConstraint(ToggleStatusSchema),5onValidate: ({ formData }) =>6parseWithZod(formData, { schema: ToggleStatusSchema }),7})89return (10<fetcher.Form method="POST" {...getFormProps(form)} style={{ display:"contents" }}>11<input {...getInputProps(fields.intent, { type:"hidden" })} value="toggle-status" />12<input {...getInputProps(fields.productSlug,{ type:"hidden" })} value={product.slug} />13<Switch14checked={product.isActive}15onClick={(e) => fetcher.submit(e.currentTarget.form)}16/>17</fetcher.Form>18)19}
useFetcher()déclenche l’action sans navigation.- La validation
ToggleStatusSchema(Zod) garantit queproductSlugexiste.
3.2 Suppression protégée
Un AlertDialog confirme l’intention, puis :
1formData.set("intent", "delete-product")
Le même action() gère les deux cas grâce à une Discriminated Union :
1export const ActionSchema = z.discriminatedUnion("intent", [2ToggleStatusSchema,3DeleteProductSchema,4])56switch (submission.value.intent) {7case "toggle-status": await toggleProductStatus(slug); break8case "delete-product": await deleteProduct(slug); break9}
4 Créer / éditer un produit
Route dynamique products.$productSlug.tsx.
1export const ProductSchema = z.object({2name: z.string().min(1),3slug: z.string().min(1),4description: z.string().optional(),5content: z.string().optional(),6priceCents: z.coerce.number().min(0),7currency: z.string().default("EUR"),8stock: z.coerce.number().min(0),9isActive: z.boolean().default(false),10})
- Même schéma pour création et édition.
isSlugTaken()vérifie l’unicité avant d’accepter la requête.
Le formulaire Conform réutilise les <Field> de l’Epic Stack :
1<Field2labelProps={{ children:"Slug" }}3inputProps={{4...getInputProps(fields.slug,{ type:"text" }),5placeholder:"nouveau-robot-ultra-pro",6}}7errors={fields.slug.errors}8/>
Après validation :
createProduct()insère en BDD puis redirige vers le slug créé.updateProduct()renvoiehasUpdatedSlug→ redirection si besoin.
5 Séparer layout public et admin
En déplaçant Navbar + Footer dans _publicLayout.tsx :
1<Navbar />2<main className="flex-1">3<Outlet />4</main>5<Footer />
Le back-office reste épuré (pas de barre publique dans /admin).
6 Ce que tu gagnes
- Interface admin complète en une seule branche.
- Code lisible : Zod + Conform gèrent la validation client/serveur sans
useState. - Sidebar Shadcn : responsive et réutilisable.
- Actions isolées par
useFetcher(): pas de rechargement global.
La base pour gérer produits, stock et statut est posée : tu es prêt pour ajouter la gestion des images et l’authentification admin !
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 ?