Liste des produits (catalogue e-commerce) avec React Router 7

Découvre comment créer une page catalogue produits en routing file-based avec React Router 7 et Prisma. Exemples clairs, UI moderne et code optimisé.

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
76 leçons
Accès à vie
119.80
-80%

Basculer sur le file-based routing

La branche main utilisait encore la configuration manuelle des routes. On passe au file-based routing pour :

  • ne plus toucher au fichier de configuration routes.ts;
  • détecter automatiquement chaque fichier .tsx dans app/routes.

Installation des plugins

install.sh
1
npm add -D @react-router/remix-routes-option-adapter remix-flat-routes

Nouveau fichier root.tsx

app/root.tsx
1
import {
2
Links,
3
Meta,
4
Outlet,
5
Scripts,
6
ScrollRestoration,
7
} from "react-router";
8
import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";
9
import { flatRoutes } from "remix-flat-routes";
10
11
/* … Links + Layout inchangés … */
12
13
export const routeOptions = remixRoutesOptionAdapter((defineRoutes) =>
14
flatRoutes("routes", defineRoutes),
15
);

Pourquoi ?

  • flatRoutes parcourt app/routes/** : plus besoin de déclarer les routes à la main.
  • L’auto-refresh de Vite recharge la liste quand tu ajoutes un fichier.

Remplacer les decimal par des integer en centimes

decimal apporte une précision inutile pour un petit catalogue et complique les calculs. On stocke désormais le prix en centimes.

Diff du schéma

prisma/schema.prisma
1
-model Product {
2
- price Decimal @db.Decimal(10, 2)
3
+model Product {
4
+ priceCents Int @default(0)

Même traitement pour Order, OrderItem, Invoice.

Migration

Terminal
1
npx prisma migrate dev --name replace_decimal_par_integer

Prisma réclame une valeur par défaut car des lignes existent déjà : on fixe @default(0) puis on accepte la suppression de l’ancienne colonne price.


Adapter le seed

prisma/seed.ts
1
await prisma.product.create({
2
data: {
3
name,
4
slug: `${slug(name)}-${i}`,
5
description: `Description de ${name}`,
6
priceCents: Math.round(Math.random() * 15000) + 500, // 5 € – 150 €
7
stock: Math.floor(Math.random() * 100),
8
},
9
});

Exécution :

Terminal
1
npx prisma db seed

Extraire la requête Prisma dans un fichier serveur

app/server/products.server.ts
1
import { prisma } from "./db.server";
2
3
export async function getProducts() {
4
const products = await prisma.product.findMany({
5
select: {
6
id: true,
7
name: true,
8
description: true,
9
priceCents: true,
10
},
11
});
12
13
// image placeholder stable (Picsum)
14
return products.map((p) => ({
15
...p,
16
imageUrl: `https://picsum.photos/seed/${p.id}/600/400`,
17
}));
18
}

Pourquoi ?

  • Centralise la logique DB.
  • Le type est inféré : Awaited<ReturnType<typeof getProducts>>[number].

Page liste des produits

Route app/routes/products.tsx

app/routes/products.tsx
1
import { useLoaderData } from "react-router";
2
import { getProducts } from "~/server/products.server";
3
import { ProductList } from "~/components/products/ProductList";
4
5
export async function loader() {
6
return getProducts();
7
}
8
9
export default function ProductsRoute() {
10
const products = useLoaderData<typeof loader>();
11
12
return (
13
<main className="flex justify-center pt-12 pb-8">
14
<div className="w-full max-w-7xl px-4">
15
<header className="mb-6">
16
<h1 className="text-3xl font-semibold text-primary">Catalogue</h1>
17
</header>
18
19
<ProductList products={products} />
20
</div>
21
</main>
22
);
23
}

Composants UI shadcn

<ProductCard>

app/components/products/ProductCard.tsx
1
import { ShoppingCart } from "lucide-react";
2
import { Link } from "react-router";
3
import { Card } from "~/components/ui/card";
4
import { Button } from "~/components/ui/button";
5
import { cn } from "~/lib/utils";
6
import type { getProducts } from "~/server/products.server";
7
8
export function ProductCard({
9
product,
10
className,
11
}: {
12
product: Awaited<ReturnType<typeof getProducts>>[number];
13
className?: string;
14
}) {
15
return (
16
<Card
17
className={cn(
18
"relative flex h-full flex-col overflow-hidden rounded-none p-0 shadow",
19
className,
20
)}
21
>
22
{/* lien couvrant toute la carte */}
23
<Link
24
to={`/products/${product.slug}`}
25
className="absolute inset-0 z-10"
26
aria-label={`Voir ${product.name}`}
27
/>
28
29
{/* image 16/9 */}
30
<img
31
src={product.imageUrl}
32
alt={product.name}
33
className="aspect-video w-full object-cover"
34
/>
35
36
<div className="flex flex-grow flex-col p-2 lg:p-4">
37
<h3 className="truncate text-sm font-semibold lg:text-base">
38
{product.name}
39
</h3>
40
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground lg:text-sm">
41
{product.description}
42
</p>
43
44
<div className="mt-auto flex items-center justify-between pt-2">
45
<span className="text-sm font-medium lg:text-base">
46
{(product.priceCents / 100).toFixed(2)}{" €"}
47
</span>
48
<Button
49
size="sm"
50
className="rounded-none p-2 lg:p-2.5"
51
onClick={(e) => {
52
e.preventDefault(); // évite la navigation
53
/* TODO : addToCart(product.id) */
54
}}
55
>
56
<ShoppingCart className="h-4 w-4" />
57
</Button>
58
</div>
59
</div>
60
</Card>
61
);
62
}

<ProductList>

app/components/products/ProductList.tsx
1
import { ProductCard } from "./ProductCard";
2
import type { getProducts } from "~/server/products.server";
3
4
export function ProductList({
5
products,
6
}: {
7
products: Awaited<ReturnType<typeof getProducts>>;
8
}) {
9
return (
10
<section
11
className="grid auto-rows-fr gap-3 p-2
12
grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5
13
lg:gap-4 lg:p-4"
14
>
15
{products.map((p) => (
16
<ProductCard key={p.id} product={p} />
17
))}
18
</section>
19
);
20
}

Explications :

  • auto-rows-fr : toutes les cartes ont la même hauteur.
  • Responsive via classes Tailwind (grid-cols-2, md:grid-cols-3…).
  • Bouton « Ajouter au panier » ne casse pas le lien : e.preventDefault().

Mise à jour de la page d’accueil temporaire

Pour tester rapidement, l’index reprend la même requête :

tsx app/routes/index.tsx
1
-<h1 className="text-4xl …">Shop Router</h1>
2
+<h1 className="text-4xl …">Bienvenue sur Shop Router</h1>

Vérifier le tout

Terminal
1
npm run dev
  1. localhost:5173/products affiche 10 produits, cartes alignées sans bord-radius.
  2. Prix formatés (priceCents / 100).
  3. Page mobile : deux cartes visibles dès le premier viewport.

Versionner la progression

Terminal
1
git checkout -b 7-3-ajout-de-la-page-liste-des-produits
2
git add .
3
git commit -m "feat(products): list page + card UI + decimal→centimes"
4
git push -u origin 7-3-ajout-de-la-page-liste-des-produits

Pour aller plus loin

  • Ajouter une barre de recherche : useDebounce côté client + loader filtré.
  • Précharger les images LCP via la fonction links() (module SEO).
  • Brancher le panier avec useFetcher() pour une UI optimiste (prochaine leçon).

Ton catalogue est désormais prêt, typé et responsif – bravo !

Inclus
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 inclus en achetant ce cours