Tworzenie własnej biblioteki walidacji React: podstawy (część 1)

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Zastanawiałeś się kiedyś, jak działają biblioteki walidacji? W tym artykule dowiesz się, jak krok po kroku zbudować własną bibliotekę walidacji dla Reacta. Następna część doda bardziej zaawansowane funkcje, a ostatnia część skupi się na poprawie doświadczenia programisty.

Zawsze uważałem, że biblioteki walidacji formularzy są całkiem fajne. Wiem, to niszowe zainteresowanie — ale tak często ich używamy! Przynajmniej w mojej pracy — większość tego, co robię, to konstruowanie mniej lub bardziej złożonych formularzy z regułami walidacji, które zależą od wcześniejszych wyborów i ścieżek. Zrozumienie, jak działa biblioteka walidacji formularzy, jest najważniejsze.

W zeszłym roku napisałem jedną taką bibliotekę walidacji formularzy. Nazwałem go „Calidation” i możesz przeczytać wprowadzający wpis na blogu tutaj. To dobra biblioteka, która oferuje dużą elastyczność i wykorzystuje nieco inne podejście niż inne dostępne na rynku. Istnieje jednak mnóstwo innych świetnych bibliotek — moja po prostu działała dobrze dla naszych wymagań.

Dzisiaj pokażę Ci, jak napisać własną bibliotekę walidacji dla Reacta. Przejdziemy przez ten proces krok po kroku, a przykłady CodeSandbox znajdziesz w miarę postępów. Pod koniec tego artykułu będziesz wiedział, jak napisać własną bibliotekę walidacji, lub przynajmniej będziesz miał głębsze zrozumienie tego, jak inne biblioteki wdrażają „magię walidacji”.

  • Część 1: Podstawy
  • Część 2: Funkcje
  • Część 3: Doświadczenie
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Krok 1: Projektowanie API

Pierwszym krokiem tworzenia dowolnej biblioteki jest zaprojektowanie sposobu jej wykorzystania. Stanowi podstawę dla wielu przyszłych prac i moim zdaniem jest to najważniejsza decyzja, jaką podejmiesz w swojej bibliotece.

Ważne jest, aby utworzyć interfejs API, który jest „łatwy w użyciu”, a jednocześnie wystarczająco elastyczny, aby umożliwić przyszłe ulepszenia i zaawansowane przypadki użycia. Postaramy się osiągnąć oba te cele.

Stworzymy niestandardowy hook, który zaakceptuje pojedynczy obiekt konfiguracyjny. Pozwoli to na przekazywanie przyszłych opcji bez wprowadzania istotnych zmian.

Uwaga o hakach

Hooki to całkiem nowy sposób pisania Reacta. Jeśli pisałeś React w przeszłości, możesz nie rozpoznać kilku z tych pojęć. W takim przypadku prosimy o zapoznanie się z oficjalną dokumentacją. Jest niesamowicie dobrze napisany i zawiera podstawowe informacje, które musisz znać.

Na razie wywołamy nasz niestandardowy hook useValidation . Jego użycie może wyglądać mniej więcej tak:

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

Obiekt config akceptuje właściwości fields , które ustalają reguły walidacji dla każdego pola. Ponadto akceptuje wywołanie zwrotne w momencie przesłania formularza.

Obiekt fields zawiera klucz dla każdego pola, które chcemy zweryfikować. Każde pole ma swoją własną konfigurację, gdzie każdy klucz jest nazwą walidatora, a każda wartość jest właściwością konfiguracyjną tego walidatora. Innym sposobem napisania tego samego byłoby:

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

Nasz hook useValidation zwróci obiekt z kilkoma właściwościami — getFieldProps , getFormProps i errors . Dwie pierwsze funkcje są tym, co Kent C. Dodds nazywa „pobieraczami właściwości” (zobacz tutaj świetny artykuł na ich temat) i służą do pobierania odpowiednich właściwości dla danego pola formularza lub tagu formularza. Właściwość errors to obiekt z dowolnymi komunikatami o błędach, kluczowany według pola.

To użycie wyglądałoby tak:

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

Wporządku! Więc dopracowaliśmy API.

  • Zobacz demo CodeSandbox

Zauważ, że stworzyliśmy również symulowaną implementację haka useValidation . Na razie jest to po prostu zwracanie obiektu z obiektami i funkcjami, których potrzebujemy, aby się tam znajdowały, więc nie psujemy naszej przykładowej implementacji.

Przechowywanie stanu formularza

Pierwszą rzeczą, którą musimy zrobić, jest przechowywanie całego stanu formularza w naszym niestandardowym haczyku. Musimy pamiętać wartości każdego pola, wszelkie komunikaty o błędach i czy formularz został przesłany. W tym celu użyjemy haka useReducer , ponieważ zapewnia on największą elastyczność (i mniej schematu). Jeśli kiedykolwiek korzystałeś z Redux, zobaczysz kilka znajomych koncepcji — a jeśli nie, wyjaśnimy to w dalszej części! Zaczniemy od napisania reduktora, który jest przekazywany do haka 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'); } }

Co to jest reduktor?

Reduktor to funkcja, która akceptuje obiekt wartości i „akcję” i zwraca rozszerzoną wersję obiektu wartości.

Akcje są zwykłymi obiektami JavaScript z właściwością type . Używamy instrukcji switch do obsługi każdego możliwego typu akcji.

„Obiekt wartości” jest często określany jako stan , aw naszym przypadku jest to stan naszej logiki walidacji.

Nasz stan składa się z trzech części danych — values (bieżących wartości naszych pól formularza), errors (bieżącego zestawu komunikatów o błędach) oraz flagi isSubmitted wskazującej, czy nasz formularz został przesłany przynajmniej raz.

Aby przechowywać stan formularza, musimy zaimplementować kilka części naszego haka useValidation . Kiedy wywołujemy naszą metodę getFieldProps , musimy zwrócić obiekt z wartością tego pola, procedurę obsługi zmian dla zmiany oraz nazwę właściwości do śledzenia, które pole jest które.

 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 zwraca teraz właściwości wymagane dla każdego pola. Po uruchomieniu zdarzenia zmiany upewniamy się, że pole znajduje się w naszej konfiguracji walidacji, a następnie informujemy naszego reduktora, że ​​nastąpiła akcja change . Reduktor obsłuży zmiany stanu walidacji.

  • Zobacz demo CodeSandbox

Walidacja naszego formularza

Nasza biblioteka do walidacji formularzy wygląda dobrze, ale nie robi wiele, jeśli chodzi o walidację naszych wartości formularzy! Naprawmy to.

Będziemy sprawdzać wszystkie pola przy każdym zdarzeniu zmiany. Może to nie brzmieć zbyt wydajnie, ale w rzeczywistych aplikacjach, z którymi się zetknąłem, nie stanowi to problemu.

Uwaga, nie mówimy, że musisz pokazywać każdy błąd przy każdej zmianie. W dalszej części tego artykułu omówimy ponownie, jak wyświetlać błędy tylko wtedy, gdy prześlesz lub opuścisz pole.

Jak wybrać funkcje walidatora

Jeśli chodzi o walidatory, istnieje mnóstwo bibliotek, które implementują wszystkie metody walidacji, jakich kiedykolwiek potrzebujesz. Możesz też napisać własny, jeśli chcesz. To zabawne ćwiczenie!

W tym projekcie użyjemy zestawu walidatorów, które napisałem jakiś czas temu — calidators . Te walidatory mają następujący interfejs 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;

Innymi słowy, każdy walidator akceptuje obiekt konfiguracji i zwraca w pełni skonfigurowany walidator. Gdy ta funkcja jest wywoływana z wartością, zwraca właściwość message , jeśli wartość jest nieprawidłowa, lub null , jeśli jest poprawna. Możesz zobaczyć, jak niektóre z tych walidatorów są zaimplementowane, patrząc na kod źródłowy.

Aby uzyskać dostęp do tych walidatorów, zainstaluj pakiet calidators za pomocą npm install calidators .

Sprawdź poprawność jednego pola

Pamiętasz konfigurację, którą przekazujemy do naszego obiektu useValidation ? To wygląda tak:

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

Aby uprościć naszą implementację, załóżmy, że mamy do sprawdzenia tylko jedno pole. Przejrzymy każdy klucz obiektu konfiguracyjnego pola i uruchomimy walidatory jeden po drugim, aż znajdziemy błąd lub zakończymy walidację.

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

Tutaj napisaliśmy funkcję validateField , która akceptuje wartość do sprawdzenia i konfigurację walidatora dla tego pola. Przechodzimy przez wszystkie walidatory, przekazujemy im konfigurację tego walidatora i uruchamiamy go. Jeśli otrzymamy komunikat o błędzie, pomijamy resztę walidatorów i wracamy. Jeśli nie, spróbujemy następnego walidatora.

Uwaga: w przypadku interfejsów API walidatora

Jeśli wybierzesz różne walidatory z różnymi interfejsami API (jak bardzo popularny validator.js ), ta część kodu może wyglądać nieco inaczej. Jednak ze względu na zwięzłość pozostawiamy tę część ćwiczeniem pozostawionym czytelnikowi.

Uwaga: włączone dla…w pętlach

Nigdy wcześniej nie używany for...in pętlach? W porządku, to też był mój pierwszy raz! Zasadniczo iteruje po kluczach w obiekcie. Więcej o nich przeczytasz na MDN.

Sprawdź poprawność wszystkich pól

Teraz, gdy sprawdziliśmy już jedno pole, powinniśmy być w stanie sprawdzić poprawność wszystkich pól bez większych problemów.

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

Napisaliśmy funkcję validateFields , która akceptuje wszystkie wartości pól i całą konfigurację pola. Przechodzimy w pętli przez każdą nazwę pola w konfiguracji i sprawdzamy poprawność tego pola za pomocą jego obiektu konfiguracji i wartości.

Dalej: Powiedz naszemu reduktorowi

W porządku, więc teraz mamy tę funkcję, która sprawdza wszystkie nasze rzeczy. Wciągnijmy to do reszty naszego kodu!

Najpierw dodamy procedurę obsługi akcji validate do naszego 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'); } }

Za każdym razem, gdy uruchamiamy akcję validate , zastępujemy błędy w naszym stanie tym, co zostało przekazane wraz z akcją.

Następnie uruchomimy naszą logikę walidacji z haka 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 }; };

Ten haczyk useEffect działa zawsze, gdy zmieni się nasz state.fields lub config.fields , oprócz pierwszego montowania.

Uważaj na błąd

W powyższym kodzie jest bardzo subtelny błąd. Określiliśmy, że nasz hook useEffect powinien być uruchamiany ponownie tylko wtedy, gdy zmienią się state.fields lub config.fields . Okazuje się, że „zmiana” niekoniecznie oznacza zmianę wartości! useEffect używa Object.is do zapewnienia równości między obiektami, co z kolei wykorzystuje równość referencji. To znaczy — jeśli przekażesz nowy obiekt o tej samej treści, nie będzie on taki sam (ponieważ sam obiekt jest nowy).

state.fields są zwracane z useReducer , co gwarantuje nam tę równość referencji, ale nasza config jest określona inline w naszym komponencie funkcyjnym. Oznacza to, że obiekt jest tworzony ponownie przy każdym renderowaniu, co z kolei wyzwala powyższy useEffect !

Aby rozwiązać ten problem, musimy skorzystać z biblioteki use-deep-compare-effect autorstwa Kent C. Dodds. Instalujesz go za pomocą npm install use-deep-compare-effect i zastępujesz wywołanie useEffect tym. Daje to pewność, że wykonujemy głębokie sprawdzenie równości zamiast sprawdzania równości odwołań.

Twój kod będzie teraz wyglądał tak:

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

Uwaga dotycząca efektu użytkowania

Okazuje się, useEffect to całkiem ciekawa funkcja. Dan Abramov napisał naprawdę ładny, długi artykuł o zawiłościach useEffect , jeśli chcesz dowiedzieć się wszystkiego o tym haczyku.

Teraz wszystko zaczyna wyglądać jak biblioteka walidacyjna!

  • Zobacz demo CodeSandbox

Obsługa przesyłania formularza

Ostatnim elementem naszej podstawowej biblioteki walidacji formularzy jest obsługa tego, co dzieje się po przesłaniu formularza. W tej chwili ponownie ładuje stronę i nic się nie dzieje. To nie jest optymalne. Chcemy zapobiec domyślnemu zachowaniu przeglądarki, jeśli chodzi o formularze, i zamiast tego obsługiwać je samodzielnie. Umieszczamy tę logikę wewnątrz funkcji pobierającej 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 }; };

Zmieniamy naszą funkcję getFormProps , aby zwracała funkcję onSubmit , która jest wyzwalana za każdym razem, gdy wyzwalane jest zdarzenie submit DOM. Zapobiegamy domyślnemu zachowaniu przeglądarki, wysyłamy akcję informującą nasz reduktor, że przesłaliśmy, i wywołujemy podane wywołanie zwrotne onSubmit z całym stanem — jeśli jest podany.

Streszczenie

Byli tam! Stworzyliśmy prostą, użyteczną i całkiem fajną bibliotekę walidacji. Jednak wciąż jest mnóstwo pracy do wykonania, zanim będziemy mogli zdominować interweby.

  • Część 1: Podstawy
  • Część 2: Funkcje
  • Część 3: Doświadczenie