Aller au contenu principal

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

  1. Mode de validation : Utilisez mode: 'onChange' pour une validation en temps réel
  2. Memoization : Utilisez React.memo pour les composants de formulaire
  3. Controller : Utilisez Controller pour les champs personnalisés

Accessibilité

  1. Labels : Assurez-vous que tous les champs ont des labels appropriés
  2. Messages d'erreur : Fournissez des messages d'erreur clairs
  3. Navigation clavier : Testez la navigation au clavier

Maintenance

  1. Types TypeScript : Définissez des interfaces claires pour les données
  2. Validation centralisée : Utilisez Yup pour centraliser la validation
  3. Composants réutilisables : Créez des composants de champ réutilisables

Dernière mise à jour : Décembre 2024