Comment créer un formulaire avec React JS facilement ?

Dans cet article

  • Un formulaire React JS repose sur deux approches : composants contrôlés (state) ou bibliothèques comme React Hook Form
  • React Hook Form réduit les re-renders de 70 à 80 % par rapport à un formulaire géré uniquement avec useState
  • La validation côté client se met en place en moins de 10 lignes grâce aux schémas Yup ou Zod
  • Un formulaire de contact fonctionnel se code en moins de 30 minutes avec les bonnes pratiques détaillées ici
  • Les formulaires dynamiques (ajout/suppression de champs) s’implémentent via le hook useFieldArray
  • La soumission sécurisée nécessite toujours une validation serveur en complément du contrôle client

Quand j’ai commencé à travailler avec React en 2016, la gestion des formulaires était l’un des sujets qui posait le plus de questions dans les équipes. Aujourd’hui encore, le formulaire React JS reste un passage obligé pour tout développeur front-end. Que vous construisiez un simple champ de recherche ou un tunnel d’inscription complexe, React propose une logique bien différente du HTML classique. Je vais vous guider pas à pas pour maîtriser ce sujet, des bases jusqu’aux cas avancés.

Comprendre le fonctionnement d’un formulaire React JS

En HTML natif, un formulaire gère son propre état interne : le navigateur stocke la valeur de chaque champ et l’envoie au serveur lors de la soumission. En React, la philosophie est différente. Le composant React devient la source unique de vérité (single source of truth) pour les données du formulaire.

Concrètement, chaque modification d’un champ déclenche une mise à jour du state React, qui à son tour met à jour l’affichage du champ. Ce cycle, appelé data binding unidirectionnel, garantit que l’interface reflète toujours l’état réel des données. Si vous venez du développement JavaScript classique, ce changement de paradigme demande un temps d’adaptation, mais il apporte un contrôle total sur le comportement du formulaire.

Développement d'un composant contrôlé React avec gestion du state
Développement d’un composant contrôlé React avec gestion du state

React gère les formulaires via le système d’événements synthétiques documenté dans la documentation officielle React. L’événement onChange capture chaque frappe clavier, chaque sélection dans un menu déroulant, chaque case cochée. L’événement onSubmit intercepte la soumission pour la traiter en JavaScript plutôt que de laisser le navigateur envoyer la requête.

Composants contrôlés vs non contrôlés : quelle approche choisir ?

React propose deux façons de gérer les champs de formulaire. Comprendre cette distinction est essentiel avant d’écrire la moindre ligne de code.

Les composants contrôlés

Un composant contrôlé lie sa valeur au state React. Chaque modification passe par une fonction de mise à jour :

const [email, setEmail] = useState('');

<input
  type="email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
/>

L’avantage principal : vous avez accès à la valeur du champ à tout moment, pas seulement lors de la soumission. Vous pouvez valider en temps réel, formater la saisie, désactiver un bouton tant que les champs ne sont pas remplis. C’est l’approche recommandée dans la majorité des cas.

Les composants non contrôlés

Un composant non contrôlé conserve son état dans le DOM. Vous accédez à sa valeur via une ref :

const inputRef = useRef(null);

<input type="email" ref={inputRef} />

// Accès à la valeur
const value = inputRef.current.value;

Cette approche est utile pour intégrer des bibliothèques tierces qui manipulent directement le DOM, ou pour des formulaires très simples où le contrôle en temps réel n’apporte rien. En pratique, je l’utilise rarement en production.

Critère Composant contrôlé Composant non contrôlé
Source de vérité State React DOM natif
Validation en temps réel Oui Non
Accès à la valeur À tout moment via le state Via ref ou lors du submit
Re-renders À chaque frappe Aucun
Complexité initiale Moyenne Faible
Cas d’usage idéal Formulaires interactifs Upload de fichiers, intégrations DOM

Créer un formulaire React avec useState étape par étape

Commençons par la méthode native, sans aucune bibliothèque externe. Si vous découvrez React ou si vous suivez un parcours d’apprentissage JavaScript, cette approche vous donnera les bases solides.

Étape 1 : structurer le composant

import { useState } from 'react';

function InscriptionForm() {
  const [formData, setFormData] = useState({
    nom: '',
    email: '',
    message: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  return (
    <form>
      <input name="nom" value={formData.nom} onChange={handleChange} />
      <input name="email" value={formData.email} onChange={handleChange} />
      <textarea name="message" value={formData.message} onChange={handleChange} />
    </form>
  );
}

L’astuce ici, c’est d’utiliser un objet unique pour le state plutôt qu’un useState par champ. La propriété [name] (computed property name) permet de factoriser le handler. Un seul handleChange gère tous les champs, ce qui garde le code propre même avec 10 ou 15 inputs.

Étape 2 : gérer la soumission

const handleSubmit = (e) => {
  e.preventDefault();

  // Validation basique
  if (!formData.email.includes('@')) {
    alert('Email invalide');
    return;
  }

  // Envoi des données
  fetch('/api/inscription', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(formData)
  });
};

Le e.preventDefault() est indispensable : sans lui, le navigateur recharge la page comme avec un formulaire HTML classique. C’est l’erreur numéro un que je vois chez les développeurs qui débutent avec React.

Étape 3 : ajouter le feedback utilisateur

const [isSubmitting, setIsSubmitting] = useState(false);
const [success, setSuccess] = useState(false);

const handleSubmit = async (e) => {
  e.preventDefault();
  setIsSubmitting(true);

  try {
    await fetch('/api/inscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData)
    });
    setSuccess(true);
  } catch (error) {
    console.error('Erreur:', error);
  } finally {
    setIsSubmitting(false);
  }
};

Ce pattern loading/success/error est celui que j’applique systématiquement en production. L’utilisateur voit que son action est prise en compte, et vous évitez les doubles soumissions en désactivant le bouton pendant le traitement.

React Hook Form : la solution moderne pour vos formulaires

C’est quoi React Hook Form exactement ? Il s’agit d’une bibliothèque légère (environ 9 Ko gzippée) qui simplifie drastiquement la gestion des formulaires en React. Elle repose sur les refs plutôt que sur le state contrôlé, ce qui élimine les re-renders inutiles. Selon les benchmarks publiés sur le site officiel de React Hook Form, les performances sont 70 à 80 % supérieures à celles d’un formulaire classique avec useState sur des formulaires complexes.

Validation d'un formulaire React avec affichage des erreurs en temps réel
Validation d’un formulaire React avec affichage des erreurs en temps réel

Installation et mise en place

npm install react-hook-form
import { useForm } from 'react-hook-form';

function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('nom', { required: 'Le nom est obligatoire' })}
        placeholder="Votre nom"
      />
      {errors.nom && <span>{errors.nom.message}</span>}

      <input
        {...register('email', {
          required: 'L\'email est obligatoire',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'Email invalide'
          }
        })}
        placeholder="Votre email"
      />
      {errors.email && <span>{errors.email.message}</span>}

      <button type="submit">Envoyer</button>
    </form>
  );
}

Remarquez la différence : plus besoin de useState, plus besoin de handleChange. La fonction register lie chaque champ au formulaire et la validation s’écrit directement dans les options. C’est cette simplicité qui a fait de React Hook Form la bibliothèque de formulaires la plus populaire de l’écosystème React, avec plus de 40 000 étoiles sur GitHub.

Quand préférer React Hook Form à useState ?

Pour un formulaire de 2 ou 3 champs sans validation complexe, useState suffit. Dès que vous dépassez 5 champs, que vous avez besoin de validation avancée, de formulaires conditionnels ou de performances optimales, React Hook Form devient le choix évident. Dans mes projets freelance, je l’utilise systématiquement depuis 2021. Si vous hésitez encore entre les différentes technologies, mon guide sur les frameworks JavaScript peut vous aider à situer React dans l’écosystème.

Valider un formulaire React avec JavaScript et Yup

Comment valider un formulaire avec JavaScript dans un contexte React ? Deux stratégies coexistent : la validation inline (règles écrites directement dans le composant) et la validation par schéma (règles externalisées dans un objet de validation).

Validation inline avec React Hook Form

<input
  {...register('telephone', {
    required: 'Champ obligatoire',
    minLength: { value: 10, message: 'Minimum 10 caractères' },
    maxLength: { value: 14, message: 'Maximum 14 caractères' },
    pattern: {
      value: /^[0-9+\s]+$/,
      message: 'Numéro invalide'
    }
  })}
/>

C’est suffisant pour des règles simples. Mais dès que la logique de validation se complexifie (champs conditionnels, dépendances entre champs, règles métier), un schéma de validation devient indispensable.

Validation par schéma avec Yup

npm install @hookform/resolvers yup
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object({
  nom: yup.string()
    .required('Le nom est obligatoire')
    .min(2, 'Minimum 2 caractères'),
  email: yup.string()
    .required('L\'email est obligatoire')
    .email('Format email invalide'),
  age: yup.number()
    .positive('L\'âge doit être positif')
    .integer('L\'âge doit être un entier')
    .min(18, 'Vous devez avoir au moins 18 ans'),
  password: yup.string()
    .required('Le mot de passe est obligatoire')
    .min(8, 'Minimum 8 caractères')
    .matches(/[A-Z]/, 'Au moins une majuscule')
    .matches(/[0-9]/, 'Au moins un chiffre'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], 'Les mots de passe ne correspondent pas')
}).required();

function RegisterForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema)
  });

  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Vos champs ici */}
    </form>
  );
}

L’avantage de cette approche : le schéma est testable unitairement, réutilisable entre le front et le back (si vous utilisez Node.js côté serveur), et lisible même par un développeur qui découvre le projet. La bibliothèque Zod est une alternative populaire, documentée dans les ressources MDN sur la validation de formulaires.

Construire un formulaire dynamique React

Un formulaire dynamique permet d’ajouter ou de supprimer des champs à la volée. C’est un besoin courant : lignes de facture, compétences d’un CV, participants à un événement. React Hook Form fournit le hook useFieldArray exactement pour ce cas.

import { useForm, useFieldArray } from 'react-hook-form';

function FactureForm() {
  const { register, control, handleSubmit } = useForm({
    defaultValues: {
      lignes: [{ description: '', montant: 0 }]
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'lignes'
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input
            {...register(`lignes.${index}.description`)}
            placeholder="Description"
          />
          <input
            type="number"
            {...register(`lignes.${index}.montant`, { valueAsNumber: true })}
            placeholder="Montant"
          />
          <button type="button" onClick={() => remove(index)}>
            Supprimer
          </button>
        </div>
      ))}

      <button
        type="button"
        onClick={() => append({ description: '', montant: 0 })}
      >
        Ajouter une ligne
      </button>

      <button type="submit">Valider</button>
    </form>
  );
}
Test d'un formulaire dynamique React avec ajout de champs à la volée
Test d’un formulaire dynamique React avec ajout de champs à la volée

Le point clé : utilisez field.id comme key et non l’index du tableau. React Hook Form génère un identifiant unique pour chaque entrée, ce qui évite les bugs de re-render quand vous supprimez un élément au milieu du tableau. J’ai vu cette erreur provoquer des pertes de données saisies sur plusieurs projets clients.

Pour les formulaires dont la structure elle-même est dynamique (champs générés depuis une API ou un CMS), vous pouvez combiner useFieldArray avec un fichier de configuration JSON. Cette technique est celle que j’utilise dans les projets proches du no-code où les utilisateurs finaux définissent eux-mêmes la structure de leurs formulaires.

Exemple complet : formulaire de contact en React

Voici un formulaire de contact en React prêt pour la production. Il intègre la validation, la gestion des erreurs, le feedback utilisateur et l’accessibilité.

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { useState } from 'react';

const schema = yup.object({
  nom: yup.string().required('Veuillez saisir votre nom').min(2),
  email: yup.string().required('Veuillez saisir votre email').email('Email invalide'),
  sujet: yup.string().required('Veuillez choisir un sujet'),
  message: yup.string().required('Veuillez écrire un message').min(10, 'Minimum 10 caractères')
});

function ContactForm() {
  const [submitStatus, setSubmitStatus] = useState(null);

  const {
    register,
    handleSubmit,
    reset,
    formState: { errors, isSubmitting }
  } = useForm({
    resolver: yupResolver(schema)
  });

  const onSubmit = async (data) => {
    try {
      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');

      setSubmitStatus('success');
      reset();
    } catch (error) {
      setSubmitStatus('error');
    }
  };

  if (submitStatus === 'success') {
    return <p role="alert">Merci, votre message a bien été envoyé.</p>;
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label htmlFor="nom">Nom</label>
        <input
          id="nom"
          {...register('nom')}
          aria-invalid={errors.nom ? 'true' : 'false'}
          aria-describedby={errors.nom ? 'nom-error' : undefined}
        />
        {errors.nom && <span id="nom-error" role="alert">{errors.nom.message}</span>}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          {...register('email')}
          aria-invalid={errors.email ? 'true' : 'false'}
        />
        {errors.email && <span role="alert">{errors.email.message}</span>}
      </div>

      <div>
        <label htmlFor="sujet">Sujet</label>
        <select id="sujet" {...register('sujet')}>
          <option value="">Choisir...</option>
          <option value="devis">Demande de devis</option>
          <option value="support">Support technique</option>
          <option value="autre">Autre</option>
        </select>
        {errors.sujet && <span role="alert">{errors.sujet.message}</span>}
      </div>

      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          rows="5"
          {...register('message')}
          aria-invalid={errors.message ? 'true' : 'false'}
        />
        {errors.message && <span role="alert">{errors.message.message}</span>}
      </div>

      {submitStatus === 'error' && (
        <p role="alert">Une erreur est survenue, veuillez réessayer.</p>
      )}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Envoi en cours...' : 'Envoyer'}
      </button>
    </form>
  );
}

Ce formulaire de contact en React intègre plusieurs bonnes pratiques essentielles : les attributs aria-invalid et role= »alert » pour l’accessibilité, le bouton désactivé pendant l’envoi pour éviter les doubles soumissions, et le noValidate sur le form pour désactiver la validation HTML native au profit de la validation React. Si vous cherchez à héberger ce type d’application, pensez à vérifier que votre nom de domaine est bien configuré pour servir une application React.

Les différents types de formulaires en React

Quels sont les différents types de formulaires que vous rencontrerez dans vos projets React ? Voici les principaux, classés par complexité croissante.

Formulaire simple : connexion, recherche, newsletter. Deux à trois champs, validation minimale. useState suffit généralement.

Formulaire de saisie structurée : inscription, profil utilisateur, paramètres. Cinq à quinze champs avec des types variés (texte, select, checkbox, radio). React Hook Form est recommandé.

Formulaire multi-étapes (wizard) : onboarding, tunnel de commande, déclaration administrative. Les données sont réparties sur plusieurs pages avec navigation avant/arrière. Vous combinerez React Hook Form avec un state machine ou un contexte React pour persister les données entre les étapes.

Formulaire dynamique : facture avec lignes ajoutables, questionnaire configurable. La structure elle-même change en fonction des actions de l’utilisateur. useFieldArray est la solution native de React Hook Form.

Formulaire connecté à un CMS : les champs sont définis côté serveur (via WordPress ou un headless CMS) et rendus dynamiquement côté client. Ce pattern nécessite un moteur de rendu de formulaire générique.

En contexte professionnel, 80 % des formulaires que je développe tombent dans les deux premières catégories. Les formulaires multi-étapes et dynamiques représentent les 20 % restants, mais ils concentrent la majorité de la complexité technique.

Comment soumettre un formulaire avec React proprement

La soumission d’un formulaire React se fait en trois temps : interception, validation, envoi. Voici les bonnes pratiques que j’applique systématiquement.

Toujours intercepter le comportement par défaut

Appelez e.preventDefault() dans votre handler ou utilisez le handleSubmit de React Hook Form qui le fait automatiquement. Sans cela, la page se recharge et votre state React est perdu.

Valider avant d’envoyer

Même si votre API valide les données côté serveur (et elle doit le faire), validez aussi côté client pour offrir un retour instantané. La CNIL rappelle les obligations de collecte de données via les formulaires web, notamment le consentement explicite pour les données personnelles. Pensez à ajouter une case à cocher pour la politique de confidentialité.

Gérer les états de chargement

const [state, setState] = useState('idle'); // idle | loading | success | error

const onSubmit = async (data) => {
  setState('loading');
  try {
    await apiCall(data);
    setState('success');
  } catch {
    setState('error');
  }
};

Envoyer les données au format JSON

Sauf cas particulier (upload de fichier avec FormData), envoyez vos données en JSON avec le header Content-Type: application/json. C’est le standard des API REST modernes, et c’est ce que vos collègues back-end attendent. Si vous travaillez avec un back-end PHP, comme dans les projets que je mène avec mes clients freelance, l’intégration se fait naturellement via une route API.

Réinitialiser le formulaire après succès

Avec React Hook Form, appelez reset() après une soumission réussie. Avec useState, réinitialisez manuellement chaque champ à sa valeur par défaut. Affichez un message de confirmation clair pour que l’utilisateur sache que son action a abouti.

Les erreurs fréquentes à éviter

En plus de dix ans de développement web, j’ai identifié des erreurs récurrentes dans la gestion des formulaires React. Les voici, avec leurs solutions.

Oublier l’attribut name sur les inputs : sans name, le handler générique ne peut pas identifier quel champ a changé. Chaque input contrôlé doit avoir un attribut name correspondant à sa clé dans le state.

Muter le state directement : écrire formData.nom = 'test' au lieu de passer par le setter. React ne détecte pas les mutations directes et ne met pas à jour l’affichage. Utilisez toujours le spread operator pour créer un nouvel objet.

Ne pas gérer les valeurs initiales des select : un <select> sans option vide par défaut peut envoyer silencieusement la première valeur de la liste, même si l’utilisateur n’a rien choisi. Ajoutez toujours une option placeholder désactivée.

Ignorer l’accessibilité : un formulaire sans labels associés, sans messages d’erreur liés aux champs via aria-describedby, sans gestion du focus après erreur, exclut une partie de vos utilisateurs. Les attributs htmlFor, aria-invalid et role="alert" ne sont pas optionnels.

Valider uniquement côté client : la validation JavaScript peut être contournée en quelques secondes via les outils de développement du navigateur. Votre API doit toujours revalider les données reçues. La validation client est un confort UX, pas une sécurité.

Créer un state par champ : avoir 15 appels à useState dans un même composant rend le code illisible. Regroupez les champs dans un objet unique ou passez à React Hook Form. Si votre formulaire dépasse 8 champs, la complexité justifie l’ajout d’une dépendance. Le choix des bons outils fait partie des décisions que je détaille dans mon article sur les objets en JavaScript.

À retenir

  • Utilisez des composants contrôlés par défaut et React Hook Form dès que le formulaire dépasse 5 champs
  • Validez avec un schéma Yup ou Zod pour séparer la logique de validation du composant
  • Ajoutez systématiquement e.preventDefault() ou utilisez le handleSubmit de React Hook Form
  • Ne faites jamais confiance à la validation client seule : revalidez toujours côté serveur
  • Intégrez les attributs aria-invalid et role= »alert » pour rendre vos formulaires accessibles

Questions fréquentes


Comment soumettre un formulaire avec React ?

Pour soumettre un formulaire avec React, ajoutez un handler onSubmit sur la balise <form>. Ce handler doit appeler e.preventDefault() pour empêcher le rechargement de la page, puis traiter les données via une requête HTTP (fetch ou axios). Avec React Hook Form, la méthode handleSubmit gère automatiquement l’interception et ne déclenche votre callback que si la validation passe.


C’est quoi React Hook Form ?

React Hook Form est une bibliothèque open source qui simplifie la création et la validation de formulaires en React. Elle pèse environ 9 Ko, ne provoque pas de re-renders inutiles grâce à l’utilisation de refs, et fournit des hooks comme useForm et useFieldArray. Elle est compatible avec les schémas de validation Yup, Zod et Joi via un système de resolvers.


Comment valider un formulaire avec JavaScript ?

En React, la validation JavaScript se fait soit inline (règles required, pattern, minLength passées directement dans les options du champ), soit via un schéma de validation externe avec des bibliothèques comme Yup ou Zod. L’approche par schéma est recommandée pour les formulaires complexes, car elle centralise les règles et les rend testables unitairement. La validation côté client améliore l’UX mais ne remplace jamais la validation serveur.


Quels sont les différents types de formulaire ?

Les principaux types de formulaires en React sont : les formulaires simples (connexion, recherche), les formulaires de saisie structurée (inscription, profil), les formulaires multi-étapes ou wizard (tunnel de commande, onboarding), les formulaires dynamiques (lignes ajoutables/supprimables), et les formulaires connectés à un CMS avec rendu dynamique côté client. Chaque type nécessite une approche technique adaptée.


Quelle est la différence entre un composant contrôlé et non contrôlé en React ?

Un composant contrôlé lie la valeur de l’input au state React via les props value et onChange : React est la source de vérité. Un composant non contrôlé laisse le DOM gérer la valeur et y accède via une ref. Les composants contrôlés offrent plus de contrôle (validation en temps réel, formatage), tandis que les non contrôlés sont utiles pour l’intégration de bibliothèques tierces ou les uploads de fichiers.


Comment créer un formulaire dynamique en React ?

Pour créer un formulaire dynamique en React, utilisez le hook useFieldArray de React Hook Form. Il fournit les méthodes append, remove, insert et move pour manipuler un tableau de champs. Chaque champ reçoit un identifiant unique via field.id à utiliser comme key React, ce qui évite les bugs de re-render lors de la suppression d’un élément.


Damien Roux
Damien Roux

Ingénieur système et expert hébergement web. Fondateur de web-city.fr, il partage guides pratiques, comparatifs objectifs et outils gratuits pour choisir le bon hébergeur et créer son site WordPress.

Retour en haut