Cours Next.js · Module 6: Formulaires et Server Actions

Composants contrôlés vs non contrôlés

11 min de lecture
Dans cette leçon7

En React, il existe deux approches principales pour gérer les données d'un formulaire : les composants contrôlés et les composants non contrôlés. Chacune a ses avantages et ses inconvénients, et le choix entre elles dépend du cas d'usage précis. Dans ce module, nous comparerons en détail ces deux approches.

Composants contrôlés

Les composants contrôlés sont ceux où React contrôle à la fois le rendu et le comportement des éléments du formulaire. La valeur de l'élément du formulaire est stockée dans l'état React et mise à jour en réponse aux événements de l'utilisateur.

Comment fonctionnent les composants contrôlés ?

  1. La valeur du champ est liée à l'état React via l'attribut value
  2. Chaque changement est géré par un gestionnaire d'événement (le plus souvent onChange)
  3. La nouvelle valeur est enregistrée dans l'état et utilisée pour re-rendre le composant

Exemple simple de composant contrôlé

1import { useState, ChangeEvent, FormEvent } from 'react';
2
3export default function ControlledForm() {
4  const [inputValue, setInputValue] = useState('');
5
6  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
7    setInputValue(e.target.value);
8  };
9
10  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
11    e.preventDefault();
12    alert(`Valeur soumise : ${inputValue}`);
13  };
14
15  return (
16    <form onSubmit={handleSubmit}>
17      <label>
18        Prénom :
19        <input
20          type="text"
21          value={inputValue}
22          onChange={handleChange}
23        />
24      </label>
25      <div>Valeur actuelle : {inputValue}</div>
26      <button type="submit">Envoyer</button>
27    </form>
28  );
29}

In this example:

  • L'état inputValue stocke la valeur actuelle du champ du formulaire
  • La fonction handleChange met à jour l'état à chaque changement
  • Le champ de saisie a son attribut value fixé à la valeur de l'état

Exemple plus complexe avec différents types de champs

1import { useState, ChangeEvent, FormEvent } from 'react';
2
3interface FormState {
4  name: string;
5  email: string;
6  gender: string;
7  subscribe: boolean;
8  comments: string;
9  favoriteColor: string;
10}
11
12export default function ControlledFormAdvanced() {
13  const [formData, setFormData] = useState<FormState>({
14    name: '',
15    email: '',
16    gender: '',
17    subscribe: false,
18    comments: '',
19    favoriteColor: 'blue'
20  });
21
22  const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
23    const { name, value, type } = e.target;
24    const checked = (e.target as HTMLInputElement).checked;
25
26    setFormData(prev => ({
27      ...prev,
28      [name]: type === 'checkbox' ? checked : value
29    }));
30  };
31
32  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
33    e.preventDefault();
34    console.log('Form data:', formData);
35  };
36
37  return (
38    <form onSubmit={handleSubmit} className="space-y-4">
39      <div>
40        <label htmlFor="name" className="block mb-1">Nom complet</label>
41        <input
42          type="text"
43          id="name"
44          name="name"
45          value={formData.name}
46          onChange={handleChange}
47          className="w-full p-2 border rounded"
48        />
49      </div>
50
51      <div>
52        <label htmlFor="email" className="block mb-1">Email</label>
53        <input
54          type="email"
55          id="email"
56          name="email"
57          value={formData.email}
58          onChange={handleChange}
59          className="w-full p-2 border rounded"
60        />
61      </div>
62
63      <div>
64        <label className="block mb-1">Gender</label>
65        <div className="space-x-4">
66          <label className="inline-flex items-center">
67            <input
68              type="radio"
69              name="gender"
70              value="male"
71              checked={formData.gender === 'male'}
72              onChange={handleChange}
73              className="mr-2"
74            />
75            Homme
76          </label>
77          <label className="inline-flex items-center">
78            <input
79              type="radio"
80              name="gender"
81              value="female"
82              checked={formData.gender === 'female'}
83              onChange={handleChange}
84              className="mr-2"
85            />
86            Femme
87          </label>
88          <label className="inline-flex items-center">
89            <input
90              type="radio"
91              name="gender"
92              value="other"
93              checked={formData.gender === 'other'}
94              onChange={handleChange}
95              className="mr-2"
96            />
97            Autre
98          </label>
99        </div>
100      </div>
101
102      <div>
103        <label htmlFor="favoriteColor" className="block mb-1">Couleur préférée</label>
104        <select
105          id="favoriteColor"
106          name="favoriteColor"
107          value={formData.favoriteColor}
108          onChange={handleChange}
109          className="w-full p-2 border rounded"
110        >
111          <option value="red">Rouge</option>
112          <option value="green">Vert</option>
113          <option value="blue">Bleu</option>
114          <option value="purple">Violet</option>
115          <option value="yellow">Jaune</option>
116        </select>
117      </div>
118
119      <div>
120        <label htmlFor="comments" className="block mb-1">Commentaires</label>
121        <textarea
122          id="comments"
123          name="comments"
124          value={formData.comments}
125          onChange={handleChange}
126          rows={4}
127          className="w-full p-2 border rounded"
128        />
129      </div>
130
131      <div>
132        <label className="inline-flex items-center">
133          <input
134            type="checkbox"
135            name="subscribe"
136            checked={formData.subscribe}
137            onChange={handleChange}
138            className="mr-2"
139          />
140          S'abonner à la newsletter
141        </label>
142      </div>
143
144      <button
145        type="submit"
146        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
147      >
148        Envoyer
149      </button>
150
151      <div className="mt-4 p-4 bg-gray-100 rounded">
152        <h3 className="font-bold mb-2">Aperçu des données :</h3>
153        <pre className="whitespace-pre-wrap">
154          {JSON.stringify(formData, null, 2)}
155        </pre>
156      </div>
157    </form>
158  );
159}

Avantages des composants contrôlés

  1. Contrôle total sur l'état du formulaire - tu connais la valeur exacte de chaque champ à tout moment
  2. Validation instantanée - tu peux valider les données au fur et à mesure de la saisie
  3. Modification dynamique du formulaire - tu peux modifier dynamiquement d'autres champs en fonction des données saisies
  4. Tests plus faciles - l'état est explicite et prévisible
  5. Formatage des données à la volée - par ex. le formatage d'un numéro de carte de crédit pendant la saisie

Inconvénients des composants contrôlés

  1. Plus de code - il faut définir un état et des gestionnaires pour chaque champ
  2. Plus de rendus - chaque changement de caractère provoque un re-rendu
  3. Performances moindres sur les grands formulaires - un grand nombre de champs peut affecter les performances

Composants non contrôlés

Les composants non contrôlés sont ceux où l'état du formulaire est géré par le DOM lui-même, et non par React. On utilise des références (refs) pour lire la valeur des champs.

Comment fonctionnent les composants non contrôlés ?

  1. Une ref est créée pour l'élément du formulaire via useRef ou createRef
  2. L'élément du formulaire est lié à la ref via l'attribut ref
  3. La valeur est lue directement depuis le DOM au besoin (par ex. lors de la soumission du formulaire)

Exemple simple de composant non contrôlé

1import { useRef, FormEvent } from 'react';
2
3export default function UncontrolledForm() {
4  const inputRef = useRef<HTMLInputElement>(null);
5
6  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
7    e.preventDefault();
8    const inputValue = inputRef.current?.value || '';
9    alert(`Valeur soumise : ${inputValue}`);
10  };
11
12  return (
13    <form onSubmit={handleSubmit}>
14      <label>
15        First name:
16        <input
17          type="text"
18          ref={inputRef}
19          defaultValue=""
20        />
21      </label>
22      <button type="submit">Send</button>
23    </form>
24  );
25}

In this example:

  • inputRef contient une référence à l'élément input
  • La valeur du champ est contrôlée par le DOM lui-même
  • On utilise defaultValue au lieu de value pour définir la valeur initiale

Exemple plus complexe avec différents types de champs

1import { useRef, FormEvent } from 'react';
2
3export default function UncontrolledFormAdvanced() {
4  const nameRef = useRef<HTMLInputElement>(null);
5  const emailRef = useRef<HTMLInputElement>(null);
6  const maleRef = useRef<HTMLInputElement>(null);
7  const femaleRef = useRef<HTMLInputElement>(null);
8  const otherRef = useRef<HTMLInputElement>(null);
9  const colorRef = useRef<HTMLSelectElement>(null);
10  const commentsRef = useRef<HTMLTextAreaElement>(null);
11  const subscribeRef = useRef<HTMLInputElement>(null);
12
13  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
14    e.preventDefault();
15
16    // Lecture des valeurs depuis les refs
17    const formData = {
18      name: nameRef.current?.value || '',
19      email: emailRef.current?.value || '',
20      gender: maleRef.current?.checked ? 'male' :
21              femaleRef.current?.checked ? 'female' :
22              otherRef.current?.checked ? 'other' : '',
23      favoriteColor: colorRef.current?.value || '',
24      comments: commentsRef.current?.value || '',
25      subscribe: subscribeRef.current?.checked || false
26    };
27
28    console.log('Form data:', formData);
29  };
30
31  return (
32    <form onSubmit={handleSubmit} className="space-y-4">
33      <div>
34        <label htmlFor="name" className="block mb-1">Nom complet</label>
35        <input
36          type="text"
37          id="name"
38          name="name"
39          ref={nameRef}
40          defaultValue=""
41          className="w-full p-2 border rounded"
42        />
43      </div>
44
45      <div>
46        <label htmlFor="email" className="block mb-1">Email</label>
47        <input
48          type="email"
49          id="email"
50          name="email"
51          ref={emailRef}
52          defaultValue=""
53          className="w-full p-2 border rounded"
54        />
55      </div>
56
57      <div>
58        <label className="block mb-1">Gender</label>
59        <div className="space-x-4">
60          <label className="inline-flex items-center">
61            <input
62              type="radio"
63              name="gender"
64              value="male"
65              ref={maleRef}
66              className="mr-2"
67            />
68            Homme
69          </label>
70          <label className="inline-flex items-center">
71            <input
72              type="radio"
73              name="gender"
74              value="female"
75              ref={femaleRef}
76              className="mr-2"
77            />
78            Femme
79          </label>
80          <label className="inline-flex items-center">
81            <input
82              type="radio"
83              name="gender"
84              value="other"
85              ref={otherRef}
86              className="mr-2"
87            />
88            Autre
89          </label>
90        </div>
91      </div>
92
93      <div>
94        <label htmlFor="favoriteColor" className="block mb-1">Couleur préférée</label>
95        <select
96          id="favoriteColor"
97          name="favoriteColor"
98          ref={colorRef}
99          defaultValue="blue"
100          className="w-full p-2 border rounded"
101        >
102          <option value="red">Rouge</option>
103          <option value="green">Vert</option>
104          <option value="blue">Bleu</option>
105          <option value="purple">Violet</option>
106          <option value="yellow">Jaune</option>
107        </select>
108      </div>
109
110      <div>
111        <label htmlFor="comments" className="block mb-1">Commentaires</label>
112        <textarea
113          id="comments"
114          name="comments"
115          ref={commentsRef}
116          defaultValue=""
117          rows={4}
118          className="w-full p-2 border rounded"
119        />
120      </div>
121
122      <div>
123        <label className="inline-flex items-center">
124          <input
125            type="checkbox"
126            name="subscribe"
127            ref={subscribeRef}
128            defaultChecked={false}
129            className="mr-2"
130          />
131          S'abonner à la newsletter
132        </label>
133      </div>
134
135      <button
136        type="submit"
137        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
138      >
139        Envoyer
140      </button>
141    </form>
142  );
143}

Avantages des composants non contrôlés

  1. Moins de code - pas besoin de créer un état et des gestionnaires pour chaque champ
  2. Meilleures performances - moins de re-rendus pendant l'interaction avec le formulaire
  3. Intégration plus facile avec des bibliothèques et du code tiers
  4. Implémentation plus simple pour les formulaires comportant de nombreux champs

Inconvénients des composants non contrôlés

  1. Pas de contrôle en temps réel des valeurs - tu ne les connais qu'au moment de la lecture
  2. Modification dynamique du formulaire plus difficile - comme les dépendances entre champs
  3. Validation pendant la saisie plus difficile - il faut utiliser des écouteurs d'événements DOM
  4. Approche moins « React » - s'éloigne de l'idiome du flux de données unidirectionnel

Quand utiliser chaque approche ?

Utilise les composants contrôlés lorsque :

  1. Tu as besoin d'une validation instantanée des données (par ex. pendant la saisie)
  2. Tu veux modifier dynamiquement la disponibilité ou les valeurs d'autres champs du formulaire
  3. Tu dois formater les données saisies à la volée
  4. Tu construis un formulaire interactif avec de nombreuses dépendances
  5. Tu veux implémenter l'autocomplétion ou des suggestions
  6. Tu crées un formulaire de type « aperçu en direct »

Utilise les composants non contrôlés lorsque :

  1. Tu construis un formulaire simple sans interactions complexes
  2. Tu intègres des bibliothèques de formulaires externes
  3. Tu convertis une application existante vers React
  4. Tu travailles avec de très grands formulaires où la performance est essentielle
  5. La validation n'est effectuée qu'à la soumission du formulaire

Approche hybride

Parfois, la meilleure solution consiste à combiner les deux approches. Tu peux utiliser des composants non contrôlés pour les champs simples et des composants contrôlés pour ceux qui nécessitent une interaction dynamique.

1import { useState, useRef, ChangeEvent, FormEvent } from 'react';
2
3export default function HybridForm() {
4  // Composants contrôlés pour les champs nécessitant une interaction dynamique
5  const [username, setUsername] = useState('');
6  const [isValid, setIsValid] = useState(true);
7
8  // Composants non contrôlés pour les champs plus simples
9  const emailRef = useRef<HTMLInputElement>(null);
10  const passwordRef = useRef<HTMLInputElement>(null);
11
12  const handleUsernameChange = (e: ChangeEvent<HTMLInputElement>) => {
13    const value = e.target.value;
14    setUsername(value);
15
16    // Validation dynamique - vérifier que le nom d'utilisateur fait au moins 3 caractères
17    setIsValid(value.length >= 3);
18  };
19
20  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
21    e.preventDefault();
22
23    if (username.length < 3) {
24      alert("Le nom d'utilisateur doit contenir au moins 3 caractères");
25      return;
26    }
27
28    const formData = {
29      username,
30      email: emailRef.current?.value || '',
31      password: passwordRef.current?.value || ''
32    };
33
34    console.log('Form data:', formData);
35  };
36
37  return (
38    <form onSubmit={handleSubmit} className="space-y-4">
39      <div>
40        <label htmlFor="username" className="block mb-1">Nom d'utilisateur</label>
41        <input
42          type="text"
43          id="username"
44          value={username}
45          onChange={handleUsernameChange}
46          className={`w-full p-2 border rounded ${!isValid ? 'border-red-500' : 'border-gray-300'}`}
47        />
48        {!isValid && <p className="text-red-500 text-xs mt-1">Le nom d'utilisateur doit contenir au moins 3 caractères</p>}
49      </div>
50
51      <div>
52        <label htmlFor="email" className="block mb-1">Email</label>
53        <input
54          type="email"
55          id="email"
56          ref={emailRef}
57          defaultValue=""
58          className="w-full p-2 border rounded border-gray-300"
59        />
60      </div>
61
62      <div>
63        <label htmlFor="password" className="block mb-1">Mot de passe</label>
64        <input
65          type="password"
66          id="password"
67          ref={passwordRef}
68          defaultValue=""
69          className="w-full p-2 border rounded border-gray-300"
70        />
71      </div>
72
73      <div>
74        <div className="flex items-center">
75          <span className="mr-2">Nom d'utilisateur actuel :</span>
76          <span className={`font-semibold ${isValid ? 'text-green-600' : 'text-red-600'}`}>
77            {username || '(non renseigné)'}
78          </span>
79        </div>
80      </div>
81
82      <button
83        type="submit"
84        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
85        disabled={!isValid}
86      >
87        S'inscrire
88      </button>
89    </form>
90  );
91}

In this example:

  • Le champ username est contrôlé car il nécessite une validation immédiate
  • Les champs email et password sont non contrôlés car ils sont plus simples et ne nécessitent pas d'interaction

Bonnes pratiques pour travailler avec les formulaires en React

1. Cohérence de l'approche

Essaie de t'en tenir à une seule approche au sein d'un même formulaire ou composant. Mélanger les styles peut entraîner des bugs et un code plus difficile à maintenir.

2. Utiliser des bibliothèques pour les formulaires complexes

Pour les formulaires plus complexes, envisage d'utiliser des bibliothèques comme Formik, React Hook Form ou Final Form, qui fournissent des abstractions simplifiant le travail avec les formulaires.

3. Validation à plusieurs niveaux

Implémente la validation à la fois côté client (pour une meilleure UX) et côté serveur (pour la sécurité). Des bibliothèques comme Zod, Yup, Joi ou Validator.js peuvent simplifier cette tâche.

4. Accessibilité

Quelle que soit l'approche choisie, pense à l'accessibilité :

  • Utilise des éléments <label> avec l'attribut htmlFor
  • Ajoute des attributs aria-* là où c'est nécessaire
  • Fournis des messages d'erreur appropriés
  • Teste en n'utilisant que le clavier

Comparaison directe

Le tableau suivant résume les principales différences entre les composants contrôlés et non contrôlés :

CaractéristiqueContrôléNon contrôlé
Où l'état est stockéDans l'état ReactDans le DOM
Comment récupérer les valeursDirectement depuis l'étatVia des références (refs)
Comment définir les valeursVia value et onChangeVia defaultValue
Accès immédiat aux valeursOuiNon
Validation pendant la saisieFacilePlus difficile
Gestion des dépendances dynamiquesFacilePlus difficile
PerformancesMoindres (plus de rendus)Meilleures (moins de rendus)
Quantité de codePlusMoins
Cas d'usage typiqueFormulaires avec interactions dynamiquesFormulaires simples

Résumé

Le choix entre composants contrôlés et non contrôlés dépend des exigences spécifiques du formulaire sur lequel tu travailles :

  • Les composants contrôlés offrent un plus grand contrôle et la possibilité d'une interaction immédiate, au prix de plus de code et de performances potentiellement moindres.

  • Les composants non contrôlés sont plus simples à implémenter et plus performants, mais offrent moins de contrôle sur l'état du formulaire pendant son remplissage.

En pratique, la meilleure approche consiste souvent à utiliser des composants contrôlés pour les formulaires nécessitant de riches interactions utilisateur, et des composants non contrôlés pour les cas plus simples. Tu peux aussi envisager une approche hybride ou l'utilisation de bibliothèques comme React Hook Form, qui combinent les avantages des deux mondes.

Au final, l'approche la plus « React » reste celle des composants contrôlés, car ils correspondent mieux au paradigme du flux de données unidirectionnel et au modèle de la « source unique de vérité », mais ce n'est pas toujours la meilleure solution dans tous les cas.

Code de cette leçon : App.tsx
1import React, { useState, FormEvent } from 'react';
2
3// Composants controles - l'etat React controle les valeurs des champs
4
5export default function ControlledFormDemo() {
6  const [name, setName] = useState('');
7  const [email, setEmail] = useState('');
8  const [role, setRole] = useState('citizen');
9  const [submitted, setSubmitted] = useState<any>(null);
10
11  const roles = [
12    { value: 'citizen', label: 'Citoyen' },
13    { value: 'engineer', label: 'Ingenieur' },
14    { value: 'commander', label: 'Commandant' },
15  ];
16
17  const handleSubmit = (e: FormEvent) => {
18    e.preventDefault();
19    setSubmitted({ name, email, role: roles.find(r => r.value === role)?.label });
20  };
21
22  const inputStyle: React.CSSProperties = {
23    width: '100%', padding: '10px', background: '#0d1117',
24    border: '1px solid #333', borderRadius: 6, color: '#e0e0e0',
25    fontFamily: 'monospace', fontSize: 13, boxSizing: 'border-box'
26  };
27
28  return (
29    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
30      <h1 style={{ color: '#64ffda' }}>Composants controles</h1>
31      <p style={{ color: '#888', fontSize: 13, marginBottom: 20 }}>
32        Chaque changement dans un champ met immediatement a jour l'etat React.
33      </p>
34
35      <form onSubmit={handleSubmit} style={{ maxWidth: 400 }}>
36        <div style={{ marginBottom: 12 }}>
37          <label style={{ color: '#64ffda', fontSize: 12, display: 'block', marginBottom: 4 }}>Nom *</label>
38          <input value={name} onChange={e => setName(e.target.value)} style={inputStyle} placeholder="Saisis ton nom" required />
39        </div>
40        <div style={{ marginBottom: 12 }}>
41          <label style={{ color: '#64ffda', fontSize: 12, display: 'block', marginBottom: 4 }}>Email *</label>
42          <input type="email" value={email} onChange={e => setEmail(e.target.value)} style={inputStyle} placeholder="nom@quantum.city" required />
43        </div>
44        <div style={{ marginBottom: 12 }}>
45          <label style={{ color: '#64ffda', fontSize: 12, display: 'block', marginBottom: 4 }}>Role</label>
46          <select value={role} onChange={e => setRole(e.target.value)} style={inputStyle}>
47            {roles.map(r => <option key={r.value} value={r.value}>{r.label}</option>)}
48          </select>
49        </div>
50
51        <button type="submit" disabled={!name || !email} style={{
52          background: name && email ? '#4caf50' : '#333', color: '#fff', border: 'none',
53          borderRadius: 6, padding: '10px 20px', cursor: name && email ? 'pointer' : 'default',
54          fontFamily: 'monospace', width: '100%'
55        }}>
56          Envoyer le formulaire
57        </button>
58      </form>
59
60      {/* Apercu de l'etat en temps reel */}
61      <div style={{ marginTop: 20, background: '#0d1117', borderRadius: 8, padding: 12, maxWidth: 400 }}>
62        <div style={{ color: '#64ffda', fontSize: 12, marginBottom: 8 }}>Etat du formulaire (temps reel) :</div>
63        <pre style={{ color: '#aaa', fontSize: 11, margin: 0 }}>
64{JSON.stringify({ name, email, role }, null, 2)}
65        </pre>
66      </div>
67
68      {submitted && (
69        <div style={{ marginTop: 12, background: '#1b5e20', borderRadius: 8, padding: 12, maxWidth: 400, border: '1px solid #4caf50' }}>
70          <div style={{ color: '#4caf50' }}>Envoye avec succes !</div>
71          <pre style={{ color: '#ccc', fontSize: 11, margin: '8px 0 0' }}>{JSON.stringify(submitted, null, 2)}</pre>
72        </div>
73      )}
74    </div>
75  );
76}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Les composants contrôlés en React signifient :

  2. 2. Les composants non contrôlés en React utilisent :

Articles utiles