Formulaires performants et validation TypeScript-first
Utilisez les flèches, cliquez ou glissez pour naviguer
1. Comprendre React Hook Form
Pourquoi c'est plus performant que les controlled inputs
2. Maîtriser le trio de base
useForm, register, handleSubmit
3. Valider avec Zod
Schémas, z.infer, zodResolver
4. Gérer les erreurs
Affichage ergonomique, UX
5. Formulaires complexes
useFieldArray, Controller
6. Live coding
Formulaire d'inscription complet
Pourquoi React Hook Form ?
Le problème des controlled inputs et des re-renders
useForm, register, handleSubmit
Le trio de base pour créer un formulaire
Validation avec Zod
Schémas, z.infer, @hookform/resolvers
Erreurs de validation
Affichage, messages personnalisés, UX
Live coding
Formulaire d'inscription complet avec Zod + React Hook Form
# React Hook Form
npm install react-hook-form
C'est tout! 🎉
React Hook Form ne nécessite aucune configuration supplémentaire
💡 On installera zod et @hookform/resolvers plus tard pour la validation
Chaque frappe = un re-render
// Controlled input classique ❌
const [value, setValue] = useState('');
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
⚠️ Problème
À chaque caractère tapé, le composant entier se re-render
Imaginez un formulaire avec 20 champs... 😱
❌ Controlled Input
Touche "a" → re-render
Touche "b" → re-render
Touche "c" → re-render
...
100 caractères = 100 re-renders!
✅ React Hook Form
Touche "a" → pas de re-render
Touche "b" → pas de re-render
Touche "c" → pas de re-render
...
Re-render uniquement à la soumission!
💡 React Hook Form utilise des refs (uncontrolled) au lieu du state
Controlled
React gère la valeur via useState
<input value={value} onChange={...} />
⚠️ Re-render à chaque changement
Uncontrolled
Le DOM gère la valeur via ref
<input ref={inputRef} />
✅ Pas de re-render pendant la saisie
🎯 React Hook Form = Uncontrolled par défaut
Les valeurs sont lues uniquement quand on en a besoin (soumission)
useForm, register, handleSubmit
useForm
Initialiser le formulaire
register
Connecter les inputs
handleSubmit
Gérer la soumission
import { useForm } from 'react-hook-form';
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
Ce que useForm retourne
register — Fonction pour connecter les inputs
handleSubmit — Wrapper pour la soumission
formState — État du formulaire (errors, isSubmitting...)
La fonction magique qui lie l'input au formulaire
// Syntaxe de base
<input {...register('email')} />
// Avec validation
<input
{...register('email', {
required: 'Email requis',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Email invalide'
}
})}
/>
💡 ...register('name') spread les props (onChange, onBlur, ref, name)
const onSubmit = (data) => {
console.log(data);
// { email: "[email protected]", password: "..." }
};
<form onSubmit={handleSubmit(onSubmit)}>
Ce que fait handleSubmit
1️⃣ Empêche le rechargement de la page (e.preventDefault)
2️⃣ Valide tous les champs
3️⃣ Si valide → appelle onSubmit avec les données
4️⃣ Si invalide → affiche les erreurs
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: 'Requis' })} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('password', { minLength: 6 })} />
<button type="submit">Connexion</button>
</form>
);
}
✅ Formulaire fonctionnel en ~15 lignes!
TypeScript-first schema validation
Définir le schéma ET inférer le type automatiquement
❌ Sans Zod
• Validation manuelle dans register
• Types TypeScript séparés
• Risque de désynchronisation
• Maintenance difficile
✅ Avec Zod
• Schéma unique comme source de vérité
• Types auto-générés avec z.infer
• Validation runtime + compile-time
• Messages d'erreur personnalisés
🎯 Un seul schéma = Type TypeScript + Validation runtime
# Zod + Resolver pour React Hook Form
npm install zod @hookform/resolvers
zod
Bibliothèque de validation de schémas
@hookform/resolvers
Connecte Zod à React Hook Form
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Email invalide'),
password: z.string().min(6, 'Min 6 caractères'),
age: z.number().min(18, 'Majorité requise'),
});
z.string()
Chaîne de caractères
z.number()
Nombre
z.boolean()
Booléen
Strings
.min(3) → minimum 3 caractères
.max(100) → maximum 100
.email() → format email
.url() → format URL
.regex(/.../) → pattern custom
Numbers
.min(0) → valeur minimum
.max(100) → valeur maximum
.int() → entier uniquement
.positive() → positif uniquement
💡 Toutes les méthodes acceptent un message custom : .min(3, 'Message')
Le schéma Zod génère automatiquement le type!
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
type FormData = z.infer<typeof schema>;
// Équivalent à :
// type FormData = { email: string; password: string }
🎯 Plus jamais de double définition!
Le schéma est la source unique de vérité
import { zodResolver } from '@hookform/resolvers/zod';
const { register, handleSubmit, formState } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
email: '',
password: '',
}
});
✅ La validation est maintenant automatique!
Plus besoin de règles dans register()
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Email invalide'),
password: z.string().min(6, '6 caractères min'),
});
type FormData = z.infer<typeof schema>;
function Form() {
const { register, handleSubmit, formState: { errors } } =
useForm<FormData>({ resolver: zodResolver(schema) });
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
</form>
);
}
Affichage ergonomique et UX
L'objet errors contient toutes les erreurs
const { formState: { errors } } = useForm();
// Structure de errors :
{
email: {
type: 'invalid_string',
message: 'Email invalide'
},
password: undefined // Pas d'erreur
}
Accès aux erreurs
errors.email?.message — Message de l'erreur
errors.email?.type — Type de l'erreur
❌ Basique
{errors.email && (
<span>{errors.email.message}</span>
)}
✅ Avec style
{errors.email && (
<p className="text-red-500 text-sm mt-1">
{errors.email.message}
</p>
)}
💡 Affichez l'erreur juste sous le champ concerné pour une meilleure UX
✅ mode: 'onBlur' (recommandé)
Validation quand l'utilisateur quitte le champ
useForm({ mode: 'onBlur' })
⚠️ mode: 'onChange'
Validation à chaque frappe — peut être agaçant
💡 mode: 'onTouched'
Validation après première interaction
❌ Éviter : mode: 'onChange' partout
L'utilisateur voit des erreurs avant même de finir de taper
const schema = z.object({
password: z.string()
.min(8, 'Au moins 8 caractères')
.max(100, 'Maximum 100 caractères')
.regex(/[A-Z]/, 'Une majuscule requise')
.regex(/[0-9]/, 'Un chiffre requis'),
});
✅ Messages clairs et actionables pour l'utilisateur
useFieldArray et Controller
useFieldArray
Champs dynamiques
Controller
Composants custom
Ajouter/supprimer des lignes dynamiquement
Ex: Liste de compétences, plusieurs adresses...
const { fields, append, remove } = useFieldArray({
control,
name: 'skills'
});
💡 control vient de useForm : const { control } = useForm()
const { fields, append, remove } = useFieldArray({ control, name: 'skills' });
return (
<>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`skills.${index}.name`)} />
<button onClick={() => remove(index)}>Supprimer</button>
</div>
))}
<button onClick={() => append({ name: '' })}>
Ajouter une compétence
</button>
</>
);
✅ append() ajoute, remove(index) supprime
Pour les composants qui ne propagent pas les refs
Ex: React Select, DatePicker, Slider...
<Controller
name="category"
control={control}
render={({ field }) => (
<Select {...field} options={options} />
)}
/>
🎯 Controller wrappe le composant et gère la connexion
register
Inputs natifs HTML
✅ <input />
✅ <select />
✅ <textarea />
✅ <input type="checkbox" />
<input {...register('name')} />
Controller
Composants custom
✅ React Select
✅ Material UI DatePicker
✅ Slider custom
✅ Tout composant tierce
<Controller name="..." control={control} ... />
❌ Oublier le ...register('name')
// Le champ n'est pas connecté au formulaire!
<input name="email" /> // ❌ Pas connecté
<input {...register('email')} /> // ✅
❌ Confondre register et Controller
register → inputs natifs | Controller → composants custom
❌ Ne pas utiliser z.infer
type FormData = { email: string }; // ❌ Double définition
type FormData = z.infer<typeof schema>; // ✅
❌ mode: 'onChange'
Validation à chaque frappe
"t" → Erreur: Email invalide
"te" → Erreur: Email invalide
"tes" → Erreur: Email invalide
UX frustrante!
✅ mode: 'onBlur'
Validation à la sortie du champ
L'utilisateur tape tranquillement
Il quitte le champ
→ Validation + erreur si besoin
UX optimale!
Formulaire d'inscription complet
React Hook Form + Zod + Validation + Erreurs
const registerSchema = z.object({
firstName: z.string().min(2, 'Min 2 caractères'),
lastName: z.string().min(2, 'Min 2 caractères'),
email: z.string().email('Email invalide'),
password: z.string()
.min(8, '8 caractères min')
.regex(/[A-Z]/, 'Une majuscule')
.regex(/[0-9]/, 'Un chiffre'),
confirmPassword: z.string(),
acceptTerms: z.boolean().refine(v => v, 'Requis'),
}).refine(data => data.password === data.confirmPassword, {
message: 'Les mots de passe ne correspondent pas',
path: ['confirmPassword']
});
function RegisterForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } =
useForm<RegisterData>({
resolver: zodResolver(registerSchema),
mode: 'onBlur'
});
const onSubmit = async (data: RegisterData) => {
await registerUser(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<input {...register('firstName')} placeholder="Prénom" />
{errors.firstName && <p className="text-red-500">{errors.firstName.message}</p>}
// ... autres champs
<button disabled={isSubmitting}>
{isSubmitting ? 'Inscription...' : 'S\'inscrire'}
</button>
</form>
);
}
Refs vs State
React Hook Form utilise des refs → pas de re-render à chaque frappe
Schéma unique
Zod définit le schéma ET infère le type TypeScript avec z.infer
zodResolver
Connecte Zod à React Hook Form — validation automatique
useFieldArray
Pour les champs dynamiques (ajouter/supprimer des lignes)
useForm
Initialise le formulaire
register
Connecte les inputs natifs
handleSubmit
Gère la soumission
zodResolver
Connecte Zod
⚠️ Préférez mode: 'onBlur' pour une meilleure UX
Formulaires performants avec React Hook Form + Zod