/ Articles
Créer des formulaires modernes et type-safe avec TanStack Form 📝 [Partie 2 - La validation]
Dans une première partie, nous avions créé un formulaire de connexion en React contrôlé par TanStack Form avec deux champs : le nom de l'utilisateur et le mot de passe 👇
import { useForm } from "@tanstack/react-form"
const form = useForm({
defaultValues: {
username: "",
password: "",
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
const LoginForm = () => {
return (
<form onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}>
<form.Field
name="username"
children={({ field }) =>
<input
id={field.id}
type="text"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
}
/>
<form.Field
name="password"
children={({ field }) =>
<input
id={field.id}
type="password"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
}
/>
<button type="submit">Login</button>
</form>
)
}Maintenant que notre formulaire est ébauché, passons à l'étape cruciale de la validation.
Valider ou ne pas valider ?
La validation d'un formulaire peut intervenir à différents moments :
- 👉 Durant la saisie d'un champ (réactivité maximale)
- 👉 À la sortie d'un champ (compromis entre réactivité et autonomie de l'utilisateur)
- 👉 Lorsqu'on presse sur le bouton de soumission du formulaire (autonomie de l'utilisateur maximale mais pas de réactivité)
TanStack Form nous permet de facilement définir le moment le plus opportun pour valider un ou plusieurs champs voire de combiner plusieurs stratégies de validation.
Validation à la volée
La validation à la saisie d'un champ est la plus simple et la plus réactive. Elle permet de tester la validité du champ dès que l'utilisateur tape un caractère.
On peut la configurer au niveau du champ concerné en passant une fonction de validation à la propriété onChange des validators du champ.
Tout ce qui est retourné par cette fonction sera considéré comme une erreur de validation et automatiquement ajouté au tableau field.state.meta.errors. Si cette fonction ne retourne rien (undefined) ou null, le champ est considéré comme valide.
<form.Field
name="username"
validators={{
onChange: ({ value }) => {
if (value.length < 3) {
return "Le nom d'utilisateur doit contenir au moins 3 caractères"
}
}
}}
children={({ field }) =>
<input
id={field.id}
type="text"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
}
/>Validation Ă la sortie
Lorsqu'un champ perd son focus (l'utilisateur clique en dehors de celui-ci ou se déplace sur le prochain élément focusable à l'aide de son clavier), on peut déclencher une validation en passant une fonction à la propriété onBlur des validators du champ.
<form.Field
name="username"
validators={
onBlur: ({ value }) => {
if (value.length < 3) {
return "Le nom d'utilisateur doit contenir au moins 3 caractères"
}
}
}
children={({ field }) =>
<input
id={field.id}
type="text"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
}
/>Validation Ă la soumission
Si on ne souhaite éprouver la validité du formulaire qu'après que l'utilisateur lui-même estime avoir fini de le remplir, on en passera par onSubmit qui sera exécuté à chaque appel de form.handleSubmit.
import { useForm } from "@tanstack/react-form"
const form = useForm({
defaultValues: {
username: "",
password: "",
},
validators: {
onSubmit: ({ value }) => {
if (!value.username.length || !value.password.length) {
return "Le nom d'utilisateur et le mot de passe sont requis"
}
}
},
onSubmit: async ({ value }) => {
console.log(value)
},
})De plus, rien n'empêche de combiner ces différentes stratégies de validation c'est-à -dire d'en valider certains à la volée, d'autres à la sortie, d'autres à la soumission du formulaire etc. — même s'il est préférable de ne pas surcharger l'utilisateur d'informations ou de le frustrer en lui créant trop de crans d'arrêt dans sa validation.
Par exemple on peut mettre en place une validation au changement de focus sur le champ username et une validation globale qui vérifie que les deux champs présentent une valeur non vide à la soumission 👇
import { useForm } from "@tanstack/react-form"
const form = useForm({
defaultValues: {
username: "",
password: "",
},
validators: {
onSubmit: ({ value }) => {
if (!value.username.length || !value.password.length) {
return "Le nom d'utilisateur et le mot de passe sont requis"
}
}
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
const LoginForm = () => {
return (
<form onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}>
<form.Field
name="username"
validators={{
onBlur: ({ value }) => {
if (value.length < 3) {
return "Le nom d'utilisateur doit contenir au moins 3 caractères"
}
},
}}
children={({ field }) =>
<input
id={field.id}
type="text"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
}
/>
{/* ... */}
<button type="submit">Login</button>
</form>
)
}Attendre la validation côté serveur
Jusqu'à présent, nous avons mis en place des validations simples qui se basent uniquement sur la valeur du champ déjà disponible côté client. Mais quid si on souhaite vérifier la validité d'un champ en l'envoyant à une API ? On passe alors à une validation asynchrone plus délicate car elle nécessite d'attendre la réponse de l'API (qui peut être longue ou même ne jamais arriver) pour déterminer si le champ est valide ou non.
C'est le moment de recourir à la propriété onChangeAsync qui sera voisine et complémentaire de onChange (là encore il existe des onBlurAsync et onSubmitAsync qui sont les pendants de onBlur et onSubmit). Elle prendra en valeur une nouvelle fonction chargée d'effectuer les appels API nécessaires pour tester l'input.
function validateUsername(value: string) {
fetch(`/api/validate-username?username=${value}`)
.then(response => response.json())
.then(data => {
if (data.error) {
return "Ce nom d'utilisateur est déjà pris"
}
return null
})
.catch(error => {
return "Une erreur est survenue lors de la validation du nom d'utilisateur"
})
}
<form.Field
name="username"
validators={
onChangeAsync: ({ value }) => validateUsername(value),
onChange: ({ value }) => {
if (value.length < 3) {
return "Le nom d'utilisateur doit contenir au moins 3 caractères"
}
}
}
children={({ field }) =>
<input
id={field.id}
type="text"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
}
/>À noter que la validation onChangeAsync ne se déclenchera qu'une fois les validations onChange réussies. En suivant cet ordre, on évite de solliciter inutilement une API car si l'input est déjà détecté comme invalide avant toute vérification extérieure, il est inutile de l'envoyer à l'API.
Afficher une erreur de validation pour un champ
Pour vérifier que la validation spécifique à un champ fonctionne, on affichera le plus souvent sous notre input les messages d'erreur associés. Il suffit d'inspecter la propriété field.state.meta.errors qui va nous livrer un tableau d'erreurs (tableau vide si le champ est valide).
<form.Field
name="username"
validators={
onBlur: ({ value }) => {
if (value.length < 3) {
return "Le nom d'utilisateur doit contenir au moins 3 caractères"
}
}
}
children={({ field }) =>
<>
<input
id={field.id}
type="text"
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
<div key={error} className="text-red-500">{field.state.meta.errors}</div>
</>
}
/>L'erreur disparaît automatiquement entre le moment où le champ est repris par l'utilisateur et le prochain cycle de validation qui sera déclenché par la saisie d'un nouveau caractère, le changement de focus ou la soumission du formulaire.
Afficher une erreur de validation pour le formulaire
Si on effectue une validation globale, c'est du côté de l'objet form qu'on va récupérer nos erreurs de validation. On serait alors tenté d'écrire ceci :
import { useForm } from "@tanstack/react-form"
const form = useForm({
defaultValues: {
username: "",
password: "",
},
validators: {
onSubmit: ({ value }) => {
if (!value.username.length || !value.password.length) {
return "Le nom d'utilisateur et le mot de passe sont requis"
}
}
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
const LoginForm = () => {
return (
<form {...}>
{/* ... */}
<div>{form.state.errors}</div>
<button type="submit">Login</button>
</form>
)
}Cependant on risque de ne pas afficher l'erreur au moment où elle est créée ! Pourquoi ? Car par souci d'optimisation, TanStack Form ne veut pas forcer React à rendre l'entièreté du composant contenant le formulaire à chaque modification interne du form.
Imaginons que le <form> soit pris dans un composant bien plus large : si le hook useForm qui retourne notre objet form est appelé à la hauteur de ce composant, une modification du formulaire pourrait entraîner le rendu de tous les éléments et composants enfants y compris ceux n'ont rien à voir avec la partie dédiée au formulaire. Il en résulterait une dégradation des performances si ces changements surviennent trop fréquemment. 🤷‍♂️
Pour contourner cette difficulté, TanStack Form désactive le rendu automatique du composant comportant le <form> et propose un composant <form.Subscribe> qui permet de s'abonner à un changement spécifique du form et de rendre uniquement la portion qu'il enveloppe.
const LoginForm = () => {
return (
<form {...}>
<form.Subscribe
selector={(state) => state.errors}
children={({ errors }) =>
errors.length > 0 && <div>{errors}</div>
}/>
<button type="submit">Login</button>
</form>
)
}Dans l'exemple ci-dessus, on utilise le composant <form.Subscribe> avec une propriété selector pour déterminer les informations à surveiller (ici les erreurs globales du formulaire) puis on passe à sa prop children une fonction prenant pour argument ce selector et renvoyant en échange la portion UI qui doit réagir lorsque cette information change.
Voilà ! Nous disposons à présent d'un formulaire opérationnel. Mais on peut encore réhausser l'expérience utilisateur en ajoutant des indicateurs de chargement ou de validation avant l'affichage des erreurs pour rendre l'expérience bien plus fluide et agréable 🌊
Ce point, ainsi que diverses améliorations que l'on peut apporter à notre formulaire grâce à TanStack Form, feront l'objet d'une dernière partie.