frenchdev

Créer des formulaires modernes et type-safe avec TanStack Form [Partie 1 - Introduction]

L'enfer du développeur est pavé de mauvais formulaires

Les formulaires sont un cauchemar incontournable dans le développement web et mobile : validation synchrone et asynchrone, récupération et affichage des erreurs, accessibilité, sérialisation et déserialisation des valeurs, responsivité, sauvegarde de l'état etc.

À ces impondérables s'ajoutent les différentes décisions UX à prendre : faut-il révéler les erreurs à mesure que l'utilisateur tape afin d'être le plus réactif possible au risque de le frustrer s'il n'est pas allé au bout de sa saisie ? Ajouter un délai avant de lancer la validation ? Doit-on plutôt attendre qu'il sorte du champ pour les lui signaler ? Ou une validation globale à la soumission du formulaire quitte à faire apparaître soudainement à l'écran une flopée d'erreurs alors qu'il pense en avoir fini ?

D'autant que pour s'épargner la création de certains composants complexes, il est souvent préférable de s'en remettre à des librairies UI avec derrière tout un travail pour assurer la bonne connexion de ces composants avec notre propre logique de validation et de gestion des erreurs.

Dès qu'on veut adresser toute la complexité inhérente aux formulaires on se retrouve vite avec dans des implémentations verbeuses et peu maintenables en raison de leur forte spécificité : chaque développeur recrée son petit système de gestion de formulaires.

Dès lors, quelles qualités devrait présenter une librairie de formulaires digne de ce nom pour remplacer notre implémentation maison qui se transforme vite en foutoir ?

La bonne nouvelle ? Un sérieux candidat vient de rentrer en lice pour devenir votre prochain couteau suisse dans la gestion de formulaires !

The new form in town

BigFormInTown

Terrain jusqu'à présent dominé par React Hook Form et Formik, une nouvelle librairie Typescript sortie de la galaxie TanStack vient de bousculer ce petit monde : TanStack Form.

Compatible avec tous les frameworks frontend, TanStack Form offre un typage intégral de votre formulaire qui sécurise et accélère le développement.

Porté à l'échelle de plus grosses applications, il offre la possiblité particulièrement intéressante de "composer" ses formulaires à l'aide d'options et de composants réutilisables. On peut ainsi créer notre propre fabrique de formulaires avec des abstractions choisies mais toujours adossée à la librairie de base pour une personnalisation encore plus fine.

Créer son formulaire avec TanStack Form

Trêve de bavardages, passons aux travaux pratiques !

On initialise un formulaire TanStack Form de la façon suivante en React :

1import { useForm } from "@tanstack/react-form"
2
3const form = useForm({
4 defaultValues: {
5 username: "",
6 password: "",
7 },
8 })

C'est à partir de ces defaultValues que TanStack Form va automatiquement inférer les types des champs du formulaire sans nécessiter de définition séparée.

Tout aussi important, une fonction onSubmit qui sera appelée à chaque tentative de validation.

1import { useForm } from "@tanstack/react-form"
2
3const form = useForm({
4 defaultValues: {
5 username: "",
6 password: "",
7 },
8 onSubmit: async ({ value}) => {
9 console.log(value)
10 },
11 })

Le form renvoyé par le hook va ensuite pouvoir être consommé par notre composant front pour bâtir le formulaire côté UI.

Ce form contient à la fois des composants tels que form.Field qui vont venir envelopper nos inputs pour les contrôler mais aussi des informations sur l'état actuel de notre formulaire (est-il valide ? quelles sont les erreurs en cours ? a-t-il été altéré depuis le chargement de la page ? etc.).

Première brique donc à poser : le composant form.Field dont l'attribut name se liera automatiquement à l'un des champs du formulaire déclarés plus haut.

1import { useForm } from "@tanstack/react-form"
2
3const form = useForm({
4 defaultValues: {
5 username: "",
6 password: "",
7 },
8 onSubmit: async ({ value }) => {
9 console.log(value)
10 },
11 })
12
13const LoginForm = () => {
14 return (
15 <form>
16 <form.Field
17 name="username"
18 children={({field}) =>
19 <input
20 id={field.id}
21 type="text"
22 name={field.name}
23 value={field.state.value}
24 />
25 }
26 />
27 <form.Field
28 name="password"
29 children={({field}) =>
30 <input
31 id={field.id}
32 type="password"
33 name={field.name}
34 value={field.state.value}
35 />
36 }
37 />
38 <button type="submit">Login</button>
39 </form>
40 )
41}

Dernière étape pour assurer la validation du formulaire : désactiver le comportement par défaut du bouton de soumission qui survient sur <button type="submit"> car TanStack Form gère ça pour nous.

Cette précaution est cruciale pour empêcher le rechargement de la page, comportement qu'on souhaite éviter dans le cadre d'une SPA car il nous ferait sortir de notre routing et court-circuiterait les opérations de TanStack Form.

On va donc devoir en passer par la méthode form.handleSubmit() pour déclencher la validation du formulaire.

1import { useForm } from "@tanstack/react-form"
2
3const form = useForm({
4 defaultValues: {
5 username: "",
6 password: "",
7 },
8 onSubmit: async ({ value }) => {
9 console.log(value)
10 },
11 })
12
13const LoginForm = () => {
14 return (
15 <form onSubmit={(e) => {
16 e.preventDefault();
17 e.stopPropagation();
18 void form.handleSubmit();
19 }}>
20 <form.Field
21 name="username"
22 children={({field}) =>
23 <input
24 id={field.id}
25 type="text"
26 name={field.name}
27 value={field.state.value}
28 />
29 }
30 />
31 <form.Field
32 name="password"
33 children={({field}) =>
34 <input
35 id={field.id}
36 type="password"
37 name={field.name}
38 value={field.state.value}
39 />
40 }
41 />
42 <button type="submit">Login</button>
43 </form>
44 )
45}

Avec TanStack Form, nos inputs sont contrôlés par le formulaire ce qui signifie que nous devons non seulement leur passer manuellement leur valeur via la prop value={field.state.value} mais aussi des events handlers pour réagir à la saisie de l'utilisateur et mettre à jour cette même valeur.

On utilise ainsi onChange sur la surface de l'input pour appeler field.handleChange() avec la nouvelle valeur du champ.

1import { useForm } from "@tanstack/react-form"
2
3const form = useForm({
4 defaultValues: {
5 username: "",
6 password: "",
7 },
8 onSubmit: async ({ value }) => {
9 console.log(value)
10 },
11 })
12
13const LoginForm = () => {
14 return (
15 <form onSubmit={(e) => {
16 e.preventDefault();
17 e.stopPropagation();
18 void form.handleSubmit();
19 }}>
20 <form.Field
21 name="username"
22 children={({field}) =>
23 <input
24 id={field.id}
25 type="text"
26 name={field.name}
27 value={field.state.value}
28 onChange={(e) => field.handleChange(e.target.value)}
29 />
30 }
31 />
32 <form.Field
33 name="password"
34 children={({field}) =>
35 <input
36 id={field.id}
37 type="password"
38 name={field.name}
39 value={field.state.value}
40 onChange={(e) => field.handleChange(e.target.value)}
41 />
42 }
43 />
44 <button type="submit">Login</button>
45 </form>
46 )
47}

Voilà en deux coups de cuillère à pot un bon début de formulaire de connexion.

Ne reste plus qu'à ajouter la validation des champs et gérer les erreurs, des étapes que nous verrons dans la partie suivante.