Erstellen Ihrer eigenen React-Validierungsbibliothek: Die Grundlagen (Teil 1)

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Haben Sie sich jemals gefragt, wie Validierungsbibliotheken funktionieren? In diesem Artikel erfahren Sie Schritt für Schritt, wie Sie Ihre eigene Validierungsbibliothek für React erstellen. Der nächste Teil wird einige erweiterte Funktionen hinzufügen, und der letzte Teil konzentriert sich auf die Verbesserung der Entwicklererfahrung.

Ich fand Bibliotheken zur Formularvalidierung schon immer ziemlich cool. Ich weiß, es ist ein Nischeninteresse – aber wir nutzen sie so oft! Zumindest in meinem Job – das meiste, was ich tue, ist das Erstellen mehr oder weniger komplexer Formulare mit Validierungsregeln, die von früheren Entscheidungen und Pfaden abhängen. Es ist von größter Bedeutung zu verstehen, wie eine Formularvalidierungsbibliothek funktionieren würde.

Letztes Jahr habe ich eine solche Formularvalidierungsbibliothek geschrieben. Ich habe es „Calidation“ genannt, und Sie können den einführenden Blogbeitrag hier lesen. Es ist eine gute Bibliothek, die viel Flexibilität bietet und einen etwas anderen Ansatz verfolgt als die anderen auf dem Markt. Es gibt jedoch auch unzählige andere großartige Bibliotheken – meine hat für unsere Anforderungen einfach gut funktioniert.

Heute zeige ich Ihnen, wie Sie Ihre eigene Validierungsbibliothek für React schreiben. Wir werden den Prozess Schritt für Schritt durchgehen, und Sie werden CodeSandbox-Beispiele finden, während wir fortfahren. Am Ende dieses Artikels werden Sie wissen, wie Sie Ihre eigene Validierungsbibliothek schreiben oder zumindest ein tieferes Verständnis dafür haben, wie andere Bibliotheken „die Magie der Validierung“ implementieren.

  • Teil 1: Die Grundlagen
  • Teil 2: Die Funktionen
  • Teil 3: Die Erfahrung
Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Schritt 1: Entwerfen der API

Der erste Schritt beim Erstellen einer Bibliothek besteht darin, zu entwerfen, wie sie verwendet werden soll. Es legt die Grundlage für einen Großteil der kommenden Arbeit und ist meiner Meinung nach die wichtigste Entscheidung, die Sie in Ihrer Bibliothek treffen werden.

Es ist wichtig, eine API zu erstellen, die „benutzerfreundlich“ und dennoch flexibel genug ist, um zukünftige Verbesserungen und erweiterte Anwendungsfälle zu ermöglichen. Wir werden versuchen, beide Ziele zu erreichen.

Wir werden einen benutzerdefinierten Hook erstellen, der ein einzelnes Konfigurationsobjekt akzeptiert. Auf diese Weise können zukünftige Optionen verabschiedet werden, ohne Breaking Changes einzuführen.

Eine Anmerkung zu Haken

Hooks ist eine ziemlich neue Art, React zu schreiben. Wenn Sie in der Vergangenheit React geschrieben haben, werden Sie einige dieser Konzepte möglicherweise nicht wiedererkennen. In diesem Fall werfen Sie bitte einen Blick auf die offizielle Dokumentation. Es ist unglaublich gut geschrieben und führt Sie durch die Grundlagen, die Sie wissen müssen.

Wir werden unseren benutzerdefinierten Hook useValidation nennen. Seine Verwendung könnte in etwa so aussehen:

 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);

Das config akzeptiert eine fields -Prop, die die Validierungsregeln für jedes Feld einrichtet. Darüber hinaus akzeptiert es einen Rückruf, wenn das Formular gesendet wird.

Das fields -Objekt enthält einen Schlüssel für jedes zu validierende Feld. Jedes Feld hat seine eigene Konfiguration, wobei jeder Schlüssel ein Prüfername und jeder Wert eine Konfigurationseigenschaft für diesen Prüfer ist. Eine andere Schreibweise wäre:

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

Unser useValidation Hook gibt ein Objekt mit einigen Eigenschaften zurück – getFieldProps , getFormProps und errors . Die beiden ersten Funktionen nennt Kent C. Dodds „Prop-Getter“ (hier finden Sie einen großartigen Artikel dazu) und werden verwendet, um die relevanten Props für ein bestimmtes Formularfeld oder Formular-Tag zu erhalten. Die errors -Prop ist ein Objekt mit beliebigen Fehlermeldungen, die pro Feld verschlüsselt sind.

Diese Verwendung würde wie folgt aussehen:

 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> ); };

In Ordnung! Also haben wir die API festgenagelt.

  • Siehe CodeSandbox-Demo

Beachten Sie, dass wir auch eine Scheinimplementierung des useValidation erstellt haben. Im Moment gibt es nur ein Objekt mit den Objekten und Funktionen zurück, die wir benötigen, damit wir unsere Beispielimplementierung nicht unterbrechen.

Speichern des Formularstatus

Als Erstes müssen wir den gesamten Formularstatus in unserem benutzerdefinierten Hook speichern. Wir müssen uns die Werte der einzelnen Felder merken, eventuelle Fehlermeldungen und ob das Formular gesendet wurde oder nicht. Wir verwenden dafür den useReducer Hook, da er die größte Flexibilität (und weniger Boilerplate) ermöglicht. Wenn Sie jemals Redux verwendet haben, werden Sie einige vertraute Konzepte sehen – und wenn nicht, werden wir es im weiteren Verlauf erklären! Wir beginnen damit, einen Reducer zu schreiben, der an den useReducer Hook übergeben wird:

 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'); } }

Was ist ein Reducer?

Ein Reducer ist eine Funktion, die ein Werteobjekt und eine „Aktion“ akzeptiert und eine erweiterte Version des Werteobjekts zurückgibt.

Aktionen sind einfache JavaScript-Objekte mit einer type Eigenschaft. Wir verwenden eine switch Anweisung, um jeden möglichen Aktionstyp zu behandeln.

Das „Objekt der Werte“ wird oft als Zustand bezeichnet, und in unserem Fall ist es der Zustand unserer Validierungslogik.

Unser Status besteht aus drei Datenelementen – values (den aktuellen Werten unserer Formularfelder), errors (dem aktuellen Satz von Fehlermeldungen) und einem Flag isSubmitted , das angibt, ob unser Formular mindestens einmal gesendet wurde oder nicht.

Um unseren Formularstatus zu speichern, müssen wir einige Teile unseres useValidation . Wenn wir unsere getFieldProps Methode aufrufen, müssen wir ein Objekt mit dem Wert dieses Felds, einen Change-Handler für Änderungen und eine Namensstütze zurückgeben, um zu verfolgen, welches Feld welches ist.

 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], }), }; };

Die Methode getFieldProps gibt nun die für jedes Feld erforderlichen Requisiten zurück. Wenn ein Änderungsereignis ausgelöst wird, stellen wir sicher, dass sich das Feld in unserer Validierungskonfiguration befindet, und teilen dann unserem Reducer mit, dass eine change stattgefunden hat. Der Reducer verarbeitet die Änderungen des Validierungsstatus.

  • Siehe CodeSandbox-Demo

Validierung unseres Formulars

Unsere Formularvalidierungsbibliothek sieht gut aus, tut aber nicht viel in Bezug auf die Validierung unserer Formularwerte! Lassen Sie uns das beheben.

Wir validieren alle Felder bei jedem Änderungsereignis. Das mag nicht sehr effizient klingen, aber in den realen Anwendungen, auf die ich gestoßen bin, ist es kein wirkliches Problem.

Beachten Sie, dass wir nicht sagen, dass Sie jeden Fehler bei jeder Änderung anzeigen müssen. Wir werden später in diesem Artikel noch einmal darauf eingehen, wie Fehler nur angezeigt werden, wenn Sie ein Feld senden oder von einem Feld weg navigieren.

So wählen Sie Validator-Funktionen aus

Wenn es um Validatoren geht, gibt es unzählige Bibliotheken, die alle Validierungsmethoden implementieren, die Sie jemals benötigen würden. Sie können auch Ihre eigenen schreiben, wenn Sie möchten. Es ist eine lustige Übung!

Für dieses Projekt werden wir eine Reihe von Validatoren verwenden, die ich vor einiger Zeit geschrieben habe – calidators . Diese Validatoren haben die folgende 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;

Mit anderen Worten, jeder Validator akzeptiert ein Konfigurationsobjekt und gibt einen vollständig konfigurierten Validator zurück. Wenn diese Funktion mit einem Wert aufgerufen wird, gibt sie die message -Prop zurück, wenn der Wert ungültig ist, oder null , wenn er gültig ist. Sie können sehen, wie einige dieser Validatoren implementiert sind, indem Sie sich den Quellcode ansehen.

Um auf diese Validatoren zuzugreifen, installieren Sie das Paket calidators mit npm install calidators .

Validieren Sie ein einzelnes Feld

Erinnern Sie sich an die Konfiguration, die wir an unser useValidation Objekt übergeben? Es sieht aus wie das:

 { 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 }

Um unsere Implementierung zu vereinfachen, nehmen wir an, wir haben nur ein einziges zu validierendes Feld. Wir gehen jeden Schlüssel des Konfigurationsobjekts des Felds durch und führen die Validatoren nacheinander aus, bis wir entweder einen Fehler finden oder mit der Validierung fertig sind.

 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; }

Hier haben wir eine Funktion validateField geschrieben, die den zu validierenden Wert und die Validator-Konfigurationen für dieses Feld akzeptiert. Wir durchlaufen alle Validatoren, übergeben ihnen die Konfiguration für diesen Validator und führen ihn aus. Wenn wir eine Fehlermeldung erhalten, überspringen wir die restlichen Validatoren und kehren zurück. Wenn nicht, versuchen wir den nächsten Validator.

Hinweis: Auf Validator-APIs

Wenn Sie verschiedene Validatoren mit unterschiedlichen APIs wählen (wie den sehr beliebten validator.js ), sieht dieser Teil Ihres Codes möglicherweise etwas anders aus. Der Kürze halber überlassen wir diesen Teil jedoch dem Leser als Übung.

Hinweis: Ein für…in Schleifen

Noch nie for...in Schleifen verwendet? Das ist in Ordnung, das war auch mein erstes Mal! Grundsätzlich iteriert es über die Schlüssel in einem Objekt. Sie können mehr darüber bei MDN lesen.

Bestätigen Sie alle Felder

Nachdem wir nun ein Feld validiert haben, sollten wir in der Lage sein, alle Felder ohne allzu großen Aufwand zu validieren.

 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; }

Wir haben eine Funktion validateFields geschrieben, die alle Feldwerte und die gesamte Feldkonfiguration akzeptiert. Wir durchlaufen jeden Feldnamen in der Konfiguration und validieren dieses Feld mit seinem Konfigurationsobjekt und -wert.

Weiter: Sagen Sie es unserem Reduzierer

Okay, jetzt haben wir also diese Funktion, die all unsere Sachen validiert. Ziehen wir es in den Rest unseres Codes!

Zuerst fügen wir unserem validate einen Validate- validationReducer hinzu.

 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'); } }

Immer wenn wir die validate -Aktion auslösen, ersetzen wir die Fehler in unserem Zustand durch das, was neben der Aktion übergeben wurde.

Als nächstes werden wir unsere Validierungslogik von einem useEffect Hook auslösen:

 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 }; };

Dieser useEffect Hook wird immer dann ausgeführt, wenn sich entweder unsere state.fields oder config.fields ändern, zusätzlich zum ersten Mount.

Vorsicht vor Fehler

Es gibt einen super subtilen Fehler im obigen Code. Wir haben festgelegt, dass unser useEffect -Hook nur dann erneut ausgeführt werden soll, wenn sich die state.fields oder config.fields ändern. Es stellt sich heraus, dass „Veränderung“ nicht unbedingt eine Wertveränderung bedeutet! useEffect verwendet Object.is , um die Gleichheit zwischen Objekten sicherzustellen, was wiederum Referenzgleichheit verwendet. Das heißt – wenn Sie ein neues Objekt mit demselben Inhalt übergeben, wird es nicht dasselbe sein (da das Objekt selbst neu ist).

Die state.fields werden von useReducer , was uns diese Referenzgleichheit garantiert, aber unsere config wird inline in unserer Funktionskomponente angegeben. Das bedeutet, dass das Objekt bei jedem Rendern neu erstellt wird, was wiederum den obigen useEffect !

Um dies zu lösen, müssen wir die use-deep-compare-effect Bibliothek von Kent C. Dodds verwenden. Sie installieren es mit npm install use-deep-compare-effect und ersetzen stattdessen Ihren useEffect -Aufruf durch diesen. Dadurch wird sichergestellt, dass wir eine umfassende Gleichheitsprüfung anstelle einer Referenzgleichheitsprüfung durchführen.

Ihr Code sieht nun so aus:

 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 }; };

Eine Anmerkung zu useEffect

Es stellt sich heraus, useEffect eine ziemlich interessante Funktion ist. Dan Abramov hat einen wirklich netten, langen Artikel über die Feinheiten von useEffect , wenn Sie daran interessiert sind, alles über diesen Hook zu erfahren.

Jetzt fangen die Dinge an, wie eine Validierungsbibliothek auszusehen!

  • Siehe CodeSandbox-Demo

Umgang mit der Formularübermittlung

Der letzte Teil unserer grundlegenden Formularvalidierungsbibliothek behandelt, was passiert, wenn wir das Formular absenden. Im Moment lädt es die Seite neu und nichts passiert. Das ist nicht optimal. Wir möchten das standardmäßige Browserverhalten bei Formularen verhindern und stattdessen selbst damit umgehen. Wir platzieren diese Logik in der Prop-Getter-Funktion 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 }; };

Wir ändern unsere getFormProps -Funktion so, dass sie eine onSubmit -Funktion zurückgibt, die immer dann ausgelöst wird, wenn das submit -DOM-Ereignis ausgelöst wird. Wir verhindern das standardmäßige Browserverhalten, senden eine Aktion, um unserem Reducer mitzuteilen, dass wir gesendet haben, und rufen den bereitgestellten onSubmit Callback mit dem gesamten Status auf – sofern vorhanden.

Zusammenfassung

War da! Wir haben eine einfache, brauchbare und ziemlich coole Validierungsbibliothek erstellt. Es gibt jedoch noch eine Menge Arbeit zu tun, bevor wir die Interwebs dominieren können.

  • Teil 1: Die Grundlagen
  • Teil 2: Die Funktionen
  • Teil 3: Die Erfahrung