Zapytania HTTP w React - która metoda jest najlepsza?
W tym wpisie postaram się pokazać, jak wykonywać w Reakcie zapytania http do naszego serwera. Artykuł jest podzielony na trzy części. W pierwszej omówię jak do tego podejść bez biblioteki redux czy redux-toolkit. Następnie zademonstruję ten sam przypadek z reduksem, a na koniec przyjdzie redux-toolkit. Do zapytań w dwóch pierwszych przykładach posłużyłem się biblioteką axios.
Zapytanie w czystym Reakcie
Pisząc czysty React, mam na myśli projekt stworzony za pomocą create-react-app bez żadnych dodatkowych bibliotek pokroju redux czy redux-toolkit. Jedyną zewnętrzną paczką, tak jak wspomniałem na początku, będzie axios, który korzysta z XMLHttpRequest, oprócz tego udostępnia nam bardzo przejrzyste i proste w użyciu API.
A więc załóżmy, że mamy jakiś swój backend napisany np. w Expressie i przygotowane odpowiednie endpointy do dodania nowego zadania, pobrania wszystkich lub konkretnego zadania. Jak teraz dobrze to napisać, żeby wraz z rozwojem naszej aplikacji nie zderzyć się ze ścianą? Czy też jak to napisać, aby debugowanie kodu było w miarę jak najprostsze?
Moim zdaniem najlepszym rozwiązaniem będzie zastosowanie wzorca projektowego fasady. A cóż to jest ten wzorzec? Najprościej mówiąc, po prostu wyciągnijmy kod odpowiedzialny za komunikację z naszym backendem do osobnego pliku, stwórzmy tam klasę o nazwie np. TodoService. Dlaczego TodoService? Serwis to miejsce pośredniczące w komunikacji pomiędzy bazą danych a naszymi klasami widokowymi, kontrolerami czy innym miejscem, gdzie potrzebujemy tych danych. I dlatego TodoService - serwis obsługujący zapytania związane z todosami.
Na zdjęciach poniżej zaprezentuję przykładową implementację takiego serwisu.
Jak widzimy, klasa TodoService dziedziczy po Service, która posiada póki co tylko getter z instancją axiosa. Taki zabieg uchroni mnie przed przepisywaniem kodu, który może być później używany w innych serwisach.
Warto nadmienić, że Service korzysta z zaimportowanego obiektu config. Pozwala nam to na przechowywanie kluczowych danych, takich jak np. baseUrl do naszego backendu, w jednym miejscu i w razie potrzeby zmiany url tylko jedna linijka kodu musi zostać zmieniona.
Poniżej zdjęcie pliku konfiguracyjnego:
No dobra, mamy serwis. Co teraz?
Rozpocznijmy od stworzenia nowego komponentu funkcyjnego. Zaimportujmy TodoService, useState i useEffect. Na koniec zwróćmy sobie diva z nagłówkiem.
Prawie gotowe. Teraz wystarczy skorzystać z przygotowanych wcześniej metod w serwisie. Na początek wykorzystajmy useState do stworzenia stanu zadań - todos i funkcji zmieniającej jej stan - setTodos. Następnie w useEffect’cie dodajmy asynchroniczną funkcję wywołującą w środku, na wcześniej zaimportowanym serwisie, metodę getTodos. Oczywiście musimy tutaj wykorzystać słowo await. Poniżej await’a sprawdzamy, czy nie było żadnego błędu, jeżeli wszystko poszło pomyślnie, zwrócone dane ustawiamy w setTodos.
Po pobraniu danych wylistowanie ich to już formalność, ale to też pokażę na zdjęciu poniżej:
Zapytanie za pomocą biblioteki redux
Czym jest dokładnie redux, możecie się dowiedzieć ze wpisu mojego kolegi Arka.
W tym przykładzie wykorzystamy serwis (TodoService), który został napisany wcześniej.
Aby rozpocząć zabawę z reduksem, potrzebujemy kilku paczek: redux, react-redux i redux-thunk. Jak już mamy wszystko pobrane, przejdźmy do konfiguracji naszego store’a.
Jest to najzwyklejszy store reduksowy, z tym że do createStore jako drugi argument przekazujemy funkcję applyMiddleware z podanym w środku thunkiem. Jest to, jak sama nazwa wskazuje, middleware, który pozwala nam wykonywać asynchroniczne akcje.
Uwaga: w prawdziwej aplikacji combineReducers powinniśmy przenieść do osobnego pliku np. w obrębie folderu reducers, w którym będziemy tworzyć nasze reducery.
Przejdźmy do todoReducera, a wygląda on następująco:
Został nam action creator:
Na pierwszy rzut oka nie wygląda to jak typowy action creator. Funkcja, która zwraca funkcję, w której dispatchujemy inną akcję? Tak to właśnie wyglada w thunkach. Dzięki middleware’owi, którego dodaliśmy w storze, redux jest w stanie to wszystko ogarnąć i poprawnie przesłać pobrane dane do reducera.
Na froncie wygląda to następująco:
Tutaj ponownie, żaden “rocket science”. Pobieramy state useSelectorem, a w useEffectcie dispatchujemy nasz action creator i tyle. Możemy wyświetlić todosy lub w przypadku błędu, informację zwrotną.
Zapytanie za pomocą redux-toolkit
Redux-toolkit bardzo ułatwia nam pracę za sprawą createApi. W tym przykładzie najpierw pokażę, jak wygląda finalny kod, a poniżej go opiszę.
Korzystamy tutaj z zaimportowanej funkcji, która jako argument przyjmuje obiekt:
- reducerPath musi być unikalnym kluczem w naszym storze,
- baseQuery jako wartość przyjmuje funkcję fetchBaseQuery, do której ponownie przesyłam obiekt tym razem z konfiguracją zapytania. Najważniejszym jest baseUrl, czyli ścieżka, pod którą będą kierowane zapytania. Oprócz tego możemy przekazać wszystkie dostępne opcje z fetch API,
- tagTypes – tablica z tagami, które później mogą być wykorzystywane w zapytaniach,
- endpoints – tutaj tworzymy potrzebne zapytania pod interesujące nas endpointy. Wyróżniamy dwa rodzaje: query i mutation. Pierwsze służy do pobierania danych, a drugi do edycji danych np. zmiany tytułu już istniejącego todosa lub dodania nowych danych. Żeby móc później wykorzystać tak zadeklarowane operacje, z todosApi musimy wyciągnąć hooki, które tworzymy według pewnego wzorca. Wygląda on następująco: use + klucz + typ operacji. Czyli dla getTodos -> useGetTodosQuery. W zadeklarowanych operacjach można zauważyć pola providesTags i invalidateTags. A działa to w następujący sposób: po dodaniu nowego todo za pomocą useAddTodoMutation, które posiada invalidateTags, zostanie wykonane zapytanie getTasks. Dzięki temu w miejscu, gdzie pobieramy i listujemy wszystkie todosy, lista zostanie sama zaktualizowana i nie będziemy musieli ręcznie tego obsługiwać.
Podsumowanie
Powyżej przedstawiłem kilka sposobów na wykonywanie zapytań w Reakcie. Nie da się ukryć, że redux-toolkit wykonuje dużo roboty za nas. Jednakże uważam, że lepiej wiedzieć, jak w miarę elegancki sposób napisać serwis i odwołać się do niego później w komponencie. A także, jakiej paczki potrzebujemy, aby coś takiego napisać w reduksie.