Créer votre propre bibliothèque de validation React : les bases (partie 1)

Publié: 2022-03-10
Résumé rapide ↬ Vous êtes-vous déjà demandé comment fonctionnent les bibliothèques de validation ? Cet article vous expliquera comment créer votre propre bibliothèque de validation pour React étape par étape. La prochaine partie ajoutera des fonctionnalités plus avancées et la dernière partie se concentrera sur l'amélioration de l'expérience du développeur.

J'ai toujours pensé que les bibliothèques de validation de formulaire étaient plutôt cool. Je sais, c'est un intérêt de niche à avoir - mais nous les utilisons tellement ! Au moins dans mon travail - la plupart de ce que je fais consiste à construire des formulaires plus ou moins complexes avec des règles de validation qui dépendent de choix et de chemins antérieurs. Comprendre le fonctionnement d'une bibliothèque de validation de formulaires est primordial.

L'année dernière, j'ai écrit une telle bibliothèque de validation de formulaire. Je l'ai nommé "Calidation", et vous pouvez lire l'article de blog d'introduction ici. C'est une bonne bibliothèque qui offre beaucoup de flexibilité et utilise une approche légèrement différente des autres sur le marché. Il existe également des tonnes d'autres excellentes bibliothèques - la mienne a bien fonctionné pour nos besoins.

Aujourd'hui, je vais vous montrer comment écrire votre propre bibliothèque de validation pour React. Nous allons suivre le processus étape par étape et vous trouverez des exemples de CodeSandbox au fur et à mesure. À la fin de cet article, vous saurez comment écrire votre propre bibliothèque de validation ou, à tout le moins, vous comprendrez mieux comment d'autres bibliothèques implémentent "la magie de la validation".

  • Partie 1 : Les bases
  • Partie 2 : Les fonctionnalités
  • Partie 3 : L'expérience
Plus après saut! Continuez à lire ci-dessous ↓

Étape 1 : Concevoir l'API

La première étape de la création d'une bibliothèque consiste à concevoir comment elle va être utilisée. Cela jette les bases d'une grande partie du travail à venir et, à mon avis, c'est la décision la plus importante que vous allez prendre dans votre bibliothèque.

Il est important de créer une API « facile à utiliser » et suffisamment flexible pour permettre des améliorations futures et des cas d'utilisation avancés. Nous essaierons d'atteindre ces deux objectifs.

Nous allons créer un crochet personnalisé qui acceptera un seul objet de configuration. Cela permettra aux options futures d'être adoptées sans introduire de changements de rupture.

Une note sur les crochets

Hooks est une toute nouvelle façon d'écrire React. Si vous avez écrit React dans le passé, vous ne reconnaîtrez peut-être pas certains de ces concepts. Dans ce cas, veuillez consulter la documentation officielle. Il est incroyablement bien écrit et vous explique les bases que vous devez connaître.

Nous allons appeler notre crochet personnalisé useValidation pour l'instant. Son utilisation pourrait ressembler à ceci :

 const config = { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, onSubmit: e => { /* handle submit */ } }; const { getFieldProps, getFormProps, errors } = useValidation(config);

L'objet de config accepte un prop fields , qui définit les règles de validation pour chaque champ. De plus, il accepte un rappel lorsque le formulaire est soumis.

L'objet fields contient une clé pour chaque champ que nous voulons valider. Chaque champ a sa propre configuration, où chaque clé est un nom de validateur et chaque valeur est une propriété de configuration pour ce validateur. Une autre façon d'écrire la même chose serait:

 { fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }

Notre crochet useValidation renverra un objet avec quelques propriétés — getFieldProps , getFormProps et errors . Les deux premières fonctions sont ce que Kent C. Dodds appelle les "prop getters" (voir ici pour un excellent article sur celles-ci), et sont utilisées pour obtenir les accessoires pertinents pour un champ de formulaire ou une balise de formulaire donné. Le prop d' errors est un objet avec tous les messages d'erreur, saisis par champ.

Cette utilisation ressemblerait à ceci :

 const config = { ... }; // like above const LoginForm = props => { const { getFieldProps, getFormProps, errors } = useValidation(config); return ( <form {...getFormProps()}> <label> Username<br/> <input {...getFieldProps('username')} /> {errors.username && <div className="error">{errors.username}</div>} </label> <label> Password<br/> <input {...getFieldProps('password')} /> {errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Submit my form</button> </form> ); };

D'accord ! Nous avons donc cloué l'API.

  • Voir la démo CodeSandbox

Notez que nous avons également créé une implémentation fictive du hook useValidation . Pour l'instant, il renvoie simplement un objet avec les objets et les fonctions dont nous avons besoin pour être là, donc nous ne cassons pas notre exemple d'implémentation.

Stockage de l'état du formulaire

La première chose que nous devons faire est de stocker tout l'état du formulaire dans notre crochet personnalisé. Nous devons nous souvenir des valeurs de chaque champ, des messages d'erreur et si le formulaire a été soumis ou non. Nous utiliserons le crochet useReducer pour cela car il permet le plus de flexibilité (et moins de passe-partout). Si vous avez déjà utilisé Redux, vous verrez des concepts familiers - et sinon, nous vous expliquerons au fur et à mesure ! Nous allons commencer par écrire un réducteur, qui est passé au crochet useReducer :

 const initialState = { values: {}, errors: {}, submitted: false, }; function validationReducer(state, action) { switch(action.type) { case 'change': const values = { ...state.values, ...action.payload }; return { ...state, values, }; case 'submit': return { ...state, submitted: true }; default: throw new Error('Unknown action type'); } }

Qu'est-ce qu'un réducteur ?

Un réducteur est une fonction qui accepte un objet de valeurs et une "action" et renvoie une version augmentée de l'objet de valeurs.

Les actions sont des objets JavaScript simples avec une propriété de type . Nous utilisons une instruction switch pour gérer chaque type d'action possible.

L'« objet des valeurs » est souvent appelé état , et dans notre cas, il s'agit de l'état de notre logique de validation.

Notre état se compose de trois éléments de données - les values (les valeurs actuelles de nos champs de formulaire), les errors (l'ensemble actuel de messages d'erreur) et un indicateur isSubmitted indiquant si notre formulaire a été soumis au moins une fois ou non.

Afin de stocker notre état de formulaire, nous devons implémenter quelques parties de notre crochet useValidation . Lorsque nous appelons notre méthode getFieldProps , nous devons renvoyer un objet avec la valeur de ce champ, un gestionnaire de changement pour le moment où il change et un accessoire de nom pour suivre quel champ est lequel.

 function validationReducer(state, action) { // Like above } const initialState = { /* like above */ }; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); return { errors: state.errors, getFormProps: e => {}, getFieldProps: fieldName => ({ onChange: e => { if (!config.fields[fieldName]) { return; } dispatch({ type: 'change', payload: { [fieldName]: e.target.value } }); }, name: fieldName, value: state.values[fieldName], }), }; };

La méthode getFieldProps renvoie désormais les accessoires requis pour chaque champ. Lorsqu'un événement de modification est déclenché, nous nous assurons que ce champ est dans notre configuration de validation, puis informons notre réducteur qu'une action de change a eu lieu. Le réducteur gérera les modifications apportées à l'état de validation.

  • Voir la démo CodeSandbox

Validation de notre formulaire

Notre bibliothèque de validation de formulaire a l'air bien, mais ne fait pas grand-chose en termes de validation de nos valeurs de formulaire ! Réparons ça.

Nous allons valider tous les champs de chaque événement de modification. Cela peut ne pas sembler très efficace, mais dans les applications du monde réel que j'ai rencontrées, ce n'est pas vraiment un problème.

Notez que nous ne disons pas que vous devez afficher chaque erreur à chaque modification. Nous reviendrons sur la façon d'afficher les erreurs uniquement lorsque vous soumettez ou quittez un champ, plus loin dans cet article.

Comment choisir les fonctions du validateur

En ce qui concerne les validateurs, il existe des tonnes de bibliothèques qui implémentent toutes les méthodes de validation dont vous auriez besoin. Vous pouvez également écrire le vôtre si vous le souhaitez. C'est un exercice amusant !

Pour ce projet, nous allons utiliser un ensemble de validateurs que j'ai écrit il y a quelque temps — calidators . Ces validateurs ont l'API suivante :

 function isRequired(config) { return function(value) { if (value === '') { return config.message; } else { return null; } }; } // or the same, but terser const isRequired = config => value => value === '' ? config.message : null;

En d'autres termes, chaque validateur accepte un objet de configuration et renvoie un validateur entièrement configuré. Lorsque cette fonction est appelée avec une valeur, elle renvoie le message prop si la valeur est invalide, ou null si elle est valide. Vous pouvez voir comment certains de ces validateurs sont implémentés en regardant le code source.

Pour accéder à ces validateurs, installez le package calidators avec npm install calidators .

Valider un seul champ

Vous souvenez-vous de la configuration que nous transmettons à notre objet useValidation ? Il ressemble à ceci :

 { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, // more stuff }

Pour simplifier notre implémentation, supposons que nous n'ayons qu'un seul champ à valider. Nous allons parcourir chaque clé de l'objet de configuration du champ et exécuter les validateurs un par un jusqu'à ce que nous trouvions une erreur ou que nous ayons terminé la validation.

 import * as validators from 'calidators'; function validateField(fieldValue = '', fieldConfig) { for (let validatorName in fieldConfig) { const validatorConfig = fieldConfig[validatorName]; const validator = validators[validatorName]; const configuredValidator = validator(validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }

Ici, nous avons écrit une fonction validateField , qui accepte la valeur à valider et les configurations du validateur pour ce champ. Nous parcourons tous les validateurs, leur transmettons la configuration de ce validateur et l'exécutons. Si nous recevons un message d'erreur, nous sautons le reste des validateurs et revenons. Sinon, nous essayons le prochain validateur.

Remarque : Sur les API de validation

Si vous choisissez différents validateurs avec différentes API (comme le très populaire validator.js ), cette partie de votre code peut sembler un peu différente. Par souci de brièveté, cependant, nous laissons cette partie être un exercice laissé au lecteur.

Remarque : On for…in loops

Jamais utilisé for...in boucles auparavant ? Ça tombe bien, c'était aussi ma première fois ! Fondamentalement, il itère sur les clés d'un objet. Vous pouvez en savoir plus à leur sujet sur MDN.

Validez tous les champs

Maintenant que nous avons validé un champ, nous devrions pouvoir valider tous les champs sans trop de problèmes.

 function validateField(fieldValue = '', fieldConfig) { // as before } function validateFields(fieldValues, fieldConfigs) { const errors = {}; for (let fieldName in fieldConfigs) { const fieldConfig = fieldConfigs[fieldName]; const fieldValue = fieldValues[fieldName]; errors[fieldName] = validateField(fieldValue, fieldConfig); } return errors; }

Nous avons écrit une fonction validateFields qui accepte toutes les valeurs de champ et la configuration complète du champ. Nous parcourons chaque nom de champ dans la configuration et validons ce champ avec son objet de configuration et sa valeur.

Suivant : Dites à notre réducteur

Très bien, nous avons donc maintenant cette fonction qui valide toutes nos affaires. Incorporons-le dans le reste de notre code !

Tout d'abord, nous allons ajouter un gestionnaire d'action de validate à notre validationReducer .

 function validationReducer(state, action) { switch (action.type) { case 'change': // as before case 'submit': // as before case 'validate': return { ...state, errors: action.payload }; default: throw new Error('Unknown action type'); } }

Chaque fois que nous déclenchons l'action de validate , nous remplaçons les erreurs dans notre état par tout ce qui a été transmis parallèlement à l'action.

Ensuite, nous allons déclencher notre logique de validation à partir d'un crochet useEffect :

 const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };

Ce crochet useEffect s'exécute chaque fois que nos state.fields ou config.fields changent, en plus du premier montage.

Méfiez-vous des bogues

Il y a un bogue super subtil dans le code ci-dessus. Nous avons spécifié que notre crochet useEffect ne devrait être réexécuté que lorsque les state.fields ou config.fields changent. Il s'avère que « changer » ne signifie pas nécessairement un changement de valeur ! useEffect utilise Object.is pour assurer l'égalité entre les objets, qui à son tour utilise l'égalité des références. C'est-à-dire que si vous passez un nouvel objet avec le même contenu, ce ne sera pas le même (puisque l'objet lui-même est nouveau).

Les state.fields sont renvoyés par useReducer , ce qui nous garantit cette égalité de référence, mais notre config est spécifiée en ligne dans notre composant de fonction. Cela signifie que l'objet est recréé à chaque rendu, ce qui déclenchera à son tour l' useEffect ci-dessus !

Pour résoudre ce problème, nous devons utiliser la bibliothèque use-deep-compare-effect de Kent C. Dodds. Vous l'installez avec npm install use-deep-compare-effect , et remplacez votre appel useEffect par this à la place. Cela garantit que nous effectuons une vérification d'égalité approfondie au lieu d'une vérification d'égalité de référence.

Votre code ressemblera maintenant à ceci :

 import useDeepCompareEffect from 'use-deep-compare-effect'; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useDeepCompareEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };

Une note sur useEffect

Il s'avère que useEffect est une fonction assez intéressante. Dan Abramov a écrit un long article très agréable sur les subtilités de useEffect si vous souhaitez tout savoir sur ce crochet.

Maintenant, les choses commencent à ressembler à une bibliothèque de validation !

  • Voir la démo CodeSandbox

Traitement de la soumission du formulaire

Le dernier élément de notre bibliothèque de validation de formulaire de base gère ce qui se passe lorsque nous soumettons le formulaire. En ce moment, il recharge la page et rien ne se passe. Ce n'est pas optimal. Nous voulons empêcher le comportement par défaut du navigateur en ce qui concerne les formulaires et le gérer nous-mêmes à la place. Nous plaçons cette logique dans la fonction getter prop getFormProps :

 const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); // as before return { getFormProps: () => ({ onSubmit: e => { e.preventDefault(); dispatch({ type: 'submit' }); if (config.onSubmit) { config.onSubmit(state); } }, }), // as before }; };

Nous modifions notre fonction getFormProps pour renvoyer une fonction onSubmit , qui est déclenchée chaque fois que l'événement submit DOM est déclenché. Nous empêchons le comportement par défaut du navigateur, envoyons une action pour indiquer à notre réducteur que nous avons soumis et appelons le rappel onSubmit fourni avec l'état complet - s'il est fourni.

Sommaire

Nous y sommes ! Nous avons créé une bibliothèque de validation simple, utilisable et plutôt cool. Cependant, il reste encore beaucoup de travail à faire avant de pouvoir dominer les interwebs.

  • Partie 1 : Les bases
  • Partie 2 : Les fonctionnalités
  • Partie 3 : L'expérience