Frontend, Mobile

Jak wykorzystać Hooks na przykładzie warstwy odpowiedzialnej za komunikację z backendem

Podczas React Conf 2018 została zaprezentowana nowa funkcjonalność Reacta — Hooks. Jest to nowe podejście do zarządzania stanem i współdzielenia funkcjonalność między komponentami. Ma na celu ułatwić przenoszenie logiki wymagającej użycia stanu oraz ograniczyć liczbę komponentów w aplikacji. Możliwe, że z czasem Hooks wyprą popularne aktualnie High Order Components (HOC) i render propsy.

Tomasz Wierzchowski. Frontend Architect w IPFDigital, absolwent Politechniki Warszawskiej. Entuzjasta JavaScriptu i Reacta. Brał udział w wielu projektach komercyjnych, specjalizuje się w tworzeniu aplikacji związanych z branżą finansową. Fan rozwiązań serverless i rozsądnego podejścia do nowinek technicznych.


Warto zaznaczyć, że aktualnie całe omawiane API jest jeszcze propozycją i może się gruntownie zmienić. Jednak biorąc pod uwagę bardzo ciepłe przyjęcie w środowisku programistów związanych z React, warto zapoznać się z nim już teraz. Więcej informacji o samym Hooks API znajdziecie tutaj.

Powstało już wiele materiałów o tym czym są Hooki, jak działają, jak można ich użyć, czy jak stworzyć własny Hook. Dlatego w tym artykule chciałbym się skupić na praktycznym zastosowaniu nowego API na przykładzie warstwy odpowiedzialnej za komunikację z backendem.

Nie chcąc komplikować całego zagadnienia stawiam przed rozwiązaniem kilka podstawowych wymagań:

  • Przechowywanie stanu (pobrane dane, stan zapytań, informacje o ewentualnych błędach)
  • Dostęp do tego stanu
  • Możliwość wykonywania zapytań w całej aplikacji

useState

Zacznijmy od implementacji wykorzystującej lokalny stan komponentu. Do przechowywania stanu i danych wykorzystamy hook useState. Nasz hook będzie wykonywany przy każdym renderze. Dlatego potrzebujemy użyć jeszcze funkcji useMemo, aby wykonywać zapytanie tylko przy zmianie parametrów.

Nazwijmy nasz hook useApiFetch. Stwórzmy funkcję przyjmującą dwa parametry. Pierwszy to funkcja wykonująca zapytanie, która zwraca Promise. Drugi to parametry dla przekazanej funkcji. Otrzymujemy coś takiego:

import {
  useState,
  useMemo,
} from 'react';


export default (fetchAction, params = []) => {

};

Dodajmy kod odpowiedzialny za przechowywanie stanu korzystając z hooka useState:

import {
  useState,
  useMemo,
} from 'react';


export default (fetchAction, params = []) => {
  const [isPending, setPending] = useState(false);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
};

Użyty hook zwraca zmienną zawierającą aktualny stan oraz funkcję pozwalającą na zmianę wartości stanu. W ten sposób możemy przechować informacje o statusie i wyniku żądania. Pozostało już tylko wykonać zapytanie i zwrócić dane. Najpierw potrzebujemy zmienić stan na w trakcie (isPending), wywołać przekazaną funkcję wraz z parametrami i obsłużyć odpowiedź. Całość można zapisać jako:

setPending(true);

fetchAction(...params)
  .then((resp) => {
    setData(resp);
    setError(null);
    setPending(false);
  })
  .catch((err) => {
    setError(err);
    setData(null);
    setPending(false);
  });

W tym miejscu pojawia się problem. Nasz hook jest wykonywany przy każdym renderze, razem z zapytaniem. Chcemy, aby zapytanie było wykonane, tylko kiedy zmienią się parametry. Hook useMemo umożliwia nam wykonanie funkcji tylko kiedy zmienią się jej parametry. Wystarczy owinąć nasz kod w arrow function i przekazać go do wspomnianego hooka. Od razu dodajmy zwrócenie danych i mamy gotową funkcję:

import {
  useState,
  useMemo,
} from 'react';


export default (fetchAction, params = []) => {
  const [isPending, setPending] = useState(false);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useMemo(() => {
    setPending(true);

    fetchAction(...params)
      .then((resp) => {
        setData(resp);
        setError(null);
        setPending(false);
      })
      .catch((err) => {
        setError(err);
        setData(null);
        setPending(false);
      });
  }, params);

  return [
    data,
    error,
    {
      isPending,
      isSuccess: !isPending && !!data,
      isError: !isPending && !!error,
    },
  ];
};

Pora wykorzystać nasz hook w przykładowym komponencie. Mamy do dyspozycji funkcję pobierającą informację o ilości lajków danego obiektu i musimy te dane wyświetlić. Potrzebujemy stworzyć komponent, który będzie korzystał z naszego hooka. Id obiektu będzie dostarczane jako props. Nazwijmy nasz komponent <LikesCount> i w czasie oczekiwania na dane pokażmy użytkownikowi trzy kropki:

import React from 'react';
import PropTypes from 'prop-types';

import fetchLikesCount from 'api/fetchLikesCount';
import useApiFetch from 'hooks/useApiFetch';


export default function LikeCount({
  objectId,
}) {
  const [data, error, {
    isPending,
    isSuccess,
  }] = useApiFetch(fetchLikesCount, [objectId]);

  return (
    <p>
      {isPending && '...'}
      {isSuccess && data.likesCount}
    </p>
  );
}

LikeCount.propTypes = {
  objectId: PropTypes.number.isRequired,
};

W ten sposób szybko stworzyliśmy komponent, który łączy się z naszym backendem i wyświetla wynik użytkownikowi. Jednocześnie logikę pobierania możemy łatwo przenieść do innych komponentów razem z obsługą stanu. Nie potrzebujemy tworzyć dodatkowych komponentów w drzewie VDOM, aby współdzielić logikę między komponentami. Na pewno nie wystąpi konflikt nazw propsów. Dodatkowo cały kod jest bardzo czytelny i łatwo wywnioskować co się w nim dzieje. Po prostu wywołujemy funkcję, która pobiera nam dane. A wszystko działa bez problemu i prosto z pudełka.

Niestety zaproponowane rozwiązanie ma swoje minusy. Nie jesteśmy w stanie pobrać danych raz dla kilku komponentów, czy łatwo przekazać danych w dół drzewa. Obydwa problemy można częściowo rozwiązać za pomocą Context API. Jednak korzystanie z wielu contextów w aplikacji często prowadzi do powstania nieczytelnego, skomplikowanego kodu. Na szczęście możemy użyć reduxa wraz z hooksami.

Redux + Hooks = <3

Hooks API dostarcza co prawda funkcję useReducer, która działa bardzo podobnie do Reduxa. Niestety nie daje możliwości użycia middleware stworzonych dla Reduxa, które w wielu przypadkach są bardzo przydatne. Do rozwiązania naszego problemu wygodnie będzie użyć redux-thunk do obsługi asynchroniczności. Skorzystamy też z hooków napisanych przez środowisko, dostępnych w paczce redux-react-hook.

Zacznijmy od stworzenia hooka useRedux, który pozwoli nam stworzyć i skonfigurować store. Jako parametry będzie przyjmował reducer (lub obiekt reducerów), tablice zawierającą middlewares, tablice zawierającą enhancers i inicjalny (wczytany stan). Sama funkcja useRedux powinna zwrócić store i context, który jest wymagany dla wybranej biblioteki:

import {
  useMemo,
} from 'react';
import {
  applyMiddleware,
  combineReducers,
  compose,
  createStore,
} from 'redux';
import {
  StoreContext,
} from 'redux-react-hook';


export default (
  reducers = {},
  middlewares = [],
  enhencers = [],
  preloadedState,
) => {
  const store = useMemo(() => {
    const rootReducer = typeof reducers === 'function'
      ? reducers
      : combineReducers(reducers);

    const middlewareEnhancer = applyMiddleware(...middlewares);
    const composedEnhancers = compose(middlewareEnhancer, ...enhencers);

    return createStore(rootReducer, preloadedState, composedEnhancers);
  }, []);

  return [
    store,
    StoreContext,
  ];
};

Teraz wystarczy użyć w głównym komponencie naszego nowego hooka:

import React from 'react';
import thunkMiddleware from 'redux-thunk';

import useRedux from 'hooks/useRedux';
import reducers from 'reducers';


export default function App() {
  const [store, StoreContext] = useRedux(
    reducers,
    [thunkMiddleware],
  );

  return (
    <StoreContext.Provider value={store}>
      /* Reszta komponentów aplikacji */
    </StoreContext.Provider>
  );
}

Będziemy teraz mogli skorzystać z wybranej biblioteki, żeby móc wykorzystywać wszystkie możliwości i standardy wypracowane dla reduxa. Nie mamy natomiast żadnej potrzeby używania dekoratorów, czy martwienia się o contexty. Nasza aplikacja będzie łatwiejsza do debugowania bez licznych komponentów służących do współdzielenia logiki.

Wybrana biblioteka ma kilka dobrych przykładów użycia w swojej dokumentacji, a podejść do rozwiązania problemu asynchronicznych zapytań wykorzystując Reduxa jest mnóstwo. Myślę, że to dobre miejsce do rozpoczęcia własnych eksperymentów z Hooks API.

Podsumowanie

Nowe API Reacta na pewno mocno wpłynie na sposób w jaki piszemy aplikacje korzystające z tej biblioteki. Pojawiają się też głosy, że jest to krok w stronę pełnoprawnego frameworka typu Angular. Moim zdaniem Hooks API na pewno zmniejszy liczbę linii, które musimy napisać w każdej aplikacji. Przy czym jest tak elastyczne, że nie wymaga dramatycznych zmian w sposobie pisania aplikacji. Osobiście uważam, że cała propozycja bardzo pasuje do Reacta. Sama z siebie niczego nie narzuca, nie trzeba nawet z niej korzystać, ale pozwala szybko stworzyć dobrze działający fragment kodu.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://justjoin.it/blog/wykorzystac-hooks-przykladzie-warstwy-odpowiedzialnej-komunikacje-backendem" order_type="social" width="100%" count_of_comments="8" ]