Cours JavaScript et React · Module 16: Gestion des erreurs et Suspense
La gestion des erreurs dans les formulaires
Dans cette leçon5
Imagine que ta station spatiale dispose d'un panneau d'enregistrement de l'équipage. Chaque champ de formulaire est comme un capteur du vaisseau : si les données sont incorrectes, le système doit immédiatement avertir l'opérateur avant le lancement de la séquence de décollage. La gestion des erreurs dans les formulaires est un élément UX clé de toute application React.
Les modèles de validation des formulaires
Dans React, on peut distinguer deux grandes approches de validation des formulaires :
1. Validation côté client
Validation effectuée dans le navigateur avant l'envoi des données au serveur. Elle est rapide et offre un retour immédiat.
1function RegistrationForm() {
2 const [email, setEmail] = useState('');
3 const [errors, setErrors] = useState({});
4
5 const validateEmail = (value) => {
6 if (!value) return 'L\'email est requis';
7 if (!value.includes('@')) return 'Format d\'email invalide';
8 return null;
9 };
10
11 const handleChange = (e) => {
12 const value = e.target.value;
13 setEmail(value);
14 const error = validateEmail(value);
15 setErrors(prev => ({ ...prev, email: error }));
16 };
17
18 return (
19 <div>
20 <input value={email} onChange={handleChange} />
21 {errors.email && <p className="error">{errors.email}</p>}
22 </div>
23 );
24}2. Validation côté serveur
Le serveur valide les données après l'envoi du formulaire et retourne les erreurs. Elle est essentielle pour la sécurité : la validation côté client peut être contournée.
1const handleSubmit = async (formData) => {
2 try {
3 const response = await fetch('/api/register', {
4 method: 'POST',
5 body: JSON.stringify(formData),
6 });
7 const result = await response.json();
8
9 if (!response.ok) {
10 // Le serveur a retourné des erreurs de validation
11 setErrors(result.errors);
12 }
13 } catch (error) {
14 setErrors({ general: 'Erreur de connexion au serveur' });
15 }
16};Afficher les erreurs au niveau du champ
Chaque champ de formulaire doit avoir son propre message d'erreur, affiché juste sous le champ. C'est comme des alarmes individuelles pour chaque système d'un vaisseau spatial.
1function CrewRegistration() {
2 const [formData, setFormData] = useState({
3 name: '', callsign: '', rank: ''
4 });
5 const [errors, setErrors] = useState({});
6 const [touched, setTouched] = useState({});
7
8 const validate = (field, value) => {
9 switch (field) {
10 case 'name':
11 if (!value.trim()) return 'Le nom est requis';
12 if (value.length < 2) return 'Le nom doit faire au moins 2 caractères';
13 return null;
14 case 'callsign':
15 if (!value.trim()) return 'L\'indicatif est requis';
16 if (!/^[A-Z0-9-]+$/.test(value)) return 'Uniquement majuscules, chiffres et tirets';
17 return null;
18 case 'rank':
19 if (!value) return 'Choisis un grade';
20 return null;
21 default:
22 return null;
23 }
24 };
25
26 const handleBlur = (field) => {
27 setTouched(prev => ({ ...prev, [field]: true }));
28 const error = validate(field, formData[field]);
29 setErrors(prev => ({ ...prev, [field]: error }));
30 };
31
32 const handleChange = (field, value) => {
33 setFormData(prev => ({ ...prev, [field]: value }));
34 // Valide uniquement si le champ a déjà été touché
35 if (touched[field]) {
36 const error = validate(field, value);
37 setErrors(prev => ({ ...prev, [field]: error }));
38 }
39 };
40
41 return (
42 <form>
43 <div>
44 <label>Nom</label>
45 <input
46 value={formData.name}
47 onChange={(e) => handleChange('name', e.target.value)}
48 onBlur={() => handleBlur('name')}
49 className={errors.name ? 'input-error' : ''}
50 />
51 {touched.name && errors.name && (
52 <p className="field-error">{errors.name}</p>
53 )}
54 </div>
55 {/* Même modèle pour les champs restants */}
56 </form>
57 );
58}Remarque le modèle touched : nous n'affichons les erreurs qu'après que l'utilisateur a quitté le champ (onBlur). On ne veut pas effrayer le pilote avec des alarmes avant même qu'il ait fini de saisir ses données !
Gérer les erreurs serveur dans les formulaires
Le serveur peut retourner des erreurs impossibles à prévoir côté client, par ex. « cet email est déjà pris » ou « limite d'inscriptions atteinte ».
1function SubmitWithServerErrors() {
2 const [errors, setErrors] = useState({});
3 const [serverError, setServerError] = useState(null);
4
5 const handleSubmit = async (e) => {
6 e.preventDefault();
7 setServerError(null);
8
9 try {
10 const response = await fetch('/api/crew/register', {
11 method: 'POST',
12 headers: { 'Content-Type': 'application/json' },
13 body: JSON.stringify(formData),
14 });
15
16 const data = await response.json();
17
18 if (!response.ok) {
19 if (data.fieldErrors) {
20 // Erreurs spécifiques à un champ (par ex. « email déjà existant »)
21 setErrors(data.fieldErrors);
22 }
23 if (data.message) {
24 // Erreur serveur générale
25 setServerError(data.message);
26 }
27 return;
28 }
29
30 // Succès - redirige ou affiche un message
31 } catch (error) {
32 setServerError('Impossible de se connecter à la station de base');
33 }
34 };
35
36 return (
37 <form onSubmit={handleSubmit}>
38 {serverError && (
39 <div className="server-error-banner">{serverError}</div>
40 )}
41 {/* Champs du formulaire avec erreurs au niveau du champ */}
42 </form>
43 );
44}try-catch dans handleSubmit
Chaque gestionnaire de soumission de formulaire doit être enveloppé dans un try-catch. Sans cela, une erreur réseau ou une erreur serveur inattendue provoquera une exception non gérée.
1const handleSubmit = async (e) => {
2 e.preventDefault();
3 setIsSubmitting(true);
4 setErrors({});
5
6 try {
7 // 1. Validation locale
8 const validationErrors = validateAllFields(formData);
9 if (Object.keys(validationErrors).length > 0) {
10 setErrors(validationErrors);
11 return;
12 }
13
14 // 2. Envoi au serveur
15 const response = await submitToServer(formData);
16
17 // 3. Traitement de la réponse du serveur
18 if (!response.ok) {
19 const errorData = await response.json();
20 throw new Error(errorData.message || 'Server error');
21 }
22
23 // 4. Succès
24 setSuccess(true);
25 } catch (error) {
26 // 5. Gestion de l'erreur
27 setErrors({ submit: error.message });
28 } finally {
29 // 6. Toujours exécuté - désactive le loader
30 setIsSubmitting(false);
31 }
32};Bonnes pratiques UX pour les messages d'erreur
De bons messages d'erreur dans les formulaires doivent être :
- Précis - « Le mot de passe doit faire au moins 8 caractères » plutôt que « Mot de passe invalide »
- Utiles - indiquer à l'utilisateur COMMENT corriger l'erreur
- Visibles - afficher l'erreur près du champ concerné
- Doux - ne pas montrer toutes les erreurs d'un coup avant la première soumission
- Accessibles - utiliser les attributs
aria-invalidetaria-describedby
1<div className="form-field">
2 <label htmlFor="password">Mot de passe</label>
3 <input
4 id="password"
5 type="password"
6 aria-invalid={!!errors.password}
7 aria-describedby={errors.password ? 'password-error' : undefined}
8 />
9 {errors.password && (
10 <p id="password-error" className="field-error" role="alert">
11 {errors.password}
12 </p>
13 )}
14</div>Souviens-toi : un formulaire, c'est comme le panneau de contrôle d'un vaisseau spatial. Chaque erreur doit être communiquée de façon claire et précise pour que le pilote (l'utilisateur) sache exactement quoi corriger. Ne montre jamais de codes d'erreur mystérieux : traduis-les toujours en langage compréhensible par un humain.
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Formulaire d'enregistrement d'un membre d'équipage avec validation
5function CrewRegistrationForm() {
6 const [formData, setFormData] = useState({
7 name: '', callsign: '', email: '', rank: ''
8 });
9 const [errors, setErrors] = useState({});
10 const [touched, setTouched] = useState({});
11 const [serverError, setServerError] = useState(null);
12 const [isSubmitting, setIsSubmitting] = useState(false);
13 const [success, setSuccess] = useState(false);
14
15 const validate = (field, value) => {
16 switch (field) {
17 case 'name':
18 if (!value.trim()) return 'Le nom est requis';
19 if (value.length < 2) return 'Le nom doit faire au moins 2 caractères';
20 return null;
21 case 'callsign':
22 if (!value.trim()) return 'L\'indicatif est requis';
23 if (!/^[A-Za-z0-9-]+$/.test(value))
24 return 'Uniquement lettres, chiffres et tirets';
25 return null;
26 case 'email':
27 if (!value.trim()) return 'L\'email est requis';
28 if (!value.includes('@')) return 'Format d\'email invalide';
29 return null;
30 case 'rank':
31 if (!value) return 'Choisis un grade';
32 return null;
33 default:
34 return null;
35 }
36 };
37
38 const handleChange = (field, value) => {
39 setFormData(prev => ({ ...prev, [field]: value }));
40 if (touched[field]) {
41 const error = validate(field, value);
42 setErrors(prev => ({ ...prev, [field]: error }));
43 }
44 };
45
46 const handleBlur = (field) => {
47 setTouched(prev => ({ ...prev, [field]: true }));
48 const error = validate(field, formData[field]);
49 setErrors(prev => ({ ...prev, [field]: error }));
50 };
51
52 const handleSubmit = async (e) => {
53 e.preventDefault();
54 setServerError(null);
55 setIsSubmitting(true);
56
57 // Validation de tous les champs
58 const allErrors = {};
59 const allTouched = {};
60 for (const field of Object.keys(formData)) {
61 allTouched[field] = true;
62 const error = validate(field, formData[field]);
63 if (error) allErrors[field] = error;
64 }
65 setTouched(allTouched);
66 setErrors(allErrors);
67
68 if (Object.keys(allErrors).length > 0) {
69 setIsSubmitting(false);
70 return;
71 }
72
73 try {
74 // Simulation d'envoi au serveur
75 await new Promise(resolve => setTimeout(resolve, 1000));
76
77 // Simulation d'une erreur serveur (50% de chances)
78 if (Math.random() > 0.5) {
79 throw new Error('Le serveur a rejeté l\'inscription : l\'indicatif existe déjà');
80 }
81
82 setSuccess(true);
83 } catch (error) {
84 setServerError(error.message);
85 } finally {
86 setIsSubmitting(false);
87 }
88 };
89
90 if (success) {
91 return (
92 <div className="success-panel">
93 <h2>Inscription réussie !</h2>
94 <p>Bienvenue à bord, {formData.callsign} !</p>
95 <button onClick={() => { setSuccess(false); setFormData({ name: '', callsign: '', email: '', rank: '' }); setTouched({}); }}>
96 Enregistrer le suivant
97 </button>
98 </div>
99 );
100 }
101
102 return (
103 <form onSubmit={handleSubmit} className="form">
104 {serverError && (
105 <div className="server-error">{serverError}</div>
106 )}
107
108 <div className="field">
109 <label>Nom</label>
110 <input
111 value={formData.name}
112 onChange={(e) => handleChange('name', e.target.value)}
113 onBlur={() => handleBlur('name')}
114 className={touched.name && errors.name ? 'input-error' : ''}
115 placeholder="Ex. Jean Dupont"
116 />
117 {touched.name && errors.name && (
118 <p className="field-error">{errors.name}</p>
119 )}
120 </div>
121
122 <div className="field">
123 <label>Indicatif</label>
124 <input
125 value={formData.callsign}
126 onChange={(e) => handleChange('callsign', e.target.value)}
127 onBlur={() => handleBlur('callsign')}
128 className={touched.callsign && errors.callsign ? 'input-error' : ''}
129 placeholder="Ex. NOVA-7"
130 />
131 {touched.callsign && errors.callsign && (
132 <p className="field-error">{errors.callsign}</p>
133 )}
134 </div>
135
136 <div className="field">
137 <label>Email</label>
138 <input
139 value={formData.email}
140 onChange={(e) => handleChange('email', e.target.value)}
141 onBlur={() => handleBlur('email')}
142 className={touched.email && errors.email ? 'input-error' : ''}
143 placeholder="Ex. jean@station.cosmos"
144 />
145 {touched.email && errors.email && (
146 <p className="field-error">{errors.email}</p>
147 )}
148 </div>
149
150 <div className="field">
151 <label>Grade</label>
152 <select
153 value={formData.rank}
154 onChange={(e) => handleChange('rank', e.target.value)}
155 onBlur={() => handleBlur('rank')}
156 className={touched.rank && errors.rank ? 'input-error' : ''}
157 >
158 <option value="">-- Choisis un grade --</option>
159 <option value="cadet">Cadet</option>
160 <option value="officer">Officier</option>
161 <option value="commander">Commandant</option>
162 </select>
163 {touched.rank && errors.rank && (
164 <p className="field-error">{errors.rank}</p>
165 )}
166 </div>
167
168 <button type="submit" className="submit-btn" disabled={isSubmitting}>
169 {isSubmitting ? 'Envoi...' : 'Enregistrer le membre d\'équipage'}
170 </button>
171 </form>
172 );
173}
174
175export default function App() {
176 return (
177 <div className="app">
178 <h1>Enregistrement de l'équipage</h1>
179 <p className="subtitle">Validation de formulaire avec gestion des erreurs</p>
180 <CrewRegistrationForm />
181 </div>
182 );
183}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. Quel est le principal avantage de la validation de formulaire côté client ?
2. Pourquoi utilisons-nous le modèle touched (onBlur) pour afficher les erreurs de validation ?
Tâches pratiques dans le jeu
- Clique dans l'ordre
Clique les éléments dans l'ordre pour les étapes du handleSubmit d'un formulaire :
- Éditeur de code
Implémente un formulaire avec validation et gestion des erreurs
- Tri horizontal
Ordonne la syntaxe correcte pour afficher conditionnellement une erreur de champ :
- Tri vertical
Ordonne les types d'erreurs de formulaire de la plus tôt gérée à la plus tardive :