Un formulaire de date de naissance validé dans Next.js avec des champs contrôlés et des fonctions de rappel
Créez un petit formulaire client Next.js qui suive les saisies à l’aide de useState, rejette les dates de naissance invalides, affiche des erreurs accessibles et transmet des données propres à son composant parent.
Une application d’horoscope a besoin de deux informations de la part de l’utilisateur pour pouvoir générer une lecture : un nom et une date de naissance. Cela semble correspondre à un formulaire de cinq minutes, mais même cette petite composante impose des décisions de conception importantes : où il doit fonctionner dans une application Next.js, à qui appartiennent les valeurs saisies, comment les dates invalides sont rejetées, et qui décide de ce qui se passe après une soumission réussie.
Cette étude guide vous aidera à construire ce formulaire pas à pas. À la fin, vous disposerez d’une composante de formulaire contrôlée qui valide les entrées, affiche des erreurs de manière accessible et ne transmet que des données propres vers le haut, ainsi que d’un modèle mental clair que vous pourrez réutiliser pour tout formulaire comportant plus d’un champ.
Déterminez les responsabilités de la composante
- Mémoriser ce que l’utilisateur a saisi.
- Vérifier les entrées avant leur soumission.
Tout ce qui n’est pas inclus dans cette liste, comme l’appel à une API ou l’affichage des données, doit être traité ailleurs. Un ensemble restreint d’éléments rend le reste de la conception plus simple.
Pourquoi le formulaire doit être un composant client
Dans App Router, chaque composant est par défaut un composant serveur à moins de choisir autrement. Les composants serveurs s’affichent sur le serveur et ne transmettent pas de JavaScript interactif, ce qui les empêche de conserver un état ou de réagir aux événements. Le formulaire doit donc être déclaré comme composant client.
"use client";
Le composant dépend de plusieurs éléments qui n’existent que dans le navigateur :
useStatepour les valeurs actuelles,- des gestionnaires d’événement
onChangesur les champs d’entrée, - un gestionnaire d’événement
onSubmitsur le formulaire, - une interaction continue avec l’utilisateur,
En bref, le composant ne se contente pas d’afficher des informations ; il doit réagir aux actions de l’utilisateur. C’est le critère qui permet de le qualifier de composant client. Une bonne pratique consiste à garder de tels composants petits et situés aux extrémités de l’arborescence, afin que la directive n’inclue pas de grandes parties de la page dans le bundle client.
Saisir les données et les props
Le formulaire importe un type partagé Profile ainsi que useState.
import { Profile } from "../types";
import { useState } from "react";
Déclarer explicitement la structure des données soumises permet à TypeScript de vérifier chaque endroit où elles sont générées ou consommées, au lieu de laisser un objet arbitraire circuler dans l’application.
export type Profile ={
name: string;
dob: string;
}
Viennent ensuite les props du composant. Il n’y en a qu’un : une fonction de rappel fournie par le parent.
type HoroscopeFormProps = {
onSubmit: (info: Profile) => void;
};
Laisser le parent décider de la suite
C’est ici que se trace la frontière du composant. Le formulaire collecte des données, mais il n’a pas à décider quoi en faire. Selon l’écran affiché, le composant parent peut :
- appeler une API,
- générer l’horoscope,
- conserver le profil,
- afficher le résultat,
- mettre à jour un autre état.
Si l’une de ces actions était codée en dur à l’intérieur du formulaire, celui-ci serait lié à un seul écran. En utilisant plutôt une fonction onSubmit, il reste réutilisable. Ce type indique que onSubmit reçoit un objet Profile et ne renvoie rien (void) ; ainsi, le formulaire déclenche cette fonction puis passe à l’étape suivante. Le flux global est le suivant :
User enters information
↓
HoroscopeForm collects it
↓
HoroscopeForm validates it
↓
onSubmit(user)
↓
Parent decides what happens next
Chaque étape a un seul responsable, et la partie concernant le formulaire s’achève dès qu’il appelle la fonction de rappel.
Stocker les données saisies dans l’état React
Le formulaire a besoin d’un endroit pour conserver les valeurs actuelles. Trois éléments d’état suffisent pour tout gérer.
const [name, setName] = useState<string>("");
const [dob, setDob] = useState<string>("");
const [error, setError] = useState<string>("");
Regardez d’abord le nom.
const [name, setName] = useState<string>("");
useState renvoie une paire. name représente la valeur actuelle, tandis que setName est la fonction à appeler pour la remplacer, ce qui déclenche également un re-render. La valeur initiale est une chaîne vide car rien n’a encore été saisi. Initialiser avec une chaîne plutôt qu’avec undefined est important pour les champs contrôlés : React émet une alerte si un champ passe de mode non contrôlé à mode contrôlé lorsque sa value change de undefined à une chaîne.
La date de naissance suit le même schéma.
const [dob, setDob] = useState<string>("");
Le dernier élément d’état contient le message d’erreur actuel.
const [error, setError] = useState<string>("");
Une chaîne vide signifie qu’il n’y a pas d’erreur pour le moment. La validation écrira un message dans cet état lorsqu’il y a un problème, et le supprimera une fois que la saisie est valide.
Garder la validation des dates dans sa propre fonction
Les règles de validation ont tendance à s’accumuler, donc plutôt que de les regrouper dans le gestionnaire d’envoi, la vérification des dates se trouve dans une fonction dédiée. Sa signature indique les contraintes à respecter.
function validateDOB(dob: string): string | null {
Il y a exactement deux résultats possibles. Une date invalide génère une chaîne expliquant le problème ; une date valide génère null.
Valid date
↓
return null
Invalid date
↓
return error message
Puisque la fonction répond à une seule question, à savoir « cette date de naissance est-elle acceptable ? », elle est facile à lire, facile à tester en unité sans afficher quoi que ce soit, et facile à réutiliser sur un serveur si vous effectuez également des validations là-bas. Retourner un message au lieu de lancer une exception simplifie le code appelant : il suffit de vérifier le résultat et de l’afficher s’il est présent.
Quelles dates rejeter
Deux règles s’imposent pour une date de naissance :
- Aucune date future. Personne ne peut être né un jour qui n’a pas encore eu lieu.
- Une limite inférieure raisonnable. Les dates datant de plus de 150 ans en arrière sont rejetées, car elles sont presque certainement erronées.
Les dates semblent simples tant que l’heure n’est pas prise en compte. Un <input type="date"> produit une chaîne au format YYYY-MM-DD, et new Date("2024-05-01") interprète cette chaîne comme minuit UTC, tandis que « aujourd’hui » généré à l’aide de new Date() inclut les heures, minutes et secondes locales. Selon la zone horaire de l’utilisateur, une comparaison naïve peut accepter à tort demain ou rejeter aujourd’hui. Deux solutions fiables consistent soit à normaliser les deux valeurs au début de la journée avant comparaison, soit à comparer directement les chaînes YYYY-MM-DD, qui s’ordonnent correctement en tant que texte. Quelle que soit l’approche choisie, et que vous ayez eu ou non l’aide d’un assistant IA pour la rédiger, assurez-vous de pouvoir expliquer pourquoi chaque comparaison est nécessaire ; les erreurs liées aux dates se cachent précisément dans les lignes que personne n’a comprises.
Coordonner tout dans le gestionnaire de soumission
Lorsque l’état et la validation sont prêts, handleSubmit les met en relation. Lorsque l’utilisateur soumet le formulaire, cette fonction doit :
- empêcher la soumission par défaut du navigateur, qui rechargerait ou naviguerait vers une autre page,
- vérifier que les deux champs contiennent bien des valeurs,
- valider la date de naissance,
- afficher une erreur en cas de problème,
- sinon transmettre les données au composant parent.
Cette fonction commence ainsi.
const handleSubmit = (e: React.SubmitEvent) => {
e.preventDefault();
Par défaut, la soumission d’un formulaire envoie une requête et recharge la page. Comme c’est React qui gère ici la soumission, ce comportement par défaut doit être annulé.
e.preventDefault();
Désormais, c’est le composant lui-même qui décide de ce que fait l’envoi des données. Une remarque sur le type d’événement : de nombreux projets définissent ce paramètre comme React.FormEvent<HTMLFormElement>. Vérifiez quels types d’événements de soumission sont exposés par la version @types/react que vous avez installée, puis choisissez celui que votre projet utilise systématiquement.
Réfuser les champs vides en retournant précocement
if (!name || !dob) {
setError("Please enter in information");
return;
}
Si l’un des champs est vide, le gestionnaire enregistre une erreur et retourne immédiatement. Il s’agit du schéma de retour précoce (ou clause de protection) : une fois que l’entrée est reconnue comme invalide, il n’y a plus rien à faire, donc la fonction s’arrête au lieu d’envelopper le reste de la logique dans un autre niveau de blocs if. Chaque clause de protection gère une erreur spécifique, tandis que le « chemin normal » reste simple.
Exécuter la vérification de date
Lorsqu’une date est confirmée comme existante, elle passe par le validateur.
const dobError = validateDOB(dob);
Le résultat est soit un message, soit null ; une seule vérification suffit donc.
if (dobError) {
setError(dobError);
return;
}
Un message signifie que le gestionnaire l’affiche puis s’arrête. null signifie que la date est valide et l’exécution se poursuit.
Transmettre des données propres au parent
Arriver à ce stade signifie que toutes les vérifications ont réussi, donc tout erreur ancienne issue d’une tentative précédente a été résolue.
setError("");
Ensuite, la fonction de rappel du parent reçoit le profil validé.
onSubmit({ name, dob });
C’est là le résultat de la décision de conception prise précédemment. Le formulaire ne sait pas ni ne se soucie de ce qui se passe ensuite ; il indique simplement que des données valides sont disponibles, et c’est le parent qui choisit ce qu’il faut faire. Le même composant peut alimenter un générateur d’horoscope aujourd’hui et une page de configuration de profil demain, sans aucune modification.
Relier la logique au markup
L’élément formulaire relie la soumission au gestionnaire correspondant.
<form onSubmit={handleSubmit}>
Cela indique à React d’exécuter handleSubmit chaque fois que le formulaire est soumis, que ce soit en cliquant sur le bouton ou en appuyant sur Entrée dans un champ. Le champ de nom vient ensuite.
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
Comment un input contrôlé reste en synchronisation
Ceci est une entrée contrôlée : c’est l’état de React, et non le DOM, qui constitue la source de vérité pour sa valeur. Chaque fois que l’utilisateur tape, le gestionnaire de modification s’exécute.
onChange={(e) => setName(e.target.value)}
Il lit le nouveau texte provenant de l’événement et le stocke dans l’état. Le cycle complet se présente comme suit :
User types
↓
onChange fires
↓
setName(new value)
↓
name state updates
↓
value={name}
↓
Input displays updated value
Puisque l’entrée affiche toujours la valeur contenue dans name, la valeur que vous validez est systématiquement celle affichée à l’écran. Le champ de date utilise le même principe.
<input
type="date"
value={dob}
onChange={(e) => setDob(e.target.value)}
/>
L’état suit la date choisie, et chaque modification appelle setDob. Une amélioration à considérer est d’assigner l’attribut natif max à la date d’aujourd’hui, ce qui empêche la plupart des outils de sélection de date d’afficher des dates futures, tandis que votre validateur continue de protéger contre les entrées incorrectes et les navigateurs anciens.
Chaque champ d’entrée doit également disposer d’une <label> visible associée. Un espace réservé ou un en-tête voisin ne constitue pas de substitution ; c’est la étiquette que lisent les lecteurs d’écran et qui permet de rendre le champ cliquable grâce à son texte explicatif.
Afficher les erreurs uniquement lorsqu’elles existent
Le message d’erreur ne doit apparaître que s’il y en a un. Le rendu conditionnel s’occupe de cela.
{error && (
<p role="alert">
{error}
</p>
)}
Lorsque error contient du texte, le paragraphe est affiché ; lorsqu’il s’agit d’une chaîne vide, considérée comme fausse, rien n’apparaît. Ce raccourci && est sécurisé ici car la valeur est une chaîne. Avec des nombres, cela peut poser problème : un comptage de 0 serait affiché tel quel comme le texte « 0 ».
Le paragraphe possède également un rôle ARIA.
role="alert"
role="alert" indique aux technologies d’assistance que ce contenu est important et urgent, de sorte que les lecteurs d’écran le annoncent dès son apparition. Il s’agit d’une modification d’un seul attribut qui permet aux personnes ne pouvant pas voir l’apparition du message d’utiliser les retours de validation. Pour plus de clarté, vous pouvez également marquer le champ en cause avec aria-invalid et le relier au message grâce à aria-describedby.
Ajouter le bouton de soumission
Le dernier élément est un bouton déclaré explicitement comme tel.
<button type="submit">
Submit
</button>
Dans un formulaire, un bouton avec type="submit" déclenche la méthode onSubmit du formulaire, et par conséquent handleSubmit. Les boutons à l’intérieur d’un formulaire ont par défaut pour fonction de soumettre, mais spécifier le type évite les surprises lorsque quelqu’un ajoute ultérieurement un deuxième bouton destiné à une autre action, comme vider les champs.
Le flux de données complet
Vu dans son ensemble, le composant fait circuler les données dans une seule direction :
State
↓
User input
↓
Submit
↓
Validation
↓
Parent callback
useStatestocke ce que l’utilisateur a saisi.- Les champs de saisie mettent à jour cet état à chaque modification.
- Le soumissionnement du formulaire exécute
handleSubmit. handleSubmitvalide les valeurs.- Des données invalides déclenchent l’état d’erreur et arrêtent le processus.
- Des données valides sont transmises au composant parent via
onSubmit, qui les reçoit ensuite.
Où aller ensuite
Cette approche manuelle est idéale pour l’apprentissage et tout à fait suffisante pour un formulaire à deux champs. Lorsque les formulaires comptent de nombreux champs ou nécessitent des règles inter-champs ainsi que des vérifications côté serveur, envisagez l’utilisation d’une bibliothèque de schémas afin que les mêmes règles s’appliquent tant du côté client que du serveur ; partager un schéma Zod entre le frontend React et le backend Node est une façon de le faire. La validation côté client améliore l’expérience utilisateur, mais ne remplace jamais la validation côté serveur, car toute requête peut être manipulée manuellement.
Points clés
- Marquez uniquement les composants interactifs avec
"use client"et gardez-les de taille réduite. - Donnez au formulaire une seule fonction : collecter, valider, transmettre les données. Laissez le composant parent gérer les effets secondaires via une fonction de rappel typée.
null ; elles sont faciles à tester et à réutiliser.YYYY-MM-DD pour éviter les erreurs liées aux fuseaux horaires.role="alert" et des étiquettes appropriées pour rendre les erreurs accessibles.