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>
  )
}

Ressources Pour Aller Plus Loin