Śledzenie kursów kryptowalut czyli AJAX w React – PPwRJS #10

Zbliżamy się do zakończenia pracy nad trackerem kryptowalut. Jedyne, czego brakuje do zrealizowania założeń, to wprowadzenie aktualnych danych o kursach z zewnętrznego serwera. Do realizacji tego celu posłuży nam AJAX oraz API CoinMarketCap. React jest biblioteką skoncentrowana na View, stąd na próżno szukać wbudowanego rozwiązania do obsługi asynchronicznego JSa. Otwiera to przed nami szereg możliwości, co ma tyle samo wad (co wybrać?!), co zalet (to co Ci odpowiada! ;)). Zacznę od omówienia dwóch najpopularniejszych sposobów na AJAX w React czyli natywnego fetch i biblioteki axios. Następnie przejdziemy do dopieszczania trackera kryptowalut świeżymi danymi wprost z serwerownii.

Co mają do zaoferowania przeglądarki?

fetch to następca przestarzałego XMLHttpRequest (nazwa zdradza jego archaiczne korzenie). fetch jest oparty o promise’y, co stanowi zdecydowany krok w przód jeżeli chodzi o łatwość obsługi zapytań asynchronicznych. Dostęp do interfejsu uzyskujemy za pomocą globalnej metody fetch(). Twórcy specyfikacji fetch postarali się o zachowanie możliwie największej liczby podobieństw z XHR. Dzięki temu będzie wydawał się on znajomy każdemu, kto miał (nie)przyjemność korzystania z poprzednika fetch. Czy była to trafiona decyzja? Tego dowiemy się w dalszej części wpisu (spoiler: NIE!).

fetch wymaga jednego argumentu do wykonania zapytania. Jest nim URL zasobu, który chcemy odpytać.

fetch zwraca promise’a, który wywiązuje się do obiektu Response, niezależnie od tego, czy zapytanie było pomyślne. To pierwszy problem fetch. Korzystałem z $.ajax zaledwie przez rok, a ciężko mi przestawić się na to, że błąd z punktu widzenia programisty (404 lub 500), jest pozytywnym rezultatem dla interfejsu (przecież serwer udzielił odpowiedzi, po co drążyć temat!). W związku z tą pokrętną logiką, reject jest zarezerowany wyłącznie dla problemów z siecią lub blokadą CORS po stronie serwera.

Zobaczmy, jak wspomniany problem wygląda w praktyce.

Przykład domyślnej obsługi błędów przez interfejs fetch

Zonk, serwer zwraca 404, ale catch nie przechwytuje błędu, fetch przechodzi do then, udostępniając nam obiekt Response.

Musimy rozbudować nasze procedury obsługi, żeby dowiedzieć się, z czym tak naprawdę mamy do czynienia. Sprowadza się to do testowania response.ok, która jest ustawiana na false, dla kodów statusu spoza zakresu 200-299.

Przykład rozpoznawania statusu odpowiedzi w fetch

W mniej akademickim przykładzie test response.ok deleguje się do funkcji, którą zawsze przekazujemy do pierwszego then.

Przykład delegacji obsługi błędów do funkcji handleErrors

Dobra, teraz powinno pójść z górki. Suprise! Jeżeli oczekujesz, że jedyne, czego zabrakło w poprzednim przykładzie, to obsługa danych zwróconych przez API, to muszę Cię rozczarować. response ukrywa jsona, musimy się do niego dobrać przez wywołanie response.json().

Pełen przykład podstawowego zapytania z fetch

Jak widzisz, fetch wymaga naprodukowania kilkunastu linijek kodu dla najbardziej podstawowego przykładu użycia. W każdym zapytaniu będziemy musieli przechodzić przez ten sam proces przygotowawczy.

Jak większość interfejsów AJAX, fetch opcjonalnie przyjmuje drugi parametr. To obiekt konfigurujący zapytanie. Jeżeli pominiemy ten parametr, fetch skorzysta z obiektu domyślnego.

Domyślny obiekt konfiguracyjny fetch

Wsparcie fetch w przeglądarkach nie odbiega od pozostałych funkcjonalności wprowadzonych w ES6. Jeżeli zależy nam na IE, nie obejdzie się bez polyfilla dla promise’ów. W przypadku projektów pisanych w Reactcie nie jest to żadna przeszkodza, i tak korzystamy z rozbudowanej konfiguracji babela i webpacka ;).

Jak pewnie zauważyłeś, nie jestem wielkim fanem fetch. Nie zmienia to faktu, że gdybym był zmuszony do wyboru natywnego rozwiązania, bez wahania chwyciłbym za fetch. Na tle XMLHttpRequest wypada świetnie.

Na szczęście nie jestem zmuszony do takiego wyboru, więc przejdźmy do mojej ulubionej alternatywy.

I <3 open-source

axios to klient HTTP, który tak jak fetch, jest oparty o obietnice. Cieszy się niesamowitą popularnością, na co dowodem jest 41,5 tysiąca gwiazdek na GH.

Co sprawia, że programiści tak chętnie korzystają z tej zależności? Nadaje się zarówno do prostych aplikacji w czystym js, programów node.js oraz pracy z frameworkami front-endowymi takimi jak React. axios oferuje niewiele więcej, niż fetch. Sęk w tym, że robi to w lepszym stylu. Zdecydowanie bliżej do skojarzeń z $.ajax(), niż XMLHttpRequest. Jeżeli chodzi o dodatki, mamy do dyspozycji kilka użytecznych funkcjonalności: stałe konfiguracje, instancje, transformacje, interceptory. O tym za chwilę, najpierw zróbmy praktyczne porównanie axios z fetch.

Porównanie przykładowego zapytania z fetch i axios

Dwa powyższe warianty przynoszą dokładnie takie same efekty. Różnica w objętości mówi sama za siebie.

Pozbyliśmy się pośredniego kroku z fetch, mamy natychmiastowy dostęp do jsona. Skorzystałem z destrukturyzacji ({data}), żeby oszczędzić sobie referowania response.data. Kolejne usprawnienie to intuicyjna obsługa błędów. Odpowiedź od serwera z kodem statusu spoza zakresu 200-299 jest automatycznie kategoryzowane jako błąd i trafia do catch.

Jeżeli wcześniej uważałeś, że przesadnie krytykuję fetch, to teraz wiesz, że to zasługa rozpieszczenia przez axiosa ;).

Aby w pełni uzasadnić obciążanie aplikacji dodatkowymi kilobajtami, przyjrzyjmy się dodatkowym funkcjonalnościom, o których wspominałem wcześniej.

Stała konfiguracja i instancje

W wielu aplikacjach wielokrotnie wykonujemy zapytania pod ten sam url. Tak samo będzie w trackerze kryptowalut, gdzie interesuje nas wyłącznie adres: https://api.coinmarketcap.com/v2/. axios.defaults pozwala na ustawienie baseURL, który będzie używany w wszystkich zapytaniach. W zapytaniach wystarczy dodać interesującą nas ścieżkę.

Stała konfiguracja axios

Działa to na tyle fajnie, że jeżeli przekażemy stringa rozpoczynającego się od ‚http’, to axios zda sobie sprawę, że rezygnujemy z baseURL.
Możliwości axios.defaults nie ograniczają się do url, mamy możliwość ustawienia wartości domyślnej dla dowolnej właściwości konfiguracji (headery, timeout etc.).

A co jeśli mamy dwa (lub więcej) adresów, z których chcemy korzystać zamiennie? Tutaj na pomoc przychodzą instancje.

Tworzenie instancji axios

Transformatory i interceptory

Transformatory pozwalają modyfikować dane żądania/odpowiedzi przed ich wysłaniem/przekazaniem do metod obsługi. Interceptory działają podobnie, ale pozwalają również na modyfikowanie obiektu konfiguracyjnego.

Praktyczne zastosowanie? Modyfikacja konfiguracji w oparciu o dane zwrócone z innej operacji asynchronicznej, wygenerowanie unikalnego tokena i przypisanie go do headera.

Dobrym miejscem na ustawienie interceptora jest componentDidMount() (jeżeli nie wiesz dlaczego, zerknij tutaj). Musimy pamiętać o przypisaniu go do właściwości komponentu, żeby móc po nim posprzątać w componentWillUnmount().

Tracker kryptowalut to prosta aplikacja. Nie będzie potrzeby korzystania z tych funkcjonalności, więc z czystym sumieniem zostawiam Was z przykładem z dokumentacji.

Przykład zastosowania interceptorów z dokumentacji axios

Dawać te dane!

Skoro już zapoznaliśmy się z axiosem, to można zabrać się zastąpienie czterech kryptowalut wklepanych przeze mnie na sztywno do state czymś bardziej użytecznym. Nasze prace skupią się na komponencie kontenerowym App. Jako punkt wyjścia potraktujmy ostatni commit z poprzedniego wpisu.

Na początek pozbędziemy się zawartości state.cryptos, już nie będzie nam potrzebna.

Stan App z pustą właściwością cryptos

Zanim przejdziemy dalej, przydałaby się instancja axios powiązana z API CoinMarketCap.

Deklaracja instancji CoinMarketCap

Okej, jesteśmy gotowi na get(), który odmieni oblicze tej aplikacji. Jeżeli odrobiłeś zadanie domowe z metod cyklu życia, to pewnie słusznie spodziewasz się, że skorzystamy z componentDidMount(). Wykonam zapytanie do ścieżki, która zwróci 100 najwyżej notowanych kryptowalut.

Kod metody componentDidMount komponentu App

CoinMarketCap API zwraca zagnieżdżony obiekt data, którego klucze to ID każdej kryptowaluty. Komponent CoinList oczekuje tablicy, zbudowałem ją za pomocą Object.keys() i map. Wynikiem każdego wywołania callbacka map jest dodanie do nowej tablicy obiektu kryptowaluty. Dzięki destrukturyzacji ma on taką samą formę co elementy state.cryptos, z których korzystałem wcześniej.

Natrafiłem na mały problem z ikonami kryptowalut, nie sposób przechowywać lokalnie każdej z nich. Na szczęście mogę skorzystać z zasobów CoinMarketCap. Po krótkiej analizie strony głównej zauważyłem, że ikony są przechowywane pod adresem: https://s2.coinmarketcap.com/static/img/coins/64x64/${id}.png.

W takim wypadku nie pozostało nic innego jak mała przebudowa metody getIconPath.

Kod metody getIconPath generującej url ikon

To wszystko czego tak naprawdę nam potrzeba, mamy tracker kryptowalut z prawdziwego zdarzenia. Możesz sprawdzić demo postawione na GitHub Pages, które udało mi się z łatwością postawić dzięki poradnikowi na type-of-web.

Na wersję demo składa się jeszcze kilka commitów, w których:

  1. Poprawiłem formatowanie wartości walut (commit a544a51 oraz c0a8e0d)
  2. Przeniosłem logikę mapowania kryptowalut do modułu shared/helpers.js (commit 29232be)
  3. Dodałem spinner na czas ładowania walut z API (commit 69176b4

Twoja kolej

Zostało kilka rzeczy do zrobienia. Tutaj wkraczasz Ty, mój drogi czytelniku. To świetna okazja do przećwiczenia wiedzy, którą dzieliłem się z Tobą w ostatnich dziesięciu wpisach z serii „Pierwszy projekt w ReactJS”.

Lista zadań, z którymi możesz się rozprawić:

  • Opracuj obsługę błędów na wypadek gdyby odpowiedź z CoinMarketCap nie dotarła do crypto-trackera
  • Obsłuż aktualizowanie kursów co pięć minut
  • Pobierz dane z API o całym kapitale na giełdzie i przekaż je do komponentu Header
  • Obsłuż sytuację, w której użytkownik chce uzyskać dane kryptowaluty spoza top-100
  • Posortuj listę kryptowalut według rankingu

Jest co robić. Forkuj repo, czekam na Twojego pull requesta :). Jeżeli ktoś (np. ja ]:->) Cię uprzedzi, to nic straconego, po prostu podziel się swoim rozwiązaniem w komentarzu :).

Podsumowanie

Panie i Panowie, oficjalnie dojechaliśmy do mety drugiej serii na blogu. Nie wiem jak Wy, ja bawiłem się świetnie. Wzrosła nie tylko ilość wpisów, ale i czytelników bloga. Podwoiła się liczba polubień na fb oraz pojawiło się kilka naprawdę miłych komentarzy. Za każdy ukłon w moją stronę, z wdzięcznością kłaniam się dwa razy. Dziękuję za wszystkie udostępnienia na fb, z wyróżnieniem dla strony Frontem – front-endowe ciekawostki. Kimkolwiek jest jej administrator, jego/jej uprzejmość pozwoliła mi dotrzeć do wielu z Was :).

W obliczu takich „sukcesów” nie pozostało mi nic innego jak iść za ciosem. Tydzień temu na blogu pojawiła się ankieta, która miała zadecydować o temacie kolejnej serii:
Wyniki ankiety odnośnie tematyki kolejnych wpisów - zwycięzca: Testy
Teehee, mamy remis. Z tego powodu wstrzymuję się z podjęciem decyzji do jutra. Dam okazję na oddanie głosów osobom, które jeszcze tego nie zrobiły. O ostatecznej decyzji poinformuję Was na FB i Twitterze.

Kod źródłowy projektu jest dostępny tutaj.

Podobał Ci się dzisiejszy artykuł? Udostępnij go w Twoich ulubionych mediach społecznościowych. Może znajdzie się ktoś, dla kogo również będzie wartościowy.

Bądź na bieżąco. Wystarczy polubić fanpage AlgoSmart na fejsie, obserwować mój profil na Twitterze i regularnie odwiedzać portal Polski Front-end.

PS. Daj znać jak podobają Ci się snippety kodu wygenerowane przez carbon.

Do zobaczenia za tydzień :).

Marcin Czarkowski

Cześć! Ja nazywam się Marcin Czarkowski, a to jest AlgoSmart - blog, na którym dzielę się wiedzą o ReactJS oraz JavaScript. Tworzę poradniki na YouTube i jestem współtwórcą przeprogramowani.pl

  • Siemanko, prowadzisz bardzo dobry blog, dlatego mam odwagę poprosić, o wyjaśnienie współegzystencji stanu , „query”(search) i parametru „limit”, „offset” czyli paginacji. Jak świat szeroki, znalazłem ledwie dwa źródła, z objaśnieniem jednak wydają mi się one nie najlepszymi rozwiązaniami, jestem ciekawy jak Ty rozwiązałbyś taką problematykę API.

    • Lukas – jesteś w stanie się do mnie odezwać na FB? Chciałbym lepiej zrozumieć Twój problem, żeby udzielić Ci sensownej odpowiedzi.

      PS. Wybacz delay w odpowiedzi ale w natłoku pracy rzadziej tutaj zaglądam ostatnimi czasy 😉

  • OneReaderToRuleThem