Cours Next.js · Module 6: Formulaires et Server Actions
Composants contrôlés vs non contrôlés
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 ?
- La valeur du champ est liée à l'état React via l'attribut
value - Chaque changement est géré par un gestionnaire d'événement (le plus souvent
onChange) - 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
inputValuestocke la valeur actuelle du champ du formulaire - La fonction
handleChangemet à jour l'état à chaque changement - Le champ de saisie a son attribut
valuefixé à 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
- Contrôle total sur l'état du formulaire - tu connais la valeur exacte de chaque champ à tout moment
- Validation instantanée - tu peux valider les données au fur et à mesure de la saisie
- Modification dynamique du formulaire - tu peux modifier dynamiquement d'autres champs en fonction des données saisies
- Tests plus faciles - l'état est explicite et prévisible
- 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
- Plus de code - il faut définir un état et des gestionnaires pour chaque champ
- Plus de rendus - chaque changement de caractère provoque un re-rendu
- 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 ?
- Une ref est créée pour l'élément du formulaire via
useRefoucreateRef - L'élément du formulaire est lié à la ref via l'attribut
ref - 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:
inputRefcontient une référence à l'élémentinput- La valeur du champ est contrôlée par le DOM lui-même
- On utilise
defaultValueau lieu devaluepour 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
- Moins de code - pas besoin de créer un état et des gestionnaires pour chaque champ
- Meilleures performances - moins de re-rendus pendant l'interaction avec le formulaire
- Intégration plus facile avec des bibliothèques et du code tiers
- Implémentation plus simple pour les formulaires comportant de nombreux champs
Inconvénients des composants non contrôlés
- Pas de contrôle en temps réel des valeurs - tu ne les connais qu'au moment de la lecture
- Modification dynamique du formulaire plus difficile - comme les dépendances entre champs
- Validation pendant la saisie plus difficile - il faut utiliser des écouteurs d'événements DOM
- 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 :
- Tu as besoin d'une validation instantanée des données (par ex. pendant la saisie)
- Tu veux modifier dynamiquement la disponibilité ou les valeurs d'autres champs du formulaire
- Tu dois formater les données saisies à la volée
- Tu construis un formulaire interactif avec de nombreuses dépendances
- Tu veux implémenter l'autocomplétion ou des suggestions
- Tu crées un formulaire de type « aperçu en direct »
Utilise les composants non contrôlés lorsque :
- Tu construis un formulaire simple sans interactions complexes
- Tu intègres des bibliothèques de formulaires externes
- Tu convertis une application existante vers React
- Tu travailles avec de très grands formulaires où la performance est essentielle
- 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
usernameest contrôlé car il nécessite une validation immédiate - Les champs
emailetpasswordsont 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'attributhtmlFor - 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éristique | Contrôlé | Non contrôlé |
|---|---|---|
| Où l'état est stocké | Dans l'état React | Dans le DOM |
| Comment récupérer les valeurs | Directement depuis l'état | Via des références (refs) |
| Comment définir les valeurs | Via value et onChange | Via defaultValue |
| Accès immédiat aux valeurs | Oui | Non |
| Validation pendant la saisie | Facile | Plus difficile |
| Gestion des dépendances dynamiques | Facile | Plus difficile |
| Performances | Moindres (plus de rendus) | Meilleures (moins de rendus) |
| Quantité de code | Plus | Moins |
| Cas d'usage typique | Formulaires avec interactions dynamiques | Formulaires 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. Les composants contrôlés en React signifient :
2. Les composants non contrôlés en React utilisent :