Guide React Hook Form
Ce guide explique comment utiliser React Hook Form dans le système EMTB pour créer des formulaires performants et maintenables.
🎯 Vue d'ensemble
React Hook Form est la solution de gestion de formulaires choisie pour EMTB. Il offre des performances optimales, une validation flexible et une intégration parfaite avec Material-UI.
🏗️ Configuration de base
Installation et setup
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
// Schéma de validation Yup
const schema = yup.object().shape({
nom: yup.string().required('Le nom est obligatoire'),
email: yup.string().email('Email invalide').required('L\'email est obligatoire'),
age: yup.number().positive('L\'âge doit être positif').required(),
});
// Types TypeScript
interface FormData {
nom: string;
email: string;
age: number;
}
// Composant de formulaire
const MyForm: React.FC = () => {
const {
control,
handleSubmit,
formState: { errors, isSubmitting, isValid },
reset,
watch,
setValue,
getValues,
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
nom: '',
email: '',
age: 0,
},
mode: 'onChange', // Validation en temps réel
});
const onSubmit = async (data: FormData) => {
try {
console.log('Données du formulaire:', data);
// Traitement des données
reset(); // Réinitialiser le formulaire après soumission
} catch (error) {
console.error('Erreur:', error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Champs du formulaire */}
</form>
);
};
📋 Gestion des champs
Champ texte avec Controller
import { Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
<Controller
name="nom"
control={control}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="Nom"
error={!!error}
helperText={error?.message}
fullWidth
margin="normal"
/>
)}
/>
Champ sélection avec Controller
import { FormControl, InputLabel, Select, MenuItem } from '@mui/material';
<Controller
name="categorie"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl fullWidth error={!!error} margin="normal">
<InputLabel>Catégorie</InputLabel>
<Select {...field} label="Catégorie">
<MenuItem value="client">Client</MenuItem>
<MenuItem value="fournisseur">Fournisseur</MenuItem>
<MenuItem value="partenaire">Partenaire</MenuItem>
</Select>
{error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl>
)}
/>
Champ date avec Controller
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
<Controller
name="dateNaissance"
control={control}
render={({ field, fieldState: { error } }) => (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
{...field}
label="Date de naissance"
slotProps={{
textField: {
error: !!error,
helperText: error?.message,
fullWidth: true,
margin: 'normal',
},
}}
/>
</LocalizationProvider>
)}
/>
🔧 Validation avancée
Validation conditionnelle
const schema = yup.object().shape({
type: yup.string().required(),
montant: yup.number().when('type', {
is: 'PAYANT',
then: (schema) => schema.required('Le montant est obligatoire'),
otherwise: (schema) => schema.optional(),
}),
email: yup.string().when('type', {
is: 'CLIENT',
then: (schema) => schema.email('Email invalide').required('Email obligatoire'),
otherwise: (schema) => schema.optional(),
}),
});
Validation asynchrone
const schema = yup.object().shape({
email: yup
.string()
.email('Format d\'email invalide')
.required('Email obligatoire')
.test('unique-email', 'Cet email est déjà utilisé', async (value) => {
if (!value) return true;
try {
const response = await fetch(`/api/check-email?email=${value}`);
const { exists } = await response.json();
return !exists;
} catch {
return true; // En cas d'erreur, on considère que l'email est valide
}
}),
});
Validation personnalisée
const schema = yup.object().shape({
telephone: yup
.string()
.matches(/^[0-9\s\-\+\(\)]+$/, 'Format de téléphone invalide')
.test('french-phone', 'Numéro de téléphone français invalide', (value) => {
if (!value) return true;
const cleanPhone = value.replace(/\s/g, '');
return /^(0[1-9]|33[1-9])\d{8}$/.test(cleanPhone);
}),
});
🎨 Gestion d'état avancée
Watch et setValue
const MyForm: React.FC = () => {
const { control, watch, setValue, getValues } = useForm<FormData>();
// Surveiller un champ spécifique
const watchedType = watch('type');
// Surveiller plusieurs champs
const watchedFields = watch(['nom', 'email']);
// Mettre à jour un champ programmatiquement
const handleTypeChange = (newType: string) => {
setValue('type', newType);
// Réinitialiser des champs dépendants
if (newType === 'CLIENT') {
setValue('montant', 0);
}
};
// Obtenir toutes les valeurs actuelles
const currentValues = getValues();
return (
<form>
<Controller
name="type"
control={control}
render={({ field }) => (
<Select
{...field}
onChange={(e) => {
field.onChange(e);
handleTypeChange(e.target.value);
}}
>
<MenuItem value="CLIENT">Client</MenuItem>
<MenuItem value="FOURNISSEUR">Fournisseur</MenuItem>
</Select>
)}
/>
{/* Affichage conditionnel basé sur le type */}
{watchedType === 'CLIENT' && (
<Controller
name="montant"
control={control}
render={({ field }) => (
<TextField {...field} label="Montant" type="number" />
)}
/>
)}
</form>
);
};
Reset et valeurs par défaut
const MyForm: React.FC<{ initialData?: Partial<FormData> }> = ({ initialData }) => {
const { control, reset, formState: { isDirty } } = useForm<FormData>({
defaultValues: {
nom: '',
email: '',
age: 0,
...initialData,
},
});
// Réinitialiser avec de nouvelles valeurs
const handleReset = () => {
reset({
nom: '',
email: '',
age: 0,
});
};
// Réinitialiser avec les valeurs initiales
const handleResetToInitial = () => {
reset(initialData);
};
return (
<form>
{/* Champs du formulaire */}
<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
<Button
type="button"
variant="outlined"
onClick={handleReset}
disabled={!isDirty}
>
Réinitialiser
</Button>
<Button
type="button"
variant="outlined"
onClick={handleResetToInitial}
disabled={!isDirty}
>
Annuler les modifications
</Button>
</Box>
</form>
);
};
🔄 Gestion des erreurs
Affichage des erreurs
const FormErrorDisplay: React.FC<{ errors: any }> = ({ errors }) => {
if (!errors || Object.keys(errors).length === 0) {
return null;
}
return (
<Alert severity="error" sx={{ mb: 2 }}>
<AlertTitle>Erreurs de validation</AlertTitle>
<ul style={{ margin: 0, paddingLeft: 20 }}>
{Object.entries(errors).map(([field, error]: [string, any]) => (
<li key={field}>
<strong>{field}:</strong> {error.message}
</li>
))}
</ul>
</Alert>
);
};
// Utilisation dans le formulaire
const MyForm: React.FC = () => {
const { control, formState: { errors } } = useForm<FormData>();
return (
<form>
<FormErrorDisplay errors={errors} />
{/* Champs du formulaire */}
</form>
);
};
Gestion des erreurs serveur
const MyForm: React.FC = () => {
const { control, setError, clearErrors } = useForm<FormData>();
const [serverError, setServerError] = useState<string | null>(null);
const onSubmit = async (data: FormData) => {
try {
setServerError(null);
clearErrors();
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json();
// Erreurs de validation serveur
if (errorData.errors) {
Object.entries(errorData.errors).forEach(([field, message]) => {
setError(field as keyof FormData, {
type: 'server',
message: message as string,
});
});
} else {
setServerError(errorData.message || 'Une erreur est survenue');
}
return;
}
// Succès
console.log('Formulaire soumis avec succès');
} catch (error) {
setServerError('Erreur de connexion');
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{serverError && (
<Alert severity="error" sx={{ mb: 2 }}>
{serverError}
</Alert>
)}
{/* Champs du formulaire */}
</form>
);
};
🧪 Tests avec React Hook Form
Tests unitaires
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyForm } from './MyForm';
describe('MyForm', () => {
it('should render form fields', () => {
render(<MyForm />);
expect(screen.getByLabelText(/nom/i)).toBeInTheDocument();
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
});
it('should validate required fields', async () => {
const user = userEvent.setup();
render(<MyForm />);
const submitButton = screen.getByRole('button', { name: /soumettre/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText('Le nom est obligatoire')).toBeInTheDocument();
});
});
it('should submit form with valid data', async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<MyForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/nom/i), 'John Doe');
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
await user.click(screen.getByRole('button', { name: /soumettre/i }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
nom: 'John Doe',
email: 'john@example.com',
age: 0,
});
});
});
});
Tests d'intégration
describe('Form Integration', () => {
it('should handle form reset', async () => {
const user = userEvent.setup();
render(<MyForm />);
await user.type(screen.getByLabelText(/nom/i), 'Test User');
await user.click(screen.getByRole('button', { name: /réinitialiser/i }));
expect(screen.getByLabelText(/nom/i)).toHaveValue('');
});
it('should handle conditional fields', async () => {
const user = userEvent.setup();
render(<MyForm />);
// Le champ montant ne devrait pas être visible initialement
expect(screen.queryByLabelText(/montant/i)).not.toBeInTheDocument();
// Sélectionner le type "PAYANT"
await user.click(screen.getByLabelText(/type/i));
await user.click(screen.getByText('PAYANT'));
// Le champ montant devrait maintenant être visible
expect(screen.getByLabelText(/montant/i)).toBeInTheDocument();
});
});
📚 Bonnes pratiques
Performance
- Mode de validation : Utilisez
mode: 'onChange'pour une validation en temps réel - Memoization : Utilisez React.memo pour les composants de formulaire
- Controller : Utilisez Controller pour les champs personnalisés
Accessibilité
- Labels : Assurez-vous que tous les champs ont des labels appropriés
- Messages d'erreur : Fournissez des messages d'erreur clairs
- Navigation clavier : Testez la navigation au clavier
Maintenance
- Types TypeScript : Définissez des interfaces claires pour les données
- Validation centralisée : Utilisez Yup pour centraliser la validation
- Composants réutilisables : Créez des composants de champ réutilisables
Dernière mise à jour : Décembre 2024