React Hook Form et Zod : Un an en production sans code boilerplate ni pièges cachés
Après un an d'utilisation active de la combinaison React Hook Form et Zod en production sur un grand projet, l'équipe confirme : l'approche déclarative des formulaires réduit le volume de code de 30–40 %, mais nécessite une attention aux détails d'implémentation. Nous partageons les pièges que nous avons découverts et comment les surmonter.
De MobX aux formulaires déclaratifs : Pourquoi changer d'approche ?
Dans les itérations précédentes du projet, les formulaires étaient construits avec MobX dans un style MVC. Cette méthode permettait de contrôler l'état mais générait du code boilerplate : chaque champ nécessitait des méthodes séparées dans le contrôleur, des tableaux d'erreurs et une synchronisation manuelle avec l'UI. Par exemple, ajouter un nouveau champ signifiait créer :
export class SomeController {
public setDuration = (v: number) => {
this.store.duration = v;
}
}
Un tel code boilerplate n'était pas lié à la logique métier mais occupait jusqu'à 60 % du volume du composant. Passer à React Hook Form (RHF) avec Zod a résolu le problème : le schéma de validation est décrit de manière déclarative, et la bibliothèque automatise la synchronisation de l'état et la gestion des erreurs.
Exemple d'un formulaire de base :
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
name: z.string().min(1, 'Enter name'),
email: z.string().email('Invalid email'),
age: z.coerce.number().min(0, 'Invalid vozrast').optional(),
});
type FormValues = z.infer<typeof formSchema>;
export function ExampleForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
},
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<div>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<input type="email" {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input type="number" {...register('age')} />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button type="submit">Send</button>
</form>
);
}
Résultat : le volume de code a diminué, la maintenance est devenue plus simple, et la vitesse de développement de nouveaux formulaires a augmenté. Cependant, lors d'une utilisation en conditions réelles, des nuances non couvertes par la documentation ont émergé.
Trois pièges dans la combinaison React Hook Form et Zod
Au cours d'une année d'utilisation de RHF et Zod, nous sommes tombés sur trois problèmes clés qui peuvent casser la fonctionnalité des formulaires dans certains scénarios.
1. Gestion de undefined et null
RHF ne supporte pas undefined comme valeur de champ. Tenter de définir :
methods.setValue('age', undefined)
fait ignorer le changement au champ. La doc mentionne cela, mais ce n'est pas évident. La solution standard est d'utiliser null et de déclarer le champ comme .nullable() :
const schema = z.object({
age: z.number().nullable()
});
methods.setValue('age', null);
Cependant, cela peut entrer en conflit avec des composants personnalisés attendant un number. Par exemple, un composant d'entrée d'âge pourrait planter sur null. L'envers de la médaille : le composant retourne undefined quand effacé, mais RHF ne traitera pas cette valeur.
Les boutons de reset ajoutent de la complexité supplémentaire. La méthode reset() tire les valeurs de defaultValues, mais si elles sont définies à undefined, le reset échoue. Solution : utiliser null ou une chaîne vide pour les champs string.
2. Erreurs de type inattendues dans Zod
Le problème surgit avec des incompatibilités de types de données. Par exemple, ce schéma :
const schema = z.object({
field1: z.string().min(1, 'Fill field').optional()
});
Si le composant retourne null (pas undefined), Zod lève invalid type (expected string received null). Les utilisateurs voient un message système au lieu de votre texte personnalisé.
Solution : définir explicitement le texte d'erreur pour tous les types de validation :
const schema = z.object({
field1: z.string('Fill field').min(1, 'Fill field').optional()
});
Cela garantit que toute erreur affiche votre message.
3. Instabilité du drapeau isDirty
Le drapeau formState.isDirty indique si le formulaire a des changements. Mais initialiser avec des defaultValues vides :
const methods = useForm({
resolver: zodResolver(formSchema),
defaultValues: {},
});
fait passer isDirty à true juste après le rendu, même si l'utilisateur n'a rien touché. Pendant ce temps, dirtyFields reste un objet vide.
Cela provient d'une incompatibilité entre la structure de defaultValues et le schéma. Pour éviter les déclenchements faux, assurez-vous que defaultValues inclut tous les champs du schéma — même avec undefined ou null.
Comment éviter les erreurs courantes ?
À partir de notre expérience, voici des pratiques éprouvées :
- Pour la gestion des valeurs vides :
- Champs string : utiliser une chaîne vide par défaut.
- Champs numériques/objet : appliquer .nullable() dans Zod et null pour les valeurs.
- Toujours vérifier que les composants personnalisés supportent le type choisi.
- Pour une validation fiable :
- Spécifier un message d'erreur général pour chaque étape de validation, comme dans l'exemple ci-dessus.
- Tester les scénarios null et undefined lors du développement des composants.
- Pour un comportement correct de isDirty :
- Initialiser defaultValues pour correspondre au schéma, couvrant tous les champs.
- Éviter l'objet vide {} — lister explicitement les champs avec undefined ou null.
Nous recommandons aussi d'ajouter des helpers unifiés pour les formulaires à votre projet, comme une fonction de reset qui gère les particularités de RHF :
const resetForm = () => {
methods.reset({
name: null,
email: null,
age: null,
});
};
Points clés
- La réduction de code n'élimine pas le besoin de tests : L'approche déclarative fait gagner du temps mais exige des vérifications rigoureuses des cas limites.
- La sécurité des types est votre amie : Zod avec TypeScript prévient de nombreux bugs, mais les schémas doivent être configurés correctement.
- Documentez les particularités : Notez les solutions de contournement pour les problèmes courants de RHF et Zod dans la doc interne de votre projet.
— Editorial Team
Aucun commentaire pour le moment.