Jeżeli szukasz biblioteki, która pozwoli Ci zachować formularze proste i czytelne, nie powodując dużego narzutu na kod i nie narzucając zbyt wielu własnych rozwiązań – a jednocześnie dającej duże możliwości zarządzania stanem formularza, walidacją itd to react-hook-form jest rozwiązaniem stworzonym dla Ciebie.
Czytelne i proste API oparte na hookach, dedykowane narzędzie do debugowania powodują, że react-hook-form jest rozwiązaniem pierwszego wyboru do zarządzania formularzami.
Używamy hooka useForm, z którego pobieramy 2 funkcje – handleSubmit, która przyjmuje funkcję mającą odpalić się na onSubmit i wstrzykuje do niej dane z formularza, oraz funkcję register która służy do rejestrowania pola w formularzu pod nazwą przekazaną polu w parametrze name. Nie musimy sami przetrzymywać i aktualizować stanu formularza, zrobi to za nas biblioteka.
Walidacja
Prosta walidacja
Jeżeli chcemy oznaczyć pole jako wymagane, zamiast przekazywać do ref funkcję jako parametr, możemy wywołać ją z obiektem parametrów {required: true}:
Spowoduje to, że przy pustym polu formularz nie będzie uruchamiał funkcji onSubmit. Jeżeli chcemy poinformować użytkownika o błędzie, z hooka useForm możemy pobrać obiekt errors który będzie posiadał pole userName zawierające obiekt błędu:
Błędy rozróżniamy po typie – dodatkowo musimy zabezpieczyć się przed sytuacją, gdy błędu nie ma i obiekt errors jest pusty. Można napisać kod w starym stylu:
{errors.userName && errors.userName.type === 'required' && <span>Field is required</span>}
Osobiście zachęcam Cię jednak do używania Optional Chaining Operator, który jest w standardzie EcmaScript od wersji 2020.
Customowa walidacja
Możemy również utworzyć własną funkcję walidującą i przekazać ją do kolejnego pola w formularzu:
const isEven = value => {
const number = Number(value);
return !Number.isNaN(number) && !(number % 2);
};
Funkcja ta sprawdza, czy wartość wpisana w polu jest liczbą i czy jest parzysta. Możemy łączyć walidacje natywne dla biblioteki i nasze własne funkcje walidujące:
Rezultat naszej własnej walidacji ma typ validate. Może Cię zastanawiać czemu użyłem tak skomplikowanego zestawu parametrów dla inputa oraz rzutowania wewnątrz funkcji walidującej, zamiast typu number. Otóż okazuje się, że ten typ ma problemy na urządzeniach mobilnych.
Asynchroniczna walidacja
Walidować możemy również w asynchroniczny, gdzie funkcja validate będzie zwracać Promise resolvujący się z true lub false. Dodajmy pole pseudo captcha:
Zapytanie na serwer zostało zasymulowane poprzez Promise i setTimeout:
const sendRequest = value => new Promise((resolve, reject) => { setTimeout(() => { const valid = value === 'test'; resolve(valid) }, 5000) })
Efekt sumaryczny jest następujący:
Użytkownik wpisuje złe dane, np test2
Klika „Send”
Po 5 sekundach widzi komunikat, że wpisał złe dane
Jednocześnie, dzięki temu że możemy łączyć walidacje customowe z dostarczanymi przez bibliotekę, z powodu oznaczenia pola jako required jeżeli użytkownik nie wpisze nic informację o tym by coś wpisać dostanie od razu.
Wiadomości dla błędów walidacji
Do tej pory wiadomości dla błędów walidacji renderowaliśmy za pomocą conditional render. Treść wiadomości można jednak zdefiniować już przy rejestrowaniu inputu. Zamiast samej wartości walidacji, przekazać należy obiekt z polami value i message:
register({required: true, minLength: 3});
// Poniższy zapis jest równoważny:
register({
required: {value: true, message: 'Type something'},
minLength: {value: 3, message: 'Type at least 3 characters'},
})
Dodajmy do formularza pole zawierające taką walidację:
Pierwszą zaletą jest to, że kodu jest mniej i stał się czystszy. Drugą – że takie obiekty walidacji można definiować jako stałe i je reużywać, a nawet pobierać backendu, gdzie np adminstrator aplikacji będzie je definiował w CMS.
Obsługa stanu formularza – obiekt formState
Z wywołania hooka useForm możemy pobrać również obiekt formState:
Obiekt formState udostępnia pole isSubmitting, którego możemy użyć do poinformowania użytkownika o tym, że trwa wysyłanie formularza i do zablokowania przycisku:
Aby zasygnalizować użytkownikowi pola które zmodyfikował, możemy użyć polaformState.dirtyFields które jest Setem zawierającym zmodyfikowane pola. Jeżeli do pola userName dodamy:
Napiszmy prostą galerię obrazków, do której będzie można dodawać obrazy tylko wtedy, gdy obraz nie przekroczy określonych wymiarów.
Obsługa tablic i obiektów jako pól formularza
Bilbioteka react-hook-form potrafi zarządzać tablicami jako elementami formularza. Jeżeli do pola name inputa dodamy numer w nawiasach kwadratowych, będzie to oznaczać element tablicy, np inputy
Analogicznie błędy dla poszczególnych obrazków będą w errors.images[i].name i errors.images[i].file.
Walidacja rozmiarów obrazka
Ponieważ będziemy chcieli walidować obrazek pod kątem rozmiaru, musimy napisać asynchroniczną funkcję która przetworzy plik na obrazek, załaduje go a następnie sprawdzi rozmiary. Może wyglądać na przykład tak:
const validateImage = async files => new Promise(resolve => {
const url = URL.createObjectURL(files[0])
const image = new Image()
image.onload = () => {
const valid = image.width <= 1000 && image.height <= 1000;
URL.revokeObjectURL(url)
resolve(valid? true : 'Image size is at most 1000 x 1000')
}
image.src = url;
})
Nie ma w tym momencie możliwości zadeklarowania wiadomości dla błędu walidacji customowej funkcji podczas rejestracji pola, stąd korzystamy z drugiej możliwości – zwrócenia tekstu błędu zamiast wartości boolean. Zachowanie jest następujące – jeżeli zwrotka to true, błędu nie ma. Jeżeli string, błąd jest a tekst wiadomości ma wartość tego stringa.
Pamiętamy również o tym, by po użyciu usunąć obrazek za pomocą URL.revokeObjectURL, ponieważ obrazki których adresy są utworzone przez URL.createObjectURL są przetrzymywane przez przeglądarkę do zamknięcia strony – a szkoda zajmować pamięć niepotrzebnie.
Pełny kod pierwszej wersji
Kod nadaje się do skopiowania i uruchomienia. Jest dość spory, stąd jest ukryty.
Z obecną wersją jest kilka problemów – pliki trzymane są w dziwnej formie, jako lista plików. Dodatkowo, nie da się trzymać w formularzu naszych własnych wartości – przykładowo rozmiarów obrazka a ewidentnie byłoby to przydatne. Z pomocą przychodzi nam definiowanie customowych pól formularza poprzez wywołanie register ręcznie.
Pole rejestrujemy wewnątrz metody useEffect:
useEffect(() => { register({name: `images[${i}]`}, { required: {value: true, message: 'Provide name and file'}, validate: validateImage, }) // did mount - empty array on purpose // eslint-disable-next-line }, [])
Ponieważ musimy mieć pewność że pole zarejestruje się tylko raz, przekazujemy pustą tablicę jako drugi parametr. Dobrze też wyłączyć dla tej linii walidację ESLint, ponieważ w konsoli wyświetlane są ostrzeżenia.
Stąd walidacja required sprawdzi tylko czy zamiast obiektu nie ma wartości null bądź undefined. Dlatego obsługa sytuacji braku nazwy pliku bądź samego pliku musi zostać rozwiązana w customowej funkcji walidującej:
const validateField = async el => new Promise(resolve => {
if (!el?.name) {
resolve('Provide name');
}
if (!el?.file) {
resolve('Provide file');
}
const url = URL.createObjectURL(el.file)
const image = new Image()
image.onload = () => {
const valid = image.width <= 1000 && image.height <= 1000;
URL.revokeObjectURL(url);
resolve(valid? true : 'Image size is at most 1000 x 1000');
}
image.src = url;
})
Jak pamiętasz, zwrócenie stringa oznacza błąd z wiadomością będącą tym stringiem.
Również ręcznie ustawiać należy wartość tego obiektu:
Kod ma aż 130 linii, dzięki temu jednak dobrze pokazuje możliwości biblioteki react-hook-form, oraz nadaje się do uruchomienia po przekopiowaniu do aplikacji z zainstalowanymi zależnościami:
Pokaż pełny kod
import React, {useEffect, useState} from "react"; import { useForm } from "react-hook-form";
const validateField = async el => new Promise(resolve => { if (!el?.name) { resolve('Provide name'); } if (!el?.file) { resolve('Provide file'); } const url = URL.createObjectURL(el.file); const image = new Image(); image.onload = () => { const valid = image.width <= 1000 && image.height <= 1000; URL.revokeObjectURL(url); resolve(valid? true : 'Image size is at most 1000 x 1000') } image.src = url; })
Podpięcie jest bardzo proste, najpierw należy z hooka zaimportować kontroler:
const { register, handleSubmit, errors, setValue, control } = useForm();
A następnie gdzieś wstawić komponent DevTools:
<DevTool control={control} />
Po prawej stronie pojawi się komponent zawierający dane z naszego formularza:
Wyświetlanie DevTools tylko podczas developmentu
Nie chcemy oczywiście, by docelowa aplikacja na produkcji miała takie elementy. Jeżeli nasza aplikacja została stworzona za pomocą CRA, skrypty same troszczą się o to czy wersja jest developerska czy produkcyjna. Jeżeli chcemy to sprawdzić, mamy wystawioną zmienną process.env.NODE_ENV. Jeżeli jej wartość to development, komponent jest widoczny:
Opisanie wszystkich zaawansowanych możliwości biblioteki react-hook-form wykracza daleko poza ramy tego posta. Biblioteka dostarcza chociażby zaawansowane hooki do obsługi tablic pól, pozwalające zapanować nad wydajnością. Po przeczytaniu tego wpisu i przeanalizowaniu kodu masz doskonałe przygotowanie pod rozpoczęcie używania tej biblioteki w codziennej pracy.
3 komentarze do “Biblioteki do formularzy w React #2 – react-hook-form”
3 komentarze do “Biblioteki do formularzy w React #2 – react-hook-form”
Możliwość komentowania została wyłączona.