logo oujood
🔍

React Forms : Gérez Dynamiquement les Données Utilisateur

OUJOOD.COM

1. Composants Contrôlés (Controlled Components)

Un composant contrôlé est un composant où l'état de chaque champ de saisie est géré par React. Cela signifie que chaque modification dans un champ met à jour directement l'état du composant parent.

⚠️ Note importante : Ce tutoriel utilise React 18+. Si vous utilisez une version antérieure, remplacez createRoot par ReactDOM.render.

📋 Copier le code

import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';

function Formulaire() {
const [nom, setNom] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);

async function handleSubmit(event) {
event.preventDefault(); // Empêche le rechargement de la page
setIsSubmitting(true);

try {
	// Simulation d'un appel API
	await new Promise(resolve => setTimeout(resolve, 1000));
	alert(`Nom : ${nom}, Email : ${email}`);
} catch (error) {
	console.error('Erreur lors de la soumission:', error);
} finally {
	setIsSubmitting(false);
}
}

return (
<form onSubmit={handleSubmit}>
	<div>
		<label htmlFor="nom">
			Nom :
			<input 
				id="nom"
				type="text" 
				value={nom} 
				onChange={(e) => setNom(e.target.value)}
				disabled={isSubmitting}
				aria-required="true"
			/>
		</label>
	</div>
	<div>
		<label htmlFor="email">
			Email :
			<input 
				id="email"
				type="email" 
				value={email} 
				onChange={(e) => setEmail(e.target.value)}
				disabled={isSubmitting}
				aria-required="true"
			/>
		</label>
	</div>
	<button type="submit" disabled={isSubmitting}>
		{isSubmitting ? 'Envoi en cours...' : 'Envoyer'}
	</button>
</form>
);
}

// React 18+ - Nouvelle API
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<Formulaire />);

Explication détaillée :

  1. import { createRoot } from 'react-dom/client'; :
    • Nous utilisons la nouvelle API de React 18 pour le rendu.
  2. const [isSubmitting, setIsSubmitting] = useState(false); :
    • Gestion de l'état de soumission pour améliorer l'UX.
  3. htmlFor="nom" et id="nom" :
    • Association correcte entre labels et inputs pour l'accessibilité.
  4. aria-required="true" :
    • Indication pour les lecteurs d'écran que le champ est obligatoire.
  5. disabled={isSubmitting} :
    • Empêche les soumissions multiples accidentelles.

2. Composants Non Contrôlés avec useRef (Approche Moderne)

Contrairement aux composants contrôlés, les composants non contrôlés utilisent le DOM natif pour gérer les valeurs des champs de saisie. Avec les hooks, nous utilisons useRef au lieu des références de classe.

💡 Conseil : Utilisez les composants non contrôlés uniquement pour des formulaires simples ou quand vous devez intégrer des bibliothèques tierces.

📋 Copier le code

import React, { useRef } from 'react';
import { createRoot } from 'react-dom/client';

function FormulaireNonControle() {
const nomRef = useRef(null);
const emailRef = useRef(null);

const handleSubmit = (event) => {
event.preventDefault(); // Empêche le rechargement de la page
const nom = nomRef.current.value; // Récupère la valeur du champ "Nom"
const email = emailRef.current.value; // Récupère la valeur du champ "Email"

// Validation simple
if (!nom.trim() || !email.trim()) {
	alert('Tous les champs sont obligatoires');
	return;
}

alert(`Nom : ${nom}, Email : ${email}`);
};

return (
<form onSubmit={handleSubmit}>
	<div>
		<label htmlFor="nom-uncontrolled">
			Nom :
			<input 
				id="nom-uncontrolled"
				type="text" 
				ref={nomRef}
				required
				aria-required="true"
			/>
		</label>
	</div>
	<div>
		<label htmlFor="email-uncontrolled">
			Email :
			<input 
				id="email-uncontrolled"
				type="email" 
				ref={emailRef}
				required
				aria-required="true"
			/>
		</label>
	</div>
	<button type="submit">Envoyer</button>
</form>
);
}

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<FormulaireNonControle />);

Explication détaillée :

  1. import React, { useRef } from 'react'; :
    • Nous importons le hook useRef pour créer des références.
  2. const nomRef = useRef(null); :
    • Crée une référence moderne avec les hooks au lieu de React.createRef().
  3. required :
    • Utilise la validation HTML5 native en complément de la validation JavaScript.
  4. nomRef.current.value :
    • Accède directement à la valeur du DOM, comme avec les classes mais de manière plus moderne.

3. Validation Avancée avec Hook Personnalisé

Pour des formulaires plus robustes, nous pouvons créer un hook personnalisé qui gère la validation, l'état et la soumission de manière réutilisable :

📋 Copier le code

import React, { useState, useCallback } from 'react';
import { createRoot } from 'react-dom/client';

// Hook personnalisé pour la gestion des formulaires
function useForm(initialValues, validationRules) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);

// Fonction de validation
const validateField = useCallback((name, value) => {
	const rules = validationRules[name];
	if (!rules) return null;

	if (rules.required && !value.trim()) {
		return `Le champ ${name} est obligatoire`;
	}

	if (rules.minLength && value.length < rules.minLength) {
		return `${name} doit contenir au moins ${rules.minLength} caractères`;
	}

	if (rules.pattern && !rules.pattern.test(value)) {
		return rules.message || `Format de ${name} invalide`;
	}

	return null;
}, [validationRules]);

// Gestion des changements de valeur
const handleChange = useCallback((name, value) => {
	setValues(prev => ({ ...prev, [name]: value }));
	
	// Validation en temps réel
	const error = validateField(name, value);
	setErrors(prev => ({ ...prev, [name]: error }));
}, [validateField]);

// Gestion de la soumission
const handleSubmit = useCallback(async (onSubmit) => {
	setIsSubmitting(true);
	
	// Validation complète
	const newErrors = {};
	Object.keys(values).forEach(key => {
		const error = validateField(key, values[key]);
		if (error) newErrors[key] = error;
	});

	setErrors(newErrors);

	if (Object.keys(newErrors).length === 0) {
		try {
			await onSubmit(values);
		} catch (error) {
			console.error('Erreur de soumission:', error);
		}
	}
	
	setIsSubmitting(false);
}, [values, validateField]);

return { values, errors, isSubmitting, handleChange, handleSubmit };
}

// Composant formulaire utilisant le hook
function FormulaireAvecValidationAvancee() {
const validationRules = {
	nom: {
		required: true,
		minLength: 2,
		pattern: /^[a-zA-ZÀ-ÿ\s]+$/,
		message: "Le nom ne doit contenir que des lettres"
	},
	email: {
		required: true,
		pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
		message: "Format d'email invalide"
	}
};

const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm(
	{ nom: '', email: '' },
	validationRules
);

const onSubmit = async (data) => {
	// Simulation d'appel API
	await new Promise(resolve => setTimeout(resolve, 1500));
	alert(`Données validées et envoyées:\nNom: ${data.nom}\nEmail: ${data.email}`);
};

return (
<form onSubmit={(e) => {
	e.preventDefault();
	handleSubmit(onSubmit);
}}>
	<div>
		<label htmlFor="nom-advanced">
			Nom * :
			<input
				id="nom-advanced"
				type="text"
				value={values.nom}
				onChange={(e) => handleChange('nom', e.target.value)}
				aria-invalid={!!errors.nom}
				aria-describedby={errors.nom ? 'nom-error' : undefined}
				disabled={isSubmitting}
			/>
		</label>
		{errors.nom && (
			<p id="nom-error" style={{ color: 'red', fontSize: '14px', margin: '5px 0' }} role="alert">
				{errors.nom}
			</p>
		)}
	</div>
	<div>
		<label htmlFor="email-advanced">
			Email * :
			<input
				id="email-advanced"
				type="email"
				value={values.email}
				onChange={(e) => handleChange('email', e.target.value)}
				aria-invalid={!!errors.email}
				aria-describedby={errors.email ? 'email-error' : undefined}
				disabled={isSubmitting}
			/>
		</label>
		{errors.email && (
			<p id="email-error" style={{ color: 'red', fontSize: '14px', margin: '5px 0' }} role="alert">
				{errors.email}
			</p>
		)}
	</div>
	<button type="submit" disabled={isSubmitting}>
		{isSubmitting ? 'Validation en cours...' : 'Envoyer'}
	</button>
</form>
);
}

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<FormulaireAvecValidationAvancee />);

Explication détaillée :

  1. useForm(initialValues, validationRules) :
    • Hook personnalisé qui encapsule toute la logique de formulaire.
    • Accepte des valeurs initiales et des règles de validation.
  2. useCallback :
    • Optimise les performances en mémorisant les fonctions.
  3. aria-invalid et aria-describedby :
    • Améliorent l'accessibilité en indiquant les erreurs aux lecteurs d'écran.
  4. role="alert" :
    • Annonce automatiquement les messages d'erreur aux utilisateurs de lecteurs d'écran.
  5. pattern: /^[a-zA-ZÀ-ÿ\s]+$/ :
    • Validation par expression régulière incluant les caractères accentués français.

4. Gestion de Différents Types de Champs

Les formulaires modernes incluent souvent différents types de champs. Voici comment les gérer efficacement :

📋 Copier le code

import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';

function FormulaireComplet() {
const [formData, setFormData] = useState({
	nom: '',
	email: '',
	age: '',
	pays: '',
	newsletter: false,
	genre: '',
	commentaires: ''
});

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

const handleSubmit = (e) => {
	e.preventDefault();
	console.log('Données du formulaire:', formData);
	alert('Formulaire soumis ! Voir la console pour les détails.');
};

return (
<form onSubmit={handleSubmit}>
	{/* Champ texte */}
	<div>
		<label htmlFor="nom-complet">Nom complet :</label>
		<input
			id="nom-complet"
			name="nom"
			type="text"
			value={formData.nom}
			onChange={handleChange}
			required
		/>
	</div>

	{/* Champ email */}
	<div>
		<label htmlFor="email-complet">Email :</label>
		<input
			id="email-complet"
			name="email"
			type="email"
			value={formData.email}
			onChange={handleChange}
			required
		/>
	</div>

	{/* Champ numérique */}
	<div>
		<label htmlFor="age">Âge :</label>
		<input
			id="age"
			name="age"
			type="number"
			min="13"
			max="120"
			value={formData.age}
			onChange={handleChange}
		/>
	</div>

	{/* Select */}
	<div>
		<label htmlFor="pays">Pays :</label>
		<select id="pays" name="pays" value={formData.pays} onChange={handleChange}>
			<option value="">Sélectionnez un pays</option>
			<option value="france">France</option>
			<option value="belgique">Belgique</option>
			<option value="suisse">Suisse</option>
			<option value="canada">Canada</option>
		</select>
	</div>

	{/* Checkbox */}
	<div>
		<label>
			<input
				name="newsletter"
				type="checkbox"
				checked={formData.newsletter}
				onChange={handleChange}
			/>
			S'abonner à la newsletter
		</label>
	</div>

	{/* Radio buttons */}
	<fieldset>
		<legend>Genre :</legend>
		<label>
			<input
				name="genre"
				type="radio"
				value="homme"
				checked={formData.genre === 'homme'}
				onChange={handleChange}
			/>
			Homme
		</label>
		<label>
			<input
				name="genre"
				type="radio"
				value="femme"
				checked={formData.genre === 'femme'}
				onChange={handleChange}
			/>
			Femme
		</label>
		<label>
			<input
				name="genre"
				type="radio"
				value="autre"
				checked={formData.genre === 'autre'}
				onChange={handleChange}
			/>
			Autre
		</label>
	</fieldset>

	{/* Textarea */}
	<div>
		<label htmlFor="commentaires">Commentaires :</label>
		<textarea
			id="commentaires"
			name="commentaires"
			value={formData.commentaires}
			onChange={handleChange}
			rows="4"
			cols="50"
			placeholder="Vos commentaires ici..."
		></textarea>
	</div>

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

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<FormulaireComplet />);

5. Meilleures Pratiques pour les Formulaires React

Pour créer des formulaires React efficaces et maintenables, voici quelques conseils modernisés :

  • Privilégiez les composants contrôlés : Ils offrent une meilleure intégration avec React et facilitent la validation en temps réel.
  • Utilisez toujours event.preventDefault() : Cela évite le rechargement de la page lors de la soumission du formulaire.
  • Implémentez une validation côté client robuste : Utilisez des expressions régulières et des règles métier appropriées.
  • Gérez l'état de soumission : Désactivez les champs et le bouton pendant le traitement pour éviter les doublons.
  • Pensez à l'accessibilité : Utilisez les attributs ARIA, associez correctement les labels, et gérez le focus.
  • Optimisez les performances : Utilisez useCallback et useMemo pour éviter les re-rendus inutiles.
  • Créez des hooks personnalisés : Réutilisez la logique de formulaire dans toute votre application.

6. Bibliothèques Recommandées

Pour des formulaires complexes, considérez ces bibliothèques populaires :

  • React Hook Form : Performante, avec validation intégrée et faible boilerplate.
  • Formik : Solution complète avec validation Yup intégrée.
  • React Final Form : Basée sur une architecture subscription pour les performances.
⚠️ Attention : Toujours valider les données côté serveur également, la validation côté client peut être contournée.

Conclusion

Vous avez maintenant appris à gérer des formulaires modernes dans React en utilisant les hooks, la validation avancée, et les meilleures pratiques d'accessibilité. Les techniques présentées vous permettront de créer des formulaires robustes et réutilisables.

Prochain chapitre : React Router