E-Commerce
Règles
- État du panier : store Zustand avec persistance
localStorage — survit au rechargement de la page et à la fermeture de l'onglet
- Variantes de produits : modélisées en tant que SKU distincts avec un produit parent partagé — chaque variante a son propre prix, stock et image
- Paiement : Sessions de paiement Stripe (côté serveur) — n'envoyez jamais les prix depuis le client
- Stocks : vérifiez le stock côté serveur avant de créer une session de paiement — protection contre les conditions de concurrence avec des transactions de base de données
- Cycle de vie des commandes :
pending → paid → shipped → delivered — mise à jour via des événements webhook, pas par des actions client
- Liste des produits : filtrage/tri/pagination côté serveur — paramètres d'URL de recherche pour des vues filtrées partageables
- Prix : stockés en centimes (entiers) — affichés avec
Intl.NumberFormat et le code de la devise
- Opérations sur le panier : ajouter, supprimer, mettre à jour la quantité, vider — valider le stock à chaque action sur le panier
Patterns
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface CartItem { productId: string; variantId: string; quantity: number; price: number; }
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (variantId: string) => void;
updateQuantity: (variantId: string, quantity: number) => void;
clear: () => void;
total: () => number;
}
export const useCart = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find((i) => i.variantId === item.variantId);
if (existing) {
return { items: state.items.map((i) =>
i.variantId === item.variantId ? { ...i, quantity: i.quantity + item.quantity } : i
)};
}
return { items: [...state.items, item] };
}),
removeItem: (variantId) => set((s) => ({ items: s.items.filter((i) => i.variantId !== variantId) })),
updateQuantity: (variantId, quantity) => set((s) => ({
items: quantity <= 0
? s.items.filter((i) => i.variantId !== variantId)
: s.items.map((i) => (i.variantId === variantId ? { ...i, quantity } : i)),
})),
clear: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}),
{ name: "cart-storage" }
)
);
À éviter
- Envoyer les prix depuis le client pour créer un paiement — résolvez toujours les prix côté serveur à partir de votre base de données
- Stocker le panier uniquement dans l'état React — utilisez Zustand persistant ou localStorage pour que le panier survive au rechargement
- Déduire le stock lors de l'ajout au panier — réservez le stock uniquement lors du paiement, libérez-le à l'expiration de la session
- Mettre à jour le statut de la commande depuis le client — utilisez les webhooks Stripe pour
checkout.session.completed
- Utiliser des flottants pour l'argent — stockez en entiers (centimes), formatez uniquement pour l'affichage