Crée un back-office admin efficace en React Router 7 : routing imbriqué, gestion produits, validation Zod et interface épurée.
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é
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 ?
<Outlet />
affiche la page active (routing imbriqué).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.DataTable
vient de shadcn/ui : pagination, tri et
filtre déjà inclus.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é.
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.ToggleStatusSchema
(Zod) garantit que productSlug
existe.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}
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})
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()
renvoie hasUpdatedSlug
→ redirection si besoin.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
).
useState
.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 !
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 ?