Cours Next.js · Module 6: Formulaires et Server Actions
Créer des formulaires en React et Next.js
Dans cette leçon4
Les formulaires sont un élément fondamental des applications web interactives. Ils permettent aux utilisateurs de saisir des données, qui peuvent ensuite être traitées par l'application. Dans l'environnement React et Next.js, la création de formulaires peut se faire de diverses manières - des solutions natives simples aux bibliothèques avancées. Dans ce module, nous examinerons différentes approches d'implémentation des formulaires.
Formulaires de base en React
Dans sa forme la plus basique, un formulaire en React ressemble à un formulaire HTML traditionnel, mais avec une couche supplémentaire de gestion d'état.
Composants de formulaire contrôlés
L'approche la plus couramment utilisée en React est celle des « composants contrôlés », où la valeur de chaque élément du formulaire est conservée dans l'état React :
1import { useState, FormEvent } from 'react';
2
3export default function SimpleForm() {
4 const [name, setName] = useState('');
5 const [email, setEmail] = useState('');
6 const [message, setMessage] = useState('');
7
8 function handleSubmit(event: FormEvent<HTMLFormElement>) {
9 event.preventDefault();
10 console.log('Form data:', { name, email, message });
11
12 // Here you can send data to the API
13 // fetch('/api/contact', {
14 // method: 'POST',
15 // body: JSON.stringify({ name, email, message }),
16 // headers: { 'Content-Type': 'application/json' }
17 // })
18
19 // Facultatif : réinitialiser le formulaire
20 setName('');
21 setEmail('');
22 setMessage('');
23 }
24
25 return (
26 <form onSubmit={handleSubmit} className="space-y-4">
27 <div>
28 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
29 Nom complet
30 </label>
31 <input
32 type="text"
33 id="name"
34 value={name}
35 onChange={(e) => setName(e.target.value)}
36 className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
37 required
38 />
39 </div>
40
41 <div>
42 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
43 Email
44 </label>
45 <input
46 type="email"
47 id="email"
48 value={email}
49 onChange={(e) => setEmail(e.target.value)}
50 className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
51 required
52 />
53 </div>
54
55 <div>
56 <label htmlFor="message" className="block text-sm font-medium text-gray-700">
57 Message
58 </label>
59 <textarea
60 id="message"
61 value={message}
62 onChange={(e) => setMessage(e.target.value)}
63 rows={4}
64 className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
65 required
66 />
67 </div>
68
69 <button
70 type="submit"
71 className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
72 >
73 Envoyer
74 </button>
75 </form>
76 );
77}Dans l'exemple ci-dessus :
- Chaque champ du formulaire est connecté à une variable d'état correspondante
- L'événement
onChangemet à jour l'état de chaque champ - L'événement
onSubmitdu formulaire gère l'envoi des données - L'utilisation de
event.preventDefault()empêche le rechargement par défaut de la page
Gérer plusieurs champs de formulaire
Lorsqu'un formulaire comporte de nombreux champs, maintenir un état séparé pour chacun peut devenir fastidieux. Une approche plus évolutive consiste à utiliser un seul objet d'état :
1import { useState, ChangeEvent, FormEvent } from 'react';
2
3interface FormData {
4 name: string;
5 email: string;
6 subject: string;
7 message: string;
8 agreeToTerms: boolean;
9}
10
11export default function ContactForm() {
12 const [formData, setFormData] = useState<FormData>({
13 name: '',
14 email: '',
15 subject: '',
16 message: '',
17 agreeToTerms: false
18 });
19
20 const [errors, setErrors] = useState<Partial<FormData>>({});
21
22 function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) {
23 const { name, value, type } = e.target;
24 const checked = (e.target as HTMLInputElement).checked;
25
26 setFormData(prev => ({
27 ...prev,
28 [name]: type === 'checkbox' ? checked : value
29 }));
30
31 // Réinitialiser l'erreur pendant l'édition
32 if (errors[name as keyof FormData]) {
33 setErrors(prev => ({ ...prev, [name]: undefined }));
34 }
35 }
36
37 function validateForm(): boolean {
38 const newErrors: Partial<FormData> = {};
39 let isValid = true;
40
41 if (!formData.name.trim()) {
42 newErrors.name = 'Le nom est requis';
43 isValid = false;
44 }
45
46 if (!formData.email.trim()) {
47 newErrors.email = "L'email est requis";
48 isValid = false;
49 } else if (!/^\S+@\S+\.\S+$/.test(formData.email)) {
50 newErrors.email = "L'email est invalide";
51 isValid = false;
52 }
53
54 if (!formData.message.trim()) {
55 newErrors.message = 'Le message est requis';
56 isValid = false;
57 }
58
59 if (!formData.agreeToTerms) {
60 newErrors.agreeToTerms = 'Tu dois accepter les conditions';
61 isValid = false;
62 }
63
64 setErrors(newErrors);
65 return isValid;
66 }
67
68 function handleSubmit(e: FormEvent<HTMLFormElement>) {
69 e.preventDefault();
70
71 if (validateForm()) {
72 console.log('Envoi du formulaire :', formData);
73 // Envoyer à l'API...
74
75 // Réinitialiser le formulaire après un envoi réussi
76 setFormData({
77 name: '',
78 email: '',
79 subject: '',
80 message: '',
81 agreeToTerms: false
82 });
83 }
84 }
85
86 return (
87 <form onSubmit={handleSubmit} className="space-y-4">
88 <div>
89 <label htmlFor="name" className="block text-sm font-medium">
90 Nom complet *
91 </label>
92 <input
93 type="text"
94 id="name"
95 name="name"
96 value={formData.name}
97 onChange={handleChange}
98 className={`mt-1 block w-full rounded-md ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
99 />
100 {errors.name && <p className="text-red-500 text-xs mt-1">{errors.name}</p>}
101 </div>
102
103 <div>
104 <label htmlFor="email" className="block text-sm font-medium">
105 Email *
106 </label>
107 <input
108 type="email"
109 id="email"
110 name="email"
111 value={formData.email}
112 onChange={handleChange}
113 className={`mt-1 block w-full rounded-md ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
114 />
115 {errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
116 </div>
117
118 <div>
119 <label htmlFor="subject" className="block text-sm font-medium">
120 Sujet
121 </label>
122 <select
123 id="subject"
124 name="subject"
125 value={formData.subject}
126 onChange={handleChange}
127 className="mt-1 block w-full rounded-md border-gray-300"
128 >
129 <option value="">Sélectionne un sujet</option>
130 <option value="general">Demande générale</option>
131 <option value="support">Support technique</option>
132 <option value="feedback">Retour</option>
133 </select>
134 </div>
135
136 <div>
137 <label htmlFor="message" className="block text-sm font-medium">
138 Message *
139 </label>
140 <textarea
141 id="message"
142 name="message"
143 value={formData.message}
144 onChange={handleChange}
145 rows={4}
146 className={`mt-1 block w-full rounded-md ${errors.message ? 'border-red-500' : 'border-gray-300'}`}
147 />
148 {errors.message && <p className="text-red-500 text-xs mt-1">{errors.message}</p>}
149 </div>
150
151 <div className="flex items-center">
152 <input
153 type="checkbox"
154 id="agreeToTerms"
155 name="agreeToTerms"
156 checked={formData.agreeToTerms}
157 onChange={handleChange}
158 className={`h-4 w-4 ${errors.agreeToTerms ? 'border-red-500' : 'border-gray-300'}`}
159 />
160 <label htmlFor="agreeToTerms" className="ml-2 block text-sm">
161 J'accepte les conditions et la politique de confidentialité *
162 </label>
163 {errors.agreeToTerms && (
164 <p className="text-red-500 text-xs ml-2">{errors.agreeToTerms}</p>
165 )}
166 </div>
167
168 <button
169 type="submit"
170 className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
171 >
172 Envoyer le message
173 </button>
174 </form>
175 );
176}Cette approche offre les avantages suivants :
- Une seule fonction handleChange gère tous les champs
- Mises à jour dynamiques de l'état grâce à la syntaxe
[name]: value - Le typage TypeScript assure la sûreté des types
- La validation avec messages d'erreur améliore l'expérience utilisateur
Formulaires en Next.js avec les Server Actions
Next.js 13+ a introduit les Server Actions, qui permettent de traiter les formulaires directement sur le serveur sans avoir à créer de points d'entrée API séparés.
Formulaire de base avec une Server Action
1// app/signup/page.tsx
2'use client';
3
4import { useState } from 'react';
5import { useRouter } from 'next/navigation';
6import { registerUser } from './actions';
7
8export default function SignupPage() {
9 const router = useRouter();
10 const [error, setError] = useState<string>('');
11 const [loading, setLoading] = useState(false);
12
13 async function handleSubmit(formData: FormData) {
14 setLoading(true);
15 setError('');
16
17 try {
18 // Appeler la Server Action
19 const result = await registerUser(formData);
20
21 if (result.success) {
22 // Rediriger après une inscription réussie
23 router.push('/signup/confirm');
24 } else {
25 // Afficher l'erreur
26 setError(result.error || 'Quelque chose a mal tourné');
27 }
28 } catch (e) {
29 setError("Une erreur inattendue s'est produite.");
30 } finally {
31 setLoading(false);
32 }
33 }
34
35 return (
36 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
37 <h1 className="text-2xl font-bold mb-6">Inscription</h1>
38
39 {error && (
40 <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
41 {error}
42 </div>
43 )}
44
45 <form action={handleSubmit}>
46 <div className="mb-4">
47 <label htmlFor="name" className="block text-sm font-medium mb-1">
48 Full name
49 </label>
50 <input
51 type="text"
52 id="name"
53 name="name"
54 required
55 className="w-full px-3 py-2 border border-gray-300 rounded-md"
56 />
57 </div>
58
59 <div className="mb-4">
60 <label htmlFor="email" className="block text-sm font-medium mb-1">
61 Email
62 </label>
63 <input
64 type="email"
65 id="email"
66 name="email"
67 required
68 className="w-full px-3 py-2 border border-gray-300 rounded-md"
69 />
70 </div>
71
72 <div className="mb-4">
73 <label htmlFor="password" className="block text-sm font-medium mb-1">
74 Mot de passe
75 </label>
76 <input
77 type="password"
78 id="password"
79 name="password"
80 required
81 minLength={8}
82 className="w-full px-3 py-2 border border-gray-300 rounded-md"
83 />
84 </div>
85
86 <button
87 type="submit"
88 disabled={loading}
89 className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
90 >
91 {loading ? 'Inscription...' : "S'inscrire"}
92 </button>
93 </form>
94 </div>
95 );
96}Et voici le fichier de la Server Action :
1// app/signup/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6// Schéma de validation du formulaire
7const UserSchema = z.object({
8 name: z.string().min(2, { message: 'Le nom est trop court' }),
9 email: z.string().email({ message: 'Adresse email invalide' }),
10 password: z.string().min(8, { message: 'Le mot de passe doit contenir au moins 8 caractères' })
11});
12
13type SignupResult =
14 | { success: true; userId: string }
15 | { success: false; error: string };
16
17export async function registerUser(formData: FormData): Promise<SignupResult> {
18 // 1. Valider les données du formulaire
19 const validatedFields = UserSchema.safeParse({
20 name: formData.get('name'),
21 email: formData.get('email'),
22 password: formData.get('password')
23 });
24
25 if (!validatedFields.success) {
26 return {
27 success: false,
28 error: validatedFields.error.errors[0]?.message || 'Données de formulaire invalides'
29 };
30 }
31
32 const { name, email, password } = validatedFields.data;
33
34 try {
35 // 2. Vérifier si l'utilisateur existe déjà
36 const existingUser = false; // Ici devrait se trouver la vraie logique de vérification en base de données
37
38 if (existingUser) {
39 return {
40 success: false,
41 error: 'Un utilisateur avec cette adresse email existe déjà'
42 };
43 }
44
45 // 3. Créer un nouvel utilisateur (dans une vraie application - sauvegarder en base de données)
46 // const newUser = await db.user.create({ data: { name, email, password: hashedPassword } });
47
48 // 4. Opérations supplémentaires - envoi d'email de bienvenue, connexion, etc.
49 // await sendWelcomeEmail(email, name);
50
51 return {
52 success: true,
53 userId: '123' // Dans une vraie application, l'ID de l'utilisateur créé
54 };
55 } catch (error) {
56 console.error("Erreur d'inscription :", error);
57 return {
58 success: false,
59 error: "Une erreur s'est produite lors de l'inscription. Réessaie plus tard."
60 };
61 }
62}Avantages des Server Actions :
- Architecture simplifiée - pas besoin de créer des API Routes séparées
- Amélioration progressive - les formulaires fonctionnent même sans JavaScript
- Traitement sécurisé des données - la logique de vérification et de traitement s'exécute sur le serveur
- Typage de bout en bout - TypeScript peut assurer le typage du formulaire jusqu'à la base de données
Formulaires avec états de chargement et validation
Nous pouvons combiner les Server Actions avec les Hooks React pour offrir une expérience utilisateur encore meilleure :
1// app/contact/page.tsx
2'use client';
3
4import { useFormState } from 'react-dom';
5import { useFormStatus } from 'react-dom';
6import { submitContactForm } from './actions';
7
8// Composant bouton avec gestion de l'état de chargement
9function SubmitButton() {
10 const { pending } = useFormStatus();
11
12 return (
13 <button
14 type="submit"
15 disabled={pending}
16 className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
17 >
18 {pending ? 'Envoi...' : 'Envoyer le message'}
19 </button>
20 );
21}
22
23export default function ContactPage() {
24 // État initial du formulaire
25 const initialState = {
26 message: '',
27 errors: {},
28 success: false
29 };
30
31 // Utilisation du hook useFormState pour gérer l'état du formulaire
32 const [state, formAction] = useFormState(submitContactForm, initialState);
33
34 return (
35 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
36 <h1 className="text-2xl font-bold mb-6">Formulaire de contact</h1>
37
38 {state.success ? (
39 <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
40 Merci pour ton message ! Nous te répondrons dès que possible.
41 </div>
42 ) : (
43 <form action={formAction} className="space-y-4">
44 {state.message && (
45 <div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded">
46 {state.message}
47 </div>
48 )}
49
50 <div>
51 <label htmlFor="name" className="block text-sm font-medium mb-1">
52 Nom complet *
53 </label>
54 <input
55 type="text"
56 id="name"
57 name="name"
58 className={`w-full px-3 py-2 border rounded-md ${
59 state.errors?.name ? 'border-red-500' : 'border-gray-300'
60 }`}
61 />
62 {state.errors?.name && (
63 <p className="text-red-500 text-xs mt-1">{state.errors.name}</p>
64 )}
65 </div>
66
67 <div>
68 <label htmlFor="email" className="block text-sm font-medium mb-1">
69 Email *
70 </label>
71 <input
72 type="email"
73 id="email"
74 name="email"
75 className={`w-full px-3 py-2 border rounded-md ${
76 state.errors?.email ? 'border-red-500' : 'border-gray-300'
77 }`}
78 />
79 {state.errors?.email && (
80 <p className="text-red-500 text-xs mt-1">{state.errors.email}</p>
81 )}
82 </div>
83
84 <div>
85 <label htmlFor="message" className="block text-sm font-medium mb-1">
86 Message *
87 </label>
88 <textarea
89 id="message"
90 name="message"
91 rows={4}
92 className={`w-full px-3 py-2 border rounded-md ${
93 state.errors?.messageContent ? 'border-red-500' : 'border-gray-300'
94 }`}
95 />
96 {state.errors?.messageContent && (
97 <p className="text-red-500 text-xs mt-1">{state.errors.messageContent}</p>
98 )}
99 </div>
100
101 <SubmitButton />
102 </form>
103 )}
104 </div>
105 );
106}Et le fichier d'actions associé :
1// app/contact/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6// Schéma de validation
7const ContactSchema = z.object({
8 name: z.string().min(1, 'Le nom est requis'),
9 email: z.string().email('Adresse email invalide'),
10 messageContent: z.string().min(10, 'Le message doit contenir au moins 10 caractères')
11});
12
13// Type de l'état du formulaire
14type FormState = {
15 message: string;
16 errors?: Record<string, string>;
17 success: boolean;
18};
19
20export async function submitContactForm(prevState: FormState, formData: FormData): Promise<FormState> {
21 // Préparer les données pour la validation
22 const validationData = {
23 name: formData.get('name'),
24 email: formData.get('email'),
25 messageContent: formData.get('message')
26 };
27
28 // Validation
29 const result = ContactSchema.safeParse(validationData);
30
31 if (!result.success) {
32 // Préparer la structure des erreurs
33 const errors: Record<string, string> = {};
34 result.error.errors.forEach(error => {
35 if (error.path[0]) {
36 errors[error.path[0].toString()] = error.message;
37 }
38 });
39
40 return {
41 message: 'Le formulaire contient des erreurs. Corrige-les et réessaie.',
42 errors,
43 success: false
44 };
45 }
46
47 try {
48 // Simuler un délai réseau
49 await new Promise(resolve => setTimeout(resolve, 1000));
50
51 // Ici se trouverait le code pour envoyer le message, par ex. sauvegarder en base de données ou envoyer un email
52 // await sendContactEmail(result.data);
53
54 // Renvoyer l'information de succès
55 return {
56 message: '',
57 success: true
58 };
59 } catch (error) {
60 return {
61 message: "Une erreur s'est produite lors de l'envoi du message. Réessaie plus tard.",
62 success: false
63 };
64 }
65}Dans cet exemple, nous utilisons :
- useFormState - pour gérer l'état du formulaire entre le client et le serveur
- useFormStatus - pour suivre le statut d'envoi du formulaire (pending)
- Zod - pour valider les données du formulaire sur le serveur
- Composants fonctionnels - pour organiser la logique de l'interface
Bibliothèques de formulaires populaires en React
Au-delà de l'approche native, il existe plusieurs excellentes bibliothèques qui simplifient le travail avec les formulaires en React et Next.js.
React Hook Form
React Hook Form est l'une des bibliothèques de gestion de formulaires les plus populaires en React ; elle minimise les rendus et optimise les performances.
1import { useForm, SubmitHandler } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4
5// Définition du schéma de validation
6const loginFormSchema = z.object({
7 email: z.string().email('Adresse email invalide'),
8 password: z.string().min(6, 'Password must be at least 6 characters')
9});
10
11// Type de données du formulaire inféré depuis le schéma Zod
12type LoginFormInputs = z.infer<typeof loginFormSchema>;
13
14export default function LoginForm() {
15 const {
16 register,
17 handleSubmit,
18 formState: { errors, isSubmitting }
19 } = useForm<LoginFormInputs>({
20 resolver: zodResolver(loginFormSchema),
21 defaultValues: {
22 email: '',
23 password: ''
24 }
25 });
26
27 const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
28 try {
29 // Simuler un délai réseau
30 await new Promise(resolve => setTimeout(resolve, 1000));
31
32 console.log('Login data:', data);
33 // Dans une vraie application : appeler l'API de connexion
34 // await signIn('credentials', { email: data.email, password: data.password });
35 } catch (error) {
36 console.error('Erreur de connexion :', error);
37 }
38 };
39
40 return (
41 <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
42 <div>
43 <label htmlFor="email" className="block text-sm font-medium">
44 Email
45 </label>
46 <input
47 id="email"
48 type="email"
49 className={`mt-1 block w-full rounded-md ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
50 {...register('email')}
51 />
52 {errors.email && (
53 <p className="text-red-500 text-xs mt-1">{errors.email.message}</p>
54 )}
55 </div>
56
57 <div>
58 <label htmlFor="password" className="block text-sm font-medium">
59 Mot de passe
60 </label>
61 <input
62 id="password"
63 type="password"
64 className={`mt-1 block w-full rounded-md ${errors.password ? 'border-red-500' : 'border-gray-300'}`}
65 {...register('password')}
66 />
67 {errors.password && (
68 <p className="text-red-500 text-xs mt-1">{errors.password.message}</p>
69 )}
70 </div>
71
72 <button
73 type="submit"
74 disabled={isSubmitting}
75 className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
76 >
77 {isSubmitting ? 'Connexion...' : 'Se connecter'}
78 </button>
79 </form>
80 );
81}Avantages de React Hook Form :
- Performances - re-rend les composants au minimum
- Intégration facile - fonctionne avec des éléments de formulaire HTML non modifiés
- Validation avancée - intégration native avec Zod, Yup et d'autres bibliothèques
- Formes de validation non conventionnelles - validation dépendant d'autres champs, validation conditionnelle, etc.
Formik
Formik est une autre bibliothèque de formulaires populaire en React qui propose une approche plus déclarative :
1import { Formik, Form, Field, ErrorMessage } from 'formik';
2import * as Yup from 'yup';
3
4// Schéma de validation Yup
5const CheckoutSchema = Yup.object().shape({
6 firstName: Yup.string().required('Le prénom est requis'),
7 lastName: Yup.string().required('Le nom de famille est requis'),
8 email: Yup.string().email('Email invalide').required("L'email est requis"),
9 address: Yup.string().required("L'adresse est requise"),
10 city: Yup.string().required('La ville est requise'),
11 postalCode: Yup.string()
12 .matches(/^\d{2}-\d{3}$/, 'Code postal invalide (par ex. 00-000)')
13 .required('Le code postal est requis'),
14 paymentMethod: Yup.string().required('Sélectionne un mode de paiement')
15});
16
17export default function CheckoutForm() {
18 return (
19 <div className="max-w-md mx-auto mt-10">
20 <h1 className="text-2xl font-bold mb-6">Validation de la commande</h1>
21
22 <Formik
23 initialValues={{
24 firstName: '',
25 lastName: '',
26 email: '',
27 address: '',
28 city: '',
29 postalCode: '',
30 paymentMethod: ''
31 }}
32 validationSchema={CheckoutSchema}
33 onSubmit={async (values, { setSubmitting, resetForm }) => {
34 try {
35 // Simuler un délai
36 await new Promise(resolve => setTimeout(resolve, 1000));
37 console.log('Données de commande :', values);
38
39 // Dans une vraie application : appeler l'API
40 // await processCheckout(values);
41
42 resetForm();
43 alert('Commande acceptée !');
44 } catch (error) {
45 console.error('Erreur de commande :', error);
46 alert("Une erreur s'est produite lors de la commande");
47 } finally {
48 setSubmitting(false);
49 }
50 }}
51 >
52 {({ isSubmitting }) => (
53 <Form className="space-y-4">
54 <div className="grid grid-cols-2 gap-4">
55 <div>
56 <label htmlFor="firstName" className="block text-sm font-medium">
57 Prénom
58 </label>
59 <Field
60 id="firstName"
61 name="firstName"
62 type="text"
63 className="mt-1 block w-full rounded-md border-gray-300"
64 />
65 <ErrorMessage name="firstName" component="div" className="text-red-500 text-xs mt-1" />
66 </div>
67
68 <div>
69 <label htmlFor="lastName" className="block text-sm font-medium">
70 Nom de famille
71 </label>
72 <Field
73 id="lastName"
74 name="lastName"
75 type="text"
76 className="mt-1 block w-full rounded-md border-gray-300"
77 />
78 <ErrorMessage name="lastName" component="div" className="text-red-500 text-xs mt-1" />
79 </div>
80 </div>
81
82 <div>
83 <label htmlFor="email" className="block text-sm font-medium">
84 Email
85 </label>
86 <Field
87 id="email"
88 name="email"
89 type="email"
90 className="mt-1 block w-full rounded-md border-gray-300"
91 />
92 <ErrorMessage name="email" component="div" className="text-red-500 text-xs mt-1" />
93 </div>
94
95 <div>
96 <label htmlFor="address" className="block text-sm font-medium">
97 Adresse
98 </label>
99 <Field
100 id="address"
101 name="address"
102 type="text"
103 className="mt-1 block w-full rounded-md border-gray-300"
104 />
105 <ErrorMessage name="address" component="div" className="text-red-500 text-xs mt-1" />
106 </div>
107
108 <div className="grid grid-cols-2 gap-4">
109 <div>
110 <label htmlFor="city" className="block text-sm font-medium">
111 Ville
112 </label>
113 <Field
114 id="city"
115 name="city"
116 type="text"
117 className="mt-1 block w-full rounded-md border-gray-300"
118 />
119 <ErrorMessage name="city" component="div" className="text-red-500 text-xs mt-1" />
120 </div>
121
122 <div>
123 <label htmlFor="postalCode" className="block text-sm font-medium">
124 Code postal
125 </label>
126 <Field
127 id="postalCode"
128 name="postalCode"
129 type="text"
130 className="mt-1 block w-full rounded-md border-gray-300"
131 />
132 <ErrorMessage name="postalCode" component="div" className="text-red-500 text-xs mt-1" />
133 </div>
134 </div>
135
136 <div>
137 <label className="block text-sm font-medium mb-2">
138 Mode de paiement
139 </label>
140
141 <div className="space-y-2">
142 <label className="flex items-center">
143 <Field type="radio" name="paymentMethod" value="card" className="mr-2" />
144 Carte de crédit
145 </label>
146
147 <label className="flex items-center">
148 <Field type="radio" name="paymentMethod" value="blik" className="mr-2" />
149 BLIK
150 </label>
151
152 <label className="flex items-center">
153 <Field type="radio" name="paymentMethod" value="transfer" className="mr-2" />
154 Virement bancaire
155 </label>
156 </div>
157
158 <ErrorMessage name="paymentMethod" component="div" className="text-red-500 text-xs mt-1" />
159 </div>
160
161 <button
162 type="submit"
163 disabled={isSubmitting}
164 className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
165 >
166 {isSubmitting ? 'Traitement...' : 'Passer la commande'}
167 </button>
168 </Form>
169 )}
170 </Formik>
171 </div>
172 );
173}Avantages de Formik :
- API déclarative - composants
Form,Field,ErrorMessage - Bonne documentation - documentation et exemples fournis
- Maturité - stable et éprouvée en production
- Intégration avec Yup - validation facile avec la bibliothèque Yup
Résumé
Dans ce module, nous avons exploré différentes approches de création de formulaires en React et Next.js :
- Formulaires React natifs - composants contrôlés avec état local
- Server Actions en Next.js - une nouvelle approche côté serveur
- React Hook Form - une bibliothèque efficace avec un minimum de rendus
- Formik - une bibliothèque mature avec une API déclarative
Le choix de la bonne approche dépend des spécificités du projet :
- Pour les formulaires simples : les solutions React natives
- Pour les applications Next.js : les Server Actions avec amélioration progressive
- Pour les formulaires complexes côté client : React Hook Form ou Formik
Quelle que soit l'approche choisie, il est bon de garder à l'esprit quelques bonnes pratiques :
- Validation à plusieurs niveaux - client + serveur
- Accessibilité - labels appropriés, attributs ARIA, support clavier
- Retour d'information - états de chargement, erreurs, confirmations
- Sécurité - protection CSRF, validation des données côté serveur
Dans les prochains modules, nous explorerons des techniques de gestion de formulaires plus avancées, notamment l'envoi de fichiers, les formulaires multi-étapes et les intégrations avec des API externes.
Code de cette leçon : App.tsx
1// Demo : creer des formulaires en React et Next.js
2// Bases des formulaires : composants controles, evenements, submit
3import React, { useState, FormEvent } from 'react';
4
5// En React, les formulaires fonctionnent comme des "composants controles"
6// L'etat React controle les valeurs des champs du formulaire
7// onChange met a jour l'etat -> React re-rend l'input avec la nouvelle valeur
8
9// Modele de base :
10// const [value, setValue] = useState('');
11// <input value={value} onChange={e => setValue(e.target.value)} />
12
13interface ContactData {
14 name: string;
15 email: string;
16 subject: string;
17 message: string;
18 priority: 'low' | 'medium' | 'high';
19}
20
21const initialData: ContactData = {
22 name: '',
23 email: '',
24 subject: '',
25 message: '',
26 priority: 'medium',
27};
28
29function FormInput({ label, id, type = 'text', value, onChange, placeholder }: {
30 label: string; id: string; type?: string;
31 value: string; onChange: (val: string) => void; placeholder?: string;
32}) {
33 return (
34 <div style={{ marginBottom: '16px' }}>
35 <label htmlFor={id} style={{
36 display: 'block', color: '#b0bec5', fontSize: '0.85rem', marginBottom: '6px',
37 }}>{label}</label>
38 <input
39 id={id} type={type} value={value}
40 onChange={e => onChange(e.target.value)}
41 placeholder={placeholder}
42 style={{
43 width: '100%', padding: '12px 16px', boxSizing: 'border-box',
44 background: 'rgba(255,255,255,0.05)',
45 border: '1px solid rgba(100,255,218,0.3)',
46 borderRadius: '8px', color: '#fff', fontSize: '1rem', outline: 'none',
47 }}
48 />
49 </div>
50 );
51}
52
53export default function ContactFormDemo() {
54 const [formData, setFormData] = useState<ContactData>(initialData);
55 const [submitted, setSubmitted] = useState(false);
56 const [submittedData, setSubmittedData] = useState<ContactData | null>(null);
57
58 const updateField = (field: keyof ContactData) => (value: string) => {
59 setFormData(prev => ({ ...prev, [field]: value }));
60 };
61
62 const handleSubmit = (e: FormEvent) => {
63 e.preventDefault(); // Empeche le rechargement par defaut de la page
64 setSubmittedData(formData);
65 setSubmitted(true);
66 };
67
68 const handleReset = () => {
69 setFormData(initialData);
70 setSubmitted(false);
71 setSubmittedData(null);
72 };
73
74 const priorityColors = { low: '#4caf50', medium: '#ff9800', high: '#f44336' };
75
76 return (
77 <div style={{
78 background: '#0f0f23', minHeight: '100vh', padding: '24px',
79 color: '#fff', fontFamily: 'system-ui, sans-serif',
80 display: 'flex', justifyContent: 'center',
81 }}>
82 <div style={{ maxWidth: '500px', width: '100%' }}>
83 <h1 style={{ color: '#64ffda', marginBottom: '4px' }}>Formulaire de contact quantique</h1>
84 <p style={{ color: '#b0bec5', marginBottom: '24px', fontSize: '0.9rem' }}>
85 Composants controles - React gere l'etat du formulaire
86 </p>
87
88 <form onSubmit={handleSubmit}>
89 <FormInput label="Nom" id="name" value={formData.name}
90 onChange={updateField('name')} placeholder="Nom de l'agent..." />
91 <FormInput label="Email" id="email" type="email" value={formData.email}
92 onChange={updateField('email')} placeholder="agent@quantum.io" />
93 <FormInput label="Sujet" id="subject" value={formData.subject}
94 onChange={updateField('subject')} placeholder="Sujet..." />
95
96 <div style={{ marginBottom: '16px' }}>
97 <label style={{ display: 'block', color: '#b0bec5', fontSize: '0.85rem', marginBottom: '8px' }}>
98 Priorite
99 </label>
100 <div style={{ display: 'flex', gap: '8px' }}>
101 {(['low', 'medium', 'high'] as const).map(p => (
102 <button key={p} type="button"
103 onClick={() => updateField('priority')(p)}
104 style={{
105 flex: 1, padding: '10px', borderRadius: '8px',
106 background: formData.priority === p ? `${priorityColors[p]}20` : 'transparent',
107 border: `1px solid ${formData.priority === p ? priorityColors[p] : 'rgba(255,255,255,0.1)'}`,
108 color: formData.priority === p ? priorityColors[p] : '#78909c',
109 cursor: 'pointer', textTransform: 'capitalize',
110 }}
111 >{p}</button>
112 ))}
113 </div>
114 </div>
115
116 <div style={{ marginBottom: '16px' }}>
117 <label htmlFor="message" style={{
118 display: 'block', color: '#b0bec5', fontSize: '0.85rem', marginBottom: '6px',
119 }}>Message</label>
120 <textarea id="message" rows={4} value={formData.message}
121 onChange={e => updateField('message')(e.target.value)}
122 style={{
123 width: '100%', padding: '12px', boxSizing: 'border-box',
124 background: 'rgba(255,255,255,0.05)',
125 border: '1px solid rgba(100,255,218,0.3)',
126 borderRadius: '8px', color: '#fff', fontSize: '1rem',
127 outline: 'none', resize: 'vertical',
128 }}
129 />
130 </div>
131
132 <div style={{ display: 'flex', gap: '10px' }}>
133 <button type="submit" style={{
134 flex: 2, background: '#64ffda', color: '#0f0f23',
135 border: 'none', padding: '14px', borderRadius: '8px',
136 cursor: 'pointer', fontWeight: 600, fontSize: '1rem',
137 }}>Envoyer</button>
138 <button type="button" onClick={handleReset} style={{
139 flex: 1, background: 'transparent', color: '#78909c',
140 border: '1px solid rgba(255,255,255,0.1)', padding: '14px',
141 borderRadius: '8px', cursor: 'pointer',
142 }}>Reinitialiser</button>
143 </div>
144 </form>
145
146 {/* Apercu en direct de l'etat React */}
147 <div style={{
148 marginTop: '24px', background: 'rgba(0,0,0,0.3)',
149 borderRadius: '8px', padding: '16px',
150 }}>
151 <h4 style={{ color: '#78909c', marginBottom: '8px', fontSize: '0.8rem' }}>
152 État React (en direct) :
153 </h4>
154 <pre style={{ margin: 0, color: '#b0bec5', fontSize: '0.75rem', lineHeight: 1.5 }}>
155 {JSON.stringify(formData, null, 2)}
156 </pre>
157 </div>
158
159 {submitted && submittedData && (
160 <div style={{
161 marginTop: '16px', background: 'rgba(76,175,80,0.1)',
162 border: '1px solid rgba(76,175,80,0.3)',
163 borderRadius: '8px', padding: '16px',
164 }}>
165 <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Envoyé !</h4>
166 <p style={{ color: '#b0bec5', fontSize: '0.85rem', margin: 0 }}>
167 De : {submittedData.name} ({submittedData.email})
168 </p>
169 </div>
170 )}
171 </div>
172 </div>
173 );
174}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. En React, un élément <input> dans un formulaire est contrôlé lorsque :