Apprends à valider les formulaires côté serveur avec Zod + Conform : unicité, logique métier, erreurs asynchrones et UX préservée grâce à submission.reply() et superRefine.
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é
La validation côté client (Conform + Zod) protège l’utilisateur des fautes de frappe, mais elle ne peut pas :
Bref : la dernière ligne de défense vit sur le serveur. Dans cette leçon tu vas apprendre à :
action()
;SubmissionResult
interprétable par Conform ;1import { z } from "zod"23export const UserCreateSchema = z.object({4action: z.literal("create-user"),5firstName: z.string().min(2, "2 caractères minimum"),6lastName: z.string().min(2, "2 caractères minimum"),7})89export const UserUpdateSchema = z.object({10action: z.literal("update-user"),11firstName: z.string().min(2),12lastName: z.string().min(2),13})1415export const UserActionsSchema = z.discriminatedUnion(16"action",17[UserCreateSchema, UserUpdateSchema],18)
Discriminated union 👉 un seul schéma, plusieurs cas, distingués par la clef action
.
Conform l’utilisera côté client et nous côté serveur : zéro duplication !
FormData
avec Zod côté serveur1import { parseWithZod } from "@conform-to/zod";2import { data } from "react-router";34export async function action({ request, params }) {5const formData = await request.formData()67const submission = await parseWithZod(formData, {8async: true, // autorise le code asynchrone9schema: UserActionsSchema.superRefine(async (values, ctx) => {10if (values.action === "create-user") {11const slug = createSlug({ firstName: values.firstName })12const exists = await getUserBySlug({ slug })13if (exists) {14ctx.addIssue({15path: ["firstName"],16code: "custom",17message: "Cet utilisateur existe déjà.",18})19}20}21}),22})2324if (submission.status !== "success") {25// ^ renvoie un format compris par Conform26return data({ result: submission.reply() }, { status: 400 })27}2829/* …traitement en BDD, puis redirect … */30}
parseWithZod(formData)
convertit FormData
→ objet et applique le schéma.superRefine
te donne un hook async pour parler à ta base.submission.reply()
sérialise les erreurs pour Conform.1import { useActionData, useLoaderData } from "react-router"2import { useForm, getFormProps, getInputProps } from "@conform-to/react"34export default function UserForm() {5const { isNew, user } = useLoaderData<typeof loader>()6const actionData = useActionData<typeof action>()78const [form, fields] = useForm({9id: "user-form",10lastResult: actionData?.result, // 🔗 lien serveur → client11defaultValue: {12firstName: user?.firstName,13lastName: user?.lastName,14action: isNew ? "create-user" : "update-user",15},16})1718return (19<Form method="POST" {...getFormProps(form)} className="space-y-4">20<input { ...getInputProps(fields.action, { type: "hidden" }) } />21{/* firstname */}22<Field23labelProps={{ children: "Prénom" }}24inputProps={ getInputProps(fields.firstName, { type: "text" }) }25errors={fields.firstName.errors}26/>27{/* lastname */}28<Field29labelProps={{ children: "Nom" }}30inputProps={ getInputProps(fields.lastName, { type: "text" }) }31errors={fields.lastName.errors}32/>33<button className="btn-primary">34{isNew ? "Ajouter" : "Modifier"}35</button>36</Form>37)38}
Conform mélange :
Submission.reply()
).Résultat : l’utilisateur reste sur place, ses champs ne sont pas réinitialisés, et le premier input en erreur reprend automatiquement le focus. ✨
switch
Les valeurs validées vivent dans submission.value
.
Grâce au discriminant, TypeScript sait exactement de quel cas il s’agit !
1switch (submission.value.action) {2case "create-user": {3const { slug } = await addUser(submission.value)4return redirect(href("/users/:userSlug", { userSlug: slug }))5}6case "update-user": {7const { slug } = await updateUser({8slug: params.userSlug,9userData: submission.value,10})11return redirect(href("/users/:userSlug", { userSlug: slug }))12}13}
Conform stocke les default values à l’instanciation du hook useForm
.
Si tu changes de slug sans recréer le composant tu conserves l’ancien état.
La parade : clé dynamique sur le composant enfant.
1export default function User() {2const { userSlug } = useParams()3return <UserForm key={userSlug} /> // force un remount4}
parseWithZod
+ superRefine
= validation type-safe et async en une ligne.submission.reply()
pour que Conform aligne UI et back-end.key={userSlug}
) garantit la synchronisation
des valeurs par défaut lors d’une navigation interne.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 ?