Crearea propriei biblioteci de validare React: elementele de bază (partea 1)
Publicat: 2022-03-10Întotdeauna am crezut că bibliotecile de validare a formularelor sunt destul de grozave. Știu, este un interes de nișă de a avea – dar le folosim atât de mult! Cel puțin în meseria mea — cea mai mare parte a ceea ce fac este să construiesc forme mai mult sau mai puțin complexe cu reguli de validare care depind de alegerile și căile anterioare. Înțelegerea modului în care ar funcționa o bibliotecă de validare a formularelor este esențială.
Anul trecut, am scris o astfel de bibliotecă de validare a formularelor. L-am numit „Calidare” și puteți citi articolul introductiv pe blog aici. Este o bibliotecă bună care oferă multă flexibilitate și folosește o abordare puțin diferită față de celelalte de pe piață. Există totuși o mulțime de alte biblioteci grozave, totuși – a mea tocmai a funcționat bine pentru cerințele noastre .
Astăzi, vă voi arăta cum să vă scrieți propria bibliotecă de validare pentru React. Vom parcurge procesul pas cu pas și veți găsi exemple de CodeSandbox pe măsură ce mergem mai departe. Până la sfârșitul acestui articol, veți ști cum să vă scrieți propria bibliotecă de validare sau, cel puțin, veți avea o înțelegere mai profundă a modului în care alte biblioteci implementează „magia validării”.
- Partea 1: Bazele
- Partea 2: Caracteristicile
- Partea 3: Experiența
Pasul 1: Proiectarea API-ului
Primul pas al creării oricărei biblioteci este proiectarea modului în care va fi utilizată. El pune bazele multor lucrări care vor urma și, în opinia mea, este cea mai importantă decizie pe care o veți lua în biblioteca dvs.
Este important să creați un API care să fie „ușor de utilizat” și totuși suficient de flexibil pentru a permite îmbunătățiri viitoare și cazuri de utilizare avansate. Vom încerca să atingem ambele obiective.
Vom crea un cârlig personalizat care va accepta un singur obiect de configurare. Acest lucru va permite ca opțiunile viitoare să fie trecute fără a introduce modificări rupturi.
O notă despre cârlige
Hooks este un mod destul de nou de a scrie React. Dacă ați scris React în trecut, este posibil să nu recunoașteți câteva dintre aceste concepte. În acest caz, vă rugăm să aruncați o privire la documentația oficială. Este incredibil de bine scris și vă duce prin elementele de bază pe care trebuie să le cunoașteți.
Vom numi useValidation
cârligului personalizat. Utilizarea sa ar putea arăta cam așa:
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);
Obiectul de config
acceptă o prop de fields
, care stabilește regulile de validare pentru fiecare câmp. În plus, acceptă un apel invers pentru momentul trimiterii formularului.
Obiectul fields
conține o cheie pentru fiecare câmp pe care dorim să-l validăm. Fiecare câmp are propria sa configurație, unde fiecare cheie este un nume de validator și fiecare valoare este o proprietate de configurare pentru acel validator. Un alt mod de a scrie același lucru ar fi:
{ fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }
Cârligul nostru useValidation
va returna un obiect cu câteva proprietăți — getFieldProps
, getFormProps
și errors
. Primele două funcții sunt ceea ce Kent C. Dodds numește „prop getters” (vezi aici un articol grozav despre acestea) și sunt folosite pentru a obține elementele de recuzită relevante pentru un anumit câmp de formular sau etichetă de formular. Propul errors
este un obiect cu orice mesaj de eroare, introdus pe câmp.
Această utilizare ar arăta astfel:
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> ); };
În regulă! Așa că am reușit API-ul.
- Vedeți demonstrația CodeSandbox
Rețineți că am creat și o implementare simulată a cârligului useValidation
. Deocamdată, este doar returnarea unui obiect cu obiectele și funcțiile de care avem nevoie pentru a fi acolo, așa că nu ne întrerupem implementarea eșantionului.
Stocarea stării formularului
Primul lucru pe care trebuie să-l facem este să stocăm toată starea formularului în cârligul nostru personalizat. Trebuie să ne amintim valorile fiecărui câmp, orice mesaje de eroare și dacă formularul a fost sau nu trimis. Vom folosi cârligul useReducer
pentru acest lucru, deoarece permite cea mai mare flexibilitate (și mai puțin boilerplate). Dacă ați folosit vreodată Redux, veți vedea câteva concepte familiare - și dacă nu, vă vom explica pe măsură ce mergem! Vom începe prin a scrie un reductor, care este transmis cârligului 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'); } }
Ce este un reductor?
Un reductor este o funcție care acceptă un obiect de valori și o „acțiune” și returnează o versiune augmentată a obiectului de valori.
Acțiunile sunt obiecte JavaScript simple cu o proprietate de type
. Folosim o instrucțiune switch
pentru a gestiona fiecare tip de acțiune posibil.
„Obiectul valorilor” este adesea denumit stare și, în cazul nostru, este starea logicii noastre de validare.
Starea noastră constă din trei date - values
(valorile curente ale câmpurilor formularului nostru), errors
(setul curent de mesaje de eroare) și un semnalizator isSubmitted
care indică dacă formularul nostru a fost trimis cel puțin o dată.
Pentru a stoca starea formularului, trebuie să implementăm câteva părți ale cârligului nostru useValidation
. Când apelăm metoda getFieldProps
, trebuie să returnăm un obiect cu valoarea acelui câmp, un handler de schimbare pentru când se schimbă și un nume de prop pentru a urmări câmpul care este.
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], }), }; };
Metoda getFieldProps
returnează acum elementele de recuzită necesare pentru fiecare câmp. Când este declanșat un eveniment de modificare, ne asigurăm că acel câmp se află în configurația noastră de validare și apoi îi spunem reductorului nostru că a avut loc o acțiune de change
. Reductorul se va ocupa de modificările stării de validare.
- Vedeți demonstrația CodeSandbox
Validarea formularului nostru
Biblioteca noastră de validare a formularelor arată bine, dar nu face mare lucru în ceea ce privește validarea valorilor formularelor noastre! Să reparăm asta.

Vom valida toate câmpurile la fiecare eveniment de modificare. Acest lucru poate să nu sune foarte eficient, dar în aplicațiile din lumea reală pe care le-am întâlnit, nu este cu adevărat o problemă.
Rețineți, nu spunem că trebuie să afișați fiecare eroare la fiecare modificare. Vom revizui modul de afișare a erorilor numai atunci când trimiteți sau vă îndepărtați de un câmp, mai târziu în acest articol.
Cum să alegeți funcțiile de validare
Când vine vorba de validatoare, există o mulțime de biblioteci care implementează toate metodele de validare de care ai avea nevoie vreodată. De asemenea, poți să-l scrii pe al tău dacă vrei. Este un exercițiu distractiv!
Pentru acest proiect, vom folosi un set de validatori pe care l-am scris cu ceva timp în urmă — calidators
. Acești validatori au următorul API:
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;
Cu alte cuvinte, fiecare validator acceptă un obiect de configurare și returnează un validator complet configurat. Când acea funcție este apelată cu o valoare, returnează message
prop dacă valoarea este invalidă sau null
dacă este validă. Puteți vedea cum sunt implementați unii dintre acești validatori uitându-vă la codul sursă.
Pentru a accesa aceste validatoare, instalați pachetul calidators
cu npm install calidators
.
Validați un singur câmp
Vă amintiți configurația pe care o transmitem obiectului nostru useValidation
? Arata cam asa:
{ 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 }
Pentru a simplifica implementarea noastră, să presupunem că avem doar un singur câmp de validat. Vom parcurge fiecare cheie a obiectului de configurare al câmpului și vom rula validatorii unul câte unul până când fie găsim o eroare, fie terminăm validarea.
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; }
Aici, am scris o funcție validateField
, care acceptă valoarea de validat și configurațiile validatorului pentru acel câmp. Facem o buclă prin toate validatorii, le transmitem configurația pentru acel validator și o rulăm. Dacă primim un mesaj de eroare, sărim peste restul validatorilor și revenim. Dacă nu, încercăm următorul validator.
Notă: pe API-urile validatoare
Dacă alegeți validatori diferiți cu API-uri diferite (cum ar fi foarte popularul validator.js
), această parte a codului dvs. ar putea arăta puțin diferit. De dragul conciziei, totuși, lăsăm acea parte să fie un exercițiu lăsat cititorului.
Notă: Activat pentru... în bucle
Nu a fost folosit niciodată for...in
bucle înainte? În regulă, a fost și prima dată! Practic, iterează peste cheile dintr-un obiect. Puteți citi mai multe despre ele la MDN.
Validați toate câmpurile
Acum că am validat un câmp, ar trebui să putem valida toate câmpurile fără prea multe probleme.
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; }
Am scris o funcție validateFields
care acceptă toate valorile câmpului și întreaga configurație a câmpului. Parcurgem fiecare nume de câmp din configurație și validăm acel câmp cu obiectul și valoarea de configurare.
Următorul: Spune reductorului nostru
Bine, așa că acum avem această funcție care validează toate lucrurile noastre. Să o introducem în restul codului nostru!
În primul rând, vom adăuga un handler de acțiune de validate
la 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'); } }
Ori de câte ori declanșăm acțiunea de validate
, înlocuim erorile din starea noastră cu orice a fost transmis alături de acțiune.
În continuare, vom declanșa logica noastră de validare dintr-un cârlig 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 }; };
Acest cârlig useEffect
rulează ori de câte ori state.fields
sau config.fields
se modifică, în plus față de prima montare.
Atenție la Bug
Există o eroare super subtilă în codul de mai sus. Am specificat că hook-ul nostru useEffect
ar trebui să se relueze numai ori de câte ori state.fields
sau config.fields
se schimbă. Se pare că „schimbarea” nu înseamnă neapărat o schimbare a valorii! useEffect
folosește Object.is
pentru a asigura egalitatea între obiecte, care la rândul său utilizează egalitatea de referință. Adică, dacă treceți un obiect nou cu același conținut, acesta nu va fi același (deoarece obiectul în sine este nou).
state.fields
sunt returnate de la useReducer
, ceea ce ne garantează această egalitate de referință, dar config
noastră este specificată în linie în componenta noastră de funcție. Aceasta înseamnă că obiectul este recreat la fiecare randare, care, la rândul său, va declanșa useEffect
de mai sus!
Pentru a rezolva acest lucru, trebuie să folosim biblioteca cu use-deep-compare-effect
de Kent C. Dodds. Îl instalați cu npm install use-deep-compare-effect
și înlocuiți apelul useEffect
cu acesta. Acest lucru ne asigură că facem o verificare profundă a egalității în loc de o verificare a egalității de referință.
Codul tău va arăta acum astfel:
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 }; };
O notă despre useEffect
Se pare că useEffect
este o funcție destul de interesantă. Dan Abramov a scris un articol foarte frumos și lung despre complexitatea utilizării useEffect
dacă sunteți interesat să aflați tot ce este despre acest cârlig.
Acum lucrurile încep să arate ca o bibliotecă de validare!
- Vedeți demonstrația CodeSandbox
Gestionarea depunerii formularului
Ultima componentă a bibliotecii noastre de bază de validare a formularelor se ocupă de ceea ce se întâmplă atunci când trimitem formularul. În acest moment, reîncarcă pagina și nu se întâmplă nimic. Asta nu este optim. Dorim să prevenim comportamentul implicit al browserului când vine vorba de formulare și să ne ocupăm în schimb de el. Plasăm această logică în interiorul funcției getFormProps
prop getter:
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 }; };
Ne schimbăm funcția getFormProps
pentru a returna o funcție onSubmit
, care este declanșată ori de câte ori este declanșat evenimentul DOM de submit
. Prevenim comportamentul implicit al browserului, trimitem o acțiune pentru a spune reductorului nostru pe care l-am trimis și apelăm apelul onSubmit
furnizat cu întreaga stare – dacă este furnizat.
rezumat
Au fost acolo! Am creat o bibliotecă de validare simplă, utilizabilă și destul de grozavă. Mai sunt totuși o mulțime de muncă de făcut înainte de a putea domina interweb-urile.
- Partea 1: Bazele
- Partea 2: Caracteristicile
- Partea 3: Experiența