Apprends à générer une commande fiable avec React Router 7 : validation Zod, décrémentation stock, création Order Prisma puis redirection vers Checkout Stripe.
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 terminons l’intégration Stripe : avant de lancer le checkout, il nous faut d’abord créer une vraie commande dans la base. Objectif de cette étape :
orders.server.ts
;orderId
pour la redirection.customer
et fichier orders.server.ts
1import type { Prisma } from "generated/prisma/client";2import { prisma } from "~/server/db.server";34/* ----------------------------------------------------------- */5/* helper – vérifie stock + construit chaque ligne de commande */6/* ----------------------------------------------------------- */7async function makeOrderItems(8cartItems: { productId: string; quantity: number }[],9) {10const ids = cartItems.map((i) => i.productId);11const products = await prisma.product.findMany({12where: { id: { in: ids }, isActive: true },13});1415if (products.length !== ids.length) {16throw new Error("Product missing or inactive");17}1819let subtotal = 0;20const items: Prisma.OrderItemCreateWithoutOrderInput[] = [];2122for (const ci of cartItems) {23const p = products.find((pr) => pr.id === ci.productId)!;2425if (p.stock < ci.quantity) {26throw new Error(`Stock insuffisant pour ${p.name}`);27}2829subtotal += p.priceCents * ci.quantity;3031items.push({32productId: p.id,33productName: p.name,34quantity: ci.quantity,35unitPriceCents: p.priceCents,36totalPriceCents: p.priceCents * ci.quantity,37stripePriceId: p.stripePriceId,38});39}4041return { items, subtotal };42}4344/* ----------------------------- */45/* API principale : createOrder */46/* ----------------------------- */47export async function createOrder({48cartItems,49userId,50guestEmail,51}: {52cartItems: { productId: string; quantity: number }[];53userId?: string;54guestEmail?: string;55}) {56if (!cartItems.length) throw new Error("Panier vide");5758if (!userId && !guestEmail) {59throw new Error("userId ou guestEmail requis");60}6162const { items, subtotal } = await makeOrderItems(cartItems);6364const order = await prisma.order.create({65data: {66userId,67guestEmail,68subtotalCents: subtotal,69totalCents: subtotal, // pas de taxe/livraison pour l’instant70currency: "EUR",71orderStatus: "DRAFT",72paymentStatus: "PENDING",73deliveryStatus: "PENDING",74items: { create: items },75},76});7778/* décrémente le stock */79await Promise.all(80items.map((it) =>81prisma.product.update({82where: { id: it.productId! },83data: { stock: { decrement: it.quantity } },84}),85),86);8788return order;89}
Pourquoi ?
1import { z } from "zod";23export const CreateOrderSchema = z.object({4intent: z.literal("create-order"),5items: z6.array(7z.object({8productId: z.string(),9quantity: z.number().int().positive(),10}),11)12.min(1, "Le panier ne peut pas être vide"),13guestEmail: z.string().email().optional(),14});
1import { badRequest, parseWithZod } from "@conform-to/zod";2import { createOrder } from "~/server/customer/orders.server";3import { getOptionalUser } from "~/server/auth.server";45export async function action({ request }: ActionFunctionArgs) {6const fd = await request.formData();7const submission = parseWithZod(fd, { schema: CreateOrderSchema });89if (submission.status !== "success") {10return badRequest(submission.reply());11}1213const user = await getOptionalUser(request);1415/* création effective */16const order = await createOrder({17cartItems: submission.value.items,18userId: user?.id,19guestEmail: submission.value.guestEmail,20});2122/* redirection + flush cookie panier */23return redirect(`/orders/${order.id}?success=true`);24}
useCart
: ne stocker que l’identifiant + la quantitéAu lieu de garder la copie complète du produit dans localStorage
, on
enregistre uniquement { productId, quantity }
.
En SSR, la route root charge la version « fraîche » depuis la base,
garantissant que le nom, le prix et le stock sont toujours à jour.
1type LSItem = { productId: string; quantity: number };23const [rawCart, setRawCart] = useLocalStorage<LSItem[]>("cart", []);45const products = useLoaderData<typeof rootLoader>().products; // infos fraîches67const cart = useMemo(() => {8return rawCart9.map((rc) => ({10product: products.find((p) => p.id === rc.productId)!,11quantity: rc.quantity,12}))13.filter((ci) => ci.product); // produit supprimé => item ignoré14}, [rawCart, products]);
1import { useSearchParams } from "react-router";2import { useEffect } from "react";3import { useCartContext } from "~/contexts/cart-context";45export default function CartRoute() {6const { clearCart } = useCartContext();7const [sp, setSp] = useSearchParams();89useEffect(() => {10if (sp.get("success") === "true") {11clearCart();12sp.delete("success");13setSp(sp, { replace: true });14}15}, []);16/* … */17}
1<Form method="POST" {...getFormProps(form)} className="space-y-4">2<input type="hidden" name="intent" value="create-order" />3<input4type="hidden"5name="items"6value={JSON.stringify(cart.items.map(({ product, quantity }) => ({7productId: product.id,8quantity,9})))}10/>11<Button12type="submit"13className="w-full"14isLoading={navigation.state === "submitting"}15>16Commander maintenant17</Button>18</Form>
/orders/:id
puis panier vidé.createOrder
se charge du stock et fixe le prix au moment T ;?success=true
permet de vider le panier côté client ;checkout.session.completed
.Ton e-commerce est maintenant capable de générer une commande fiable ; on attaque le paiement dans la prochaine leçon !
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 ?