React - Formulaires et Validation
Le Problème avec les Formulaires React
L'Approche Vanilla React (à éviter)
// ❌ Code horrible qu'on voit partout...
function ContactForm() {
const [nom, setNom] = useState('')
const [email, setEmail] = useState('')
const [message, setMessage] = useState('')
const [errors, setErrors] = useState({})
const handleSubmit = (e) => {
e.preventDefault()
// Validation manuelle... beurk
const newErrors = {}
if (!nom) newErrors.nom = 'Le nom est requis'
if (!email) newErrors.email = 'L\'email est requis'
else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Email invalide'
}
if (!message) newErrors.message = 'Le message est requis'
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Submit...
console.log({ nom, email, message })
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Nom"
/>
{errors.nom && <span>{errors.nom}</span>}
</div>
<div>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}
</div>
<div>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Message"
/>
{errors.message && <span>{errors.message}</span>}
</div>
<button type="submit">Envoyer</button>
</form>
)
}
Les Problèmes
- 🤮 Boilerplate de malade : état pour chaque champ
- 📋 Validation manuelle : code répétitif partout
- 🐛 Erreurs faciles : oublis, typos, inconsistances
- 📊 Pas de réutilisabilité : chaque formulaire = refaire tout
- 🎯 Performance : re-render à chaque frappe
Les Solutions Modernes
1. React Hook Form - LE BOSS ! 🔥
npm install react-hook-form
Le même formulaire, mais version pro :
// ✅ React Hook Form - Clean et efficace !
import { useForm } from 'react-hook-form'
function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm()
const onSubmit = async (data) => {
console.log(data) // { nom: '...', email: '...', message: '...' }
// Simulation d'une requête API
await new Promise(resolve => setTimeout(resolve, 1000))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('nom', {
required: 'Le nom est requis',
minLength: { value: 2, message: 'Min 2 caractères' }
})}
placeholder="Nom"
/>
{errors.nom && <span>{errors.nom.message}</span>}
</div>
<div>
<input
{...register('email', {
required: 'L\'email est requis',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Email invalide'
}
})}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<textarea
{...register('message', {
required: 'Le message est requis',
minLength: { value: 10, message: 'Min 10 caractères' }
})}
placeholder="Message"
/>
{errors.message && <span>{errors.message.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Envoi...' : 'Envoyer'}
</button>
</form>
)
}
2. Avec Zod pour la Validation - Le Combo Parfait ! ⚡
npm install zod @hookform/resolvers
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
// Schéma de validation
const contactSchema = z.object({
nom: z.string()
.min(1, 'Le nom est requis')
.min(2, 'Au moins 2 caractères'),
email: z.string()
.min(1, 'L\'email est requis')
.email('Format email invalide'),
message: z.string()
.min(1, 'Le message est requis')
.min(10, 'Au moins 10 caractères'),
age: z.number()
.min(18, 'Vous devez être majeur')
.max(120, 'Âge invalide'),
acceptTerms: z.boolean()
.refine(val => val === true, 'Vous devez accepter les conditions')
})
type ContactFormData = z.infer<typeof contactSchema>
function ContactFormAdvanced() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm<ContactFormData>({
resolver: zodResolver(contactSchema),
defaultValues: {
nom: '',
email: '',
message: '',
age: 18,
acceptTerms: false
}
})
const onSubmit = async (data: ContactFormData) => {
try {
// API call
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('Erreur serveur')
alert('Message envoyé avec succès !')
reset() // Reset le formulaire
} catch (error) {
alert('Erreur lors de l\'envoi')
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<input
{...register('nom')}
placeholder="Nom complet"
className={errors.nom ? 'border-red-500' : ''}
/>
{errors.nom && <p className="text-red-500">{errors.nom.message}</p>}
</div>
<div>
<input
{...register('email')}
type="email"
placeholder="Email"
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && <p className="text-red-500">{errors.email.message}</p>}
</div>
<div>
<input
{...register('age', { valueAsNumber: true })}
type="number"
placeholder="Âge"
min="18"
className={errors.age ? 'border-red-500' : ''}
/>
{errors.age && <p className="text-red-500">{errors.age.message}</p>}
</div>
<div>
<textarea
{...register('message')}
placeholder="Votre message"
rows={4}
className={errors.message ? 'border-red-500' : ''}
/>
{errors.message && <p className="text-red-500">{errors.message.message}</p>}
</div>
<div className="flex items-center">
<input
{...register('acceptTerms')}
type="checkbox"
id="terms"
/>
<label htmlFor="terms" className="ml-2">
J'accepte les conditions d'utilisation
</label>
</div>
{errors.acceptTerms && <p className="text-red-500">{errors.acceptTerms.message}</p>}
<button
type="submit"
disabled={isSubmitting}
className={`px-4 py-2 rounded ${isSubmitting ? 'bg-gray-400' : 'bg-blue-500 hover:bg-blue-600'}`}
>
{isSubmitting ? 'Envoi en cours...' : 'Envoyer'}
</button>
</form>
)
}