Formularze z React hooks, czyli o co tyle huku?
React jest jedną z najpopularniejszych bibliotek do tworzenia aplikacji internetowych. Razem z React Native pozwala tworzyć kompletne rozwiązania klienckie obejmujące aplikację webową oraz natywne programy na telefony z systemem iOS oraz Android. React nieustannie ewoluuje i regularnie otrzymuje aktualizacje. Czasami są to niewielkie poprawki, a czasem zmiany, które zmieniają sposób pisania całych aplikacji.
CZYM SĄ HOOKI?
Przykładem takiej aktualizacji jest wersja 16.8.0 z lutego tego roku, która oficjalnie wprowadza hooki, czyli funkcje pozwalające wykorzystywać stan oraz składowe cyklu życia komponentu w funkcyjnych komponentach. Oznacza to, że logikę związaną ze stanem i cyklem życia komponentu można z niego wyodrębnić i dzielić między różnymi komponentami. Oficjalna dokumentacja Reacta dzieli hooki na podstawowe oraz zaawansowane. W tym poście skupię się na jednym z podstawowych hooków, a mianowicie useState. Do tej pory główną motywacją do używania klasowych komponentów była możliwość korzystania ze stanu. Hook useState pozwala korzystać z niego bez konieczności tworzenia klasy.
TWORZENIE FORMULARZY
Niemal w każdej aplikacji występują mniej lub bardziej skomplikowane formularze i ich obsługa jest często powtarzającym się zadaniem. W tym wpisie stworze dwa formularze logowania. Pierwszy będzie prezentował podejście wykorzystujące klasowy komponent. Przy drugim formularzu przedstawię usprawnienia, jakie wprowadza nowa aktualizacja.
PODEJŚCIE WYKORZYSTUJĄCE KLASĘ
Do tej tworząc formularz, korzystało się zazwyczaj z klasowych komponentów, w których deklarowano stan przetrzymujący wartości pól tekstowych oraz funkcje aktualizującą ten stan.
class FormWithoutHooks extends React.Component { state = { email: "", password: "" }; handleFormInputChange = e => { this.setState({ [e.target.name]: e.target.value }); }; render() { const { email, password } = this.state; return ( <form> <input name="email" value={email} onChange={this.handleFormInputChange} type="email" placeholder="np. example@gmail.com" /> <input name="password" value={password} onChange={this.handleFormInputChange} type="password" placeholder="*********" /> <input type="submit" value="Dodaj" /> </form> ); } }
W linijkach 2-5 deklarujemy początkowy stan pól tekstowych. Linijki 7-11 zawierają funkcję aktualizującą wartości tych pól na podstawie eventu. Funkcja render zwraca dwa pola tekstowe oraz przycisk do wysłania formularza. Rozwiązania to jest jak najbardziej poprawne i stosowane w większości aplikacji, więc po co hooki?
HOOK USESTATE W PRAKTYCE
Pozwólcie, że teraz przedstawię, jak identyczny formularz można stworzyć, wykorzystując funkcyjny komponent oraz hooki.
const FormWithHooks = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); return ( <form> <input type="email" placeholder="np. example@gmail.com" value={email} onChange={e => setEmail(e.target.value)} /> <input type="password" label="hasło" placeholder="*********" value={password} onChange={e => setPassword(e.target.value)} /> <input type="button" value="Dodaj" /> </form> ); };
Funkcja useState (linijka 2-3) przyjmuje jeden parametr będący wartością domyślną stanu. Zwraca parę wartości: bieżący stan oraz funkcję, która go aktualizuje. Wykorzystuje się przy tym destrukturyzacje tablicy. Zapis:
const [email, setEmail] = useState("");
Jest równoważny z:
const emailArray = useState(""); const email = emailArray[0]; const setEmail = emailArray[1];
Następnie wartość oraz funkcję aktualizująca przekazujemy jako właściwości do pól tekstowych. Rozwiązanie to wydaje się trochę bardziej przejrzyste w porównaniu do przedstawionego wcześniej, ale nie tutaj szukałbym przewagi hooka useState nad klasycznym stanem komponentów.
TWORZENIE WŁASNYCH HOOKÓW
Pamiętacie, jak na początku wpisu wspomniałem, że istnieje możliwość wyodrębnienia logiki stanowej z komponentu. Można to wykorzystać przy obsłudze pól tekstowych, w których aktualizacja stanu wygląda zawsze tak samo. W tym celu stworzę własnego hooka useFormInput. Skąd taka nazwa? Z przyjętej konwencji, że nazwy funkcji wykorzystujące hooki prefiksowane są słowem use.
const useFormInput = initialValue => { const [value, setValue] = useState(initialValue); const onChange = e => { setValue(e.target.value); }; return { value, onChange }; };
Funkcja useFormInput przyjmuję jeden parametr określający wartość początkową, a zwraca bieżącą wartość pola tekstowego oraz funkcję aktualizującą to pole. Nazwy tych elementów nie są przypadkowe, odpowiadają one nazwom właściwości elementu input.
Teraz pozostaje zastosowanie hooka w komponencie z formularzem.
const FormWithHooks = () => { const emailInput = useFormInput(""); const passwordInput = useFormInput(""); return ( <form> <input type="email" placeholder="np. example@gmail.com" {...emailInput} /> <input type="password" placeholder="*********" {...passwordInput} /> <input type="button" value="Dodaj" /> </form> ); };
Zapis:
{...emailInput}
jest równoważny z zapisem:
value={emailInput.value} onChange={emailInput.onChange}
Czy widzicie już przewagę stosowania hooków przy formularzach? Jeżeli nie, to zwróćcie uwagę, że useFormInput jest zwykłą funkcją niezwiązaną z żadnym komponentem.
Oznacza to, że raz napisaną logikę można użyć w dowolnym komponencie w aplikacji. W podejściu klasowym jest to niemożliwe, ponieważ logika stanowa jest ściśle związana z danym komponentem. Wyobraźmy sobie sytuację, nie tak niespotykaną, że należy dodać formularz do rejestracji użytkownika. W podejściu klasowym należałoby od nowa napisać (przekopiować) funkcję handleFormInputChange do komponentu rejestracyjnego. W przypadku korzystania z hooków nie musimy tego robić, wystarczy zaimportować funkcję useFormInput. Takie podejście w znacznym stopniu wpływa na szybkość pisania, przejrzystość kodu oraz łatwość jego utrzymywania.
PODSUMOWANIE
React nieustannie się rozwija i wprowadza nowe rozwiązania zwiększające efektywność pisania kodu. W tym poście przedstawiłem tylko jeden z kilku wprowadzonych do Reacta hooków. Mimo to mam nadzieję, że post oddaje główną ideę ich wprowadzania. Hooki znacznym stopniu rozwiązują problem współdzielenia logiki między komponentami. Pozwalają również zrezygnować z klas, które nie są tak łatwe w testowaniu oraz minifikacji, jak funkcje.
Autor: Maciej Budziński