Formulaires contrôlés - Séance 3
Les formulaires sont au cœur de nombreuses applications web. En React, nous utilisons des composants contrôlés pour gérer les formulaires.
Contrôlé vs Non-contrôlé
Composant non-contrôlé (DOM)
En HTML classique, le DOM garde la valeur de l'input :
<!-- Le DOM contrôle la valeur -->
<input type="text" />
// On lit la valeur depuis le DOM
const value = document.querySelector('input').value;
Composant contrôlé (React) ✅
En React, c'est React qui contrôle la valeur via le state :
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
type="text"
value={value} // ← React contrôle la valeur
onChange={e => setValue(e.target.value)}
/>
);
}
Pourquoi préférer les composants contrôlés ?
Avantages :
- ✅ Source unique de vérité : la valeur vit dans le state React
- ✅ Validation en temps réel : vous pouvez valider à chaque saisie
- ✅ Transformation : vous pouvez modifier la valeur (majuscules, format, etc.)
- ✅ Désactivation conditionnelle : facile à gérer
- ✅ Soumission : les données sont déjà dans le state
La philosophie React : React contrôle tout !
Dans un composant contrôlé, qui contrôle la valeur de l'input ?
Input texte contrôlé
Le pattern de base pour tous les inputs texte :
function TextInput() {
const [value, setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder="Tapez quelque chose..."
/>
<p>Vous avez tapé : {value}</p>
</div>
);
}
Avec transformation en temps réel
function UppercaseInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
// Tout mettre en majuscules
setValue(e.target.value.toUpperCase());
};
return (
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Tapez en minuscules..."
/>
);
}
Avec limite de caractères
function LimitedInput() {
const [value, setValue] = useState('');
const maxLength = 10;
const handleChange = (e) => {
const newValue = e.target.value;
if (newValue.length <= maxLength) {
setValue(newValue);
}
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
/>
<p>{value.length} / {maxLength} caractères</p>
</div>
);
}
Différents types d'inputs
Email, password, number
Les autres types d'inputs texte fonctionnent pareil :
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<form>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Mot de passe"
/>
<input
type="number"
value={age}
onChange={e => setAge(e.target.value)}
placeholder="Âge"
/>
</form>
);
}
Checkbox
Pour les checkboxes, utilisez checked au lieu de value :
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
return (
<label>
<input
type="checkbox"
checked={isChecked} // ← checked, pas value
onChange={e => setIsChecked(e.target.checked)} // ← .checked
/>
J'accepte les conditions
</label>
);
}
Checkbox : checked vs value
// ❌ FAUX
<input type="checkbox" value={isChecked} />
// ✅ CORRECT
<input type="checkbox" checked={isChecked} />
Pour un composant checkbox contrôlé, quel attribut devez-vous utiliser pour lier l'état ?
Radio buttons
Pour les boutons radio, le state contient la valeur sélectionnée :
function RadioExample() {
const [role, setRole] = useState('user');
return (
<div>
<label>
<input
type="radio"
value="user"
checked={role === 'user'}
onChange={e => setRole(e.target.value)}
/>
Utilisateur
</label>
<label>
<input
type="radio"
value="admin"
checked={role === 'admin'}
onChange={e => setRole(e.target.value)}
/>
Administrateur
</label>
<p>Rôle sélectionné : {role}</p>
</div>
);
}
Select (dropdown)
function SelectExample() {
const [country, setCountry] = useState('fr');
return (
<select
value={country}
onChange={e => setCountry(e.target.value)}
>
<option value="fr">France</option>
<option value="us">États-Unis</option>
<option value="uk">Royaume-Uni</option>
</select>
);
}
Textarea
Le textarea fonctionne comme un input texte :
function TextareaExample() {
const [message, setMessage] = useState('');
return (
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
placeholder="Votre message..."
rows={5}
/>
);
}
Différence avec HTML
En HTML, le contenu du textarea est entre les balises :
<!-- HTML classique -->
<textarea>Contenu ici</textarea>
En React, utilisez l'attribut value :
// React
<textarea value={message} onChange={...} />
Quelle est la différence principale entre un textarea en HTML et en React ?
Formulaire complet avec plusieurs inputs
Pattern : un objet pour tout le formulaire
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
age: '',
terms: false,
role: 'user'
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Données du formulaire :', formData);
};
return (
<form onSubmit={handleSubmit}>
{/* Text input */}
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Nom d'utilisateur"
/>
{/* Email input */}
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{/* Password input */}
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Mot de passe"
/>
{/* Number input */}
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
placeholder="Âge"
/>
{/* Checkbox */}
<label>
<input
type="checkbox"
name="terms"
checked={formData.terms}
onChange={handleChange}
/>
J'accepte les conditions
</label>
{/* Select */}
<select
name="role"
value={formData.role}
onChange={handleChange}
>
<option value="user">Utilisateur</option>
<option value="admin">Admin</option>
</select>
<button type="submit">S'inscrire</button>
</form>
);
}
Attribut name important !
Le pattern ci-dessus utilise l'attribut name pour identifier chaque champ :
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value // ← [name] = computed property name
});
};
Avantage : un seul gestionnaire pour tous les inputs !
Pourquoi utilise-t-on l'attribut 'name' dans le pattern avec un objet pour le formulaire ?
Gestion de la soumission
Pattern complet
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitMessage, setSubmitMessage] = useState('');
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitMessage('');
try {
// Simuler un appel API
await new Promise(resolve => setTimeout(resolve, 1000));
setSubmitMessage('Message envoyé avec succès !');
// Réinitialiser le formulaire
setFormData({
name: '',
email: '',
message: ''
});
} catch (error) {
setSubmitMessage('Erreur lors de l\'envoi.');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
required
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
required
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Envoi...' : 'Envoyer'}
</button>
{submitMessage && <p>{submitMessage}</p>}
</form>
);
}
Réinitialiser un formulaire
Méthode 1 : État initial
function Form() {
const initialState = {
name: '',
email: ''
};
const [formData, setFormData] = useState(initialState);
const handleReset = () => {
setFormData(initialState);
};
return (
<form>
{/* inputs */}
<button type="button" onClick={handleReset}>
Réinitialiser
</button>
</form>
);
}
Méthode 2 : Après soumission
const handleSubmit = (e) => {
e.preventDefault();
// Traiter les données
console.log(formData);
// Réinitialiser
setFormData({ name: '', email: '' });
};
Patterns avancés
Désactiver le bouton si formulaire invalide
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const isValid = name.length > 0 && email.includes('@');
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<button type="submit" disabled={!isValid}>
Envoyer
</button>
</form>
);
}
Afficher un compteur de caractères
function MessageInput() {
const [message, setMessage] = useState('');
const maxLength = 200;
return (
<div>
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
maxLength={maxLength}
/>
<p>
{message.length} / {maxLength} caractères
</p>
</div>
);
}
Transformation automatique
function UsernameInput() {
const [username, setUsername] = useState('');
const handleChange = (e) => {
// Minuscules uniquement, sans espaces
const cleaned = e.target.value
.toLowerCase()
.replace(/\s/g, '');
setUsername(cleaned);
};
return (
<input
value={username}
onChange={handleChange}
placeholder="nom_utilisateur"
/>
);
}
Erreurs courantes
Oublier onChange
// ❌ ERREUR : input en lecture seule
<input type="text" value={name} />
// Warning: You provided a `value` prop without an `onChange` handler
// ✅ CORRECT
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
Utiliser value sur une checkbox
// ❌ ERREUR
<input
type="checkbox"
value={isChecked}
onChange={e => setIsChecked(e.target.value)}
/>
// ✅ CORRECT
<input
type="checkbox"
checked={isChecked}
onChange={e => setIsChecked(e.target.checked)}
/>
Oublier preventDefault
// ❌ La page recharge !
const handleSubmit = (e) => {
console.log('Submit');
};
// ✅ CORRECT
const handleSubmit = (e) => {
e.preventDefault(); // ← Important !
console.log('Submit');
};
Que se passe-t-il si vous oubliez e.preventDefault() dans le gestionnaire onSubmit ?
Récapitulatif
Ce que vous avez appris
Formulaires contrôlés en React :
- Input texte :
value+onChangeavece.target.value - Checkbox :
checked+onChangeavece.target.checked - Select :
value+onChangecomme un input - Textarea :
value+onChange(pas de children) - Pattern objet : un seul state pour tout le formulaire
- Soumission :
onSubmit+e.preventDefault() - Validation : désactiver le bouton si invalide
Prochaine étape : apprendre à valider les formulaires !