Cours Next.js · Module 6: Formulaires et Server Actions

Créer des formulaires en React et Next.js

16 min de lecture
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 :

  1. Chaque champ du formulaire est connecté à une variable d'état correspondante
  2. L'événement onChange met à jour l'état de chaque champ
  3. L'événement onSubmit du formulaire gère l'envoi des données
  4. 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 :

  1. Une seule fonction handleChange gère tous les champs
  2. Mises à jour dynamiques de l'état grâce à la syntaxe [name]: value
  3. Le typage TypeScript assure la sûreté des types
  4. 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 :

  1. Architecture simplifiée - pas besoin de créer des API Routes séparées
  2. Amélioration progressive - les formulaires fonctionnent même sans JavaScript
  3. Traitement sécurisé des données - la logique de vérification et de traitement s'exécute sur le serveur
  4. 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 :

  1. useFormState - pour gérer l'état du formulaire entre le client et le serveur
  2. useFormStatus - pour suivre le statut d'envoi du formulaire (pending)
  3. Zod - pour valider les données du formulaire sur le serveur
  4. 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 :

  1. Performances - re-rend les composants au minimum
  2. Intégration facile - fonctionne avec des éléments de formulaire HTML non modifiés
  3. Validation avancée - intégration native avec Zod, Yup et d'autres bibliothèques
  4. 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 :

  1. API déclarative - composants Form, Field, ErrorMessage
  2. Bonne documentation - documentation et exemples fournis
  3. Maturité - stable et éprouvée en production
  4. 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 :

  1. Formulaires React natifs - composants contrôlés avec état local
  2. Server Actions en Next.js - une nouvelle approche côté serveur
  3. React Hook Form - une bibliothèque efficace avec un minimum de rendus
  4. 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. 1. En React, un élément <input> dans un formulaire est contrôlé lorsque :

Articles utiles