Validation avec Zod et Conform

Zod et Conform permettent de valider un formulaire une seule fois, côté client et serveur, avec moins de code, des erreurs précises et des champs automatiquement configurés.

4 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
57 leçons
Accès à vie
299.49
-35%

Pourquoi abandonner la validation “maison” ?

Taper à la main un if (!firstName) { errors.push(...) } fonctionne… jusqu’au jour où tu oublies un champ, ou qu’un collègue renomme firstName en givenName. Résultat : une logique disséminée, fragile, impossible à garder en phase entre le client et le serveur.

La bonne nouvelle : le duo Zod + Conform te permet de :

  1. Écrire le schéma de tes données une seule fois.
  2. Obtenir la validation côté client et côté serveur sans copier-coller.
  3. Générer automatiquement les attributs HTML (required, minLength, …).
  4. Afficher les erreurs et donner le focus au 1ᵉʳ champ fautif, sans code supplémentaire.

Dans cette leçon on transforme le formulaire « Ajouter un utilisateur » pour qu’il bénéficie de ces super-pouvoirs.


1. Définir le schéma Zod partagé

Crée d’abord un fichier dédié aux règles métier :

app/schemas/user.ts
1
import { z } from "zod";
2
3
export const userCreateSchema = z.object({
4
action: z.literal("create-user"),
5
firstName: z
6
.string({ required_error: "Le prénom est requis" })
7
.min(2, "Longueur minimum : 2 caractères"),
8
lastName: z
9
.string({ required_error: "Le nom est requis" })
10
.min(2, "Longueur minimum : 2 caractères"),
11
});
12
13
export const userUpdateSchema = z.object({
14
action: z.literal("update-user"),
15
firstName: z.string().min(2),
16
lastName: z.string().min(2),
17
});
18
19
/** Schéma discriminé – create OU update */
20
export const userActionSchema = z.discriminatedUnion("action", [
21
userCreateSchema,
22
userUpdateSchema,
23
]);

Ce fichier est le single source of truth : toute la validation part d’ici.


2. Sécuriser l’action côté serveur

Dans la route dynamique users.$userSlug.tsx :

app/routes/users+/$userSlug.tsx {10-35}
1
import { parseWithZod } from "@conform-to/zod";
2
import { userActionSchema } from "~/schemas/user";
3
4
export async function action({ request, params }) {
5
const formData = await request.formData();
6
7
// 1. On parse de manière « safe » (pas d’exception JS)
8
const submission = await parseWithZod(formData, {
9
schema: userActionSchema.superRefine(async (data, ctx) => {
10
if (data.action === "create-user") {
11
// règle business : « slug » unique
12
const slug = createSlug({ firstName: data.firstName });
13
if (await getUserBySlug({ slug })) {
14
ctx.addIssue({
15
path: ["firstName"],
16
code: "custom",
17
message: "Un utilisateur porte déjà ce prénom",
18
});
19
}
20
}
21
}),
22
});
23
24
// 2. Si erreur → on renvoie un JSON structuré pour Conform
25
if (submission.status !== "success") {
26
return data({ result: submission.reply() }, { status: 400 });
27
}
28
29
// 3. Sinon on exécute la mutation
30
/* … addUser / updateUser … */
31
}

Avantage : la même structure submission.reply() sera réutilisée côté client – zéro sérialisation manuelle.


3. Connecter le formulaire React

3.1 Initialiser Conform

app/routes/users+/$userSlug.tsx {70-97}
1
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
2
import { getZodConstraint } from "@conform-to/zod";
3
import { useActionData, useLoaderData, Form } from "react-router";
4
import { userActionSchema } from "~/schemas/user";
5
6
function UserForm() {
7
const { user, isNew } = useLoaderData<typeof loader>();
8
const actionData = useActionData<typeof action>();
9
10
const [form, fields] = useForm({
11
id: "user-form",
12
lastResult: actionData?.result, // 🡄 erreurs serveur
13
constraint: getZodConstraint(userActionSchema), // 🡄 attrs HTML
14
onValidate({ formData }) {
15
/* Validation instantanée côté client */
16
return parseWithZod(formData, { schema: userActionSchema });
17
},
18
defaultValue: {
19
firstName: user?.firstName,
20
lastName: user?.lastName,
21
action: isNew ? "create-user" : "update-user",
22
},
23
});
  • constraint : déduit required, minLength, etc.
  • onValidate : exécute le même parse avant d’envoyer au serveur.
  • lastResult : ré-injecte les erreurs renvoyées par l’action.

3.2 Brancher les champs avec getInputProps

app/routes/users+/$userSlug.tsx {99-128}
1
return (
2
<Form method="POST" {...getFormProps(form)}
3
className="space-y-4 bg-white p-4 rounded shadow">
4
{/* champ action caché */}
5
<input {...getInputProps(fields.action, { type: "hidden" })} />
6
7
<Field /* composant réutilisable */
8
labelProps={{ children: "Prénom" }}
9
inputProps={getInputProps(fields.firstName, { type: "text" })}
10
errors={fields.firstName.errors}
11
/>
12
13
<Field
14
labelProps={{ children: "Nom" }}
15
inputProps={getInputProps(fields.lastName, { type: "text" })}
16
errors={fields.lastName.errors}
17
/>
18
19
<button
20
type="submit"
21
disabled={form.submission?.pending}
22
className={`btn ${isNew ? "bg-emerald-600" : "bg-blue-600"}`}
23
>
24
{form.submission?.pending
25
? "En cours…"
26
: isNew
27
? "Ajouter"
28
: "Modifier"}
29
</button>
30
</Form>
31
);
32
}

Magie : Conform ajoute pour toi required + minLength="2" sur l’<input> et focus automatiquement le premier champ invalide.


4. Démonstration : UX et DX au top !

  1. Tu cliques sur Ajouter sans rien saisir.
  2. Les erreurs apparaissent instantanément, le focus est sur Prénom.
  3. Tu tapes “A”, le message “2 caractères mini” reste ; tape “Ax” → hop, l’erreur disparaît.
  4. Tu valides : si le prénom existe déjà, la validation serveur renvoie l’erreur et Conform l’affiche au même endroit, sans reload brutal.

Côté dev, tu bénéficies :

  • d’un autocomplétion TypeScript (fields.firstName.errors) ;
  • d’une seule source de validation (userActionSchema) ;
  • d’un code React ultra-léger (plus de useState ni useEffect pour gérer les erreurs).

5. Points clés

  • Zod décrit le quoi ; Conform gère le comment.
  • getZodConstraint() génère les attributs HTML à partir du schéma.
  • parseWithZod() produit un objet submission homogène (succès/échec).
  • useForm() fusionne erreurs client et serveur pour un feedback unifié.
  • Un composant Field centralise label, input, accessibilité et message
Premium
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 premium en achetant ce cours