Główne logo strony
📅

Redux Thunk w aplikacjach React

Biblioteka Redux Thunk jest jednym z middlewarów których możemy używać w naszych aplikacjach zbudowanych w oparciu o React oraz Redux. Jego celem jest ułatwienie nam obsługi zdarzeń asynchronicznych takich jak zapytania do API, logowanie zdarzeń, routing, itp. W dalszej części artykułu postaram się przedstawić główne założenia którymi kieruje się Thunk i o czym należy pamiętać podczas implementacji. Postaram się utrzymać ten artykuł bardziej w formie zapoznania z biblioteką niż tutorialu. Mam nadzieję, że dzięki temu pozostanie dłużej aktualny oraz przyda się zarówno osobą początkujących jak i tym chcącym odświeżyć sobie wiedzę na jej temat.

Redux Thunk jest jednym z najbardziej popularnych middlewarów wykorzystanych w Redux. Bez mieddlewarów Redux jest w stanie jedynie wykonywać synchroniczne zmiany w globalnym stanie aplikacji. Innymi przykładami bibliotek których możemy użyć do obsługi akcji asynchronicznych są redux-saga (wykorzystująca w tym celu generatory) oraz redux-observable (korzystająca z podejścia reaktywnego). Większość jednak współczesnych aplikacji ogranicza swoją asynchroniczność do pobierania danych z API na potrzeby kolejnych odwiedzanych ekranów. Do jej obsłużenia Thunk będzie najlepszym i najprostszym rozwiązaniem jakie możemy wybrać z w/w opcji.

Thunk – co to w ogóle znaczy?

Samo pojęcie Thunk było używane w programowaniu jeszcze przed czasami Reduxa. W dużym uproszczenie jest to określenie funkcji, która zwraca inną funkcję. Niejako przez to opóźnia wywołanie zwracanej funkcji do czasu, gdy będzie to potrzebne. Bądź do czasu gdy wszystkie operacje w pierwszej funkcji zostaną wykonane i wynik tych operacji będzie argumentem dla funkcji zwracanej. Przykładowo taka funkcja może wyglądać następująco:

function first_function() {
  // wykonajmy pierwsze obliczenia
  console.log("Heavy working...");
  return function thunk() {
    // zwracana funkcja
    console.log("Continue heavy working...");
  };
}
 
first_function()();
/** console:
Heavy working...
Continue heavy working... 
**/
 
/** Inne podejście  */
const second_function = first_function(); // Heavy working...
 
// W sumie to powinienem zająć się czymś innym
console.log("Doing other stuff...");
 
// A teraz mogę kontynuować poprzednią pracę
second_function(); // Continue heavy working...

Tak na marginesie, pierwsze użycie słowa Thunk można znaleźć w tej oto już nieco leciwej książce – The New Hacker’s Dictionary:

In other words, it had 'already been thought of’; thus it was christened a thunk, which is 'the past tense of „think” at two in the morning.

The New Hacker’s Dictionary

Dalsza część artykułu zakłada, że mamy już podstawowe pojęcie na temat Reduxa i wiemy czym są akcje oraz kreatory akcji (Action Creators). Bez tej wiedzy nie ma sensu zagłębiać się dalej w temat Thunków.

Użycie Redux Thunk

Kreator akcji zwraca nam zwykły obiekt. Na podstawie property type tego obiektu wykonywana jest odpowiednia operacja przez reducer. Reducer nie może wykonywać żadnych zapytań do API, ponieważ z założenia jest funkcją bez efektów ubocznych (pure function). W takim przypadku gdy chcemy zapisać w stanie aplikacji dane pobrane z API musimy najpierw zainicjować pobieranie, wykonać zapytanie, poczekać na dane i zapisać je w stanie. Jedynym sposobem na zmianę stanu w Redux jest dispatch akcji oraz odpowiednie jej obsłużenie w reducerze. Tylko reducer powinien być upoważniony do zmiany stanu. Możemy tego dokonać oczywiście bez dodatkowych bibliotek:

// Kreator akcji
function loadData(dispatch, id) {
  // potrzebujemy funkcji dispatch
  return fetch(`http://example.com/${id}`)
    .then((res) => res.json()) // można oczywiście również skorzystać z async/await
    .then(
      (data) => dispatch({ type: "LOAD_DATA_SUCCESS", data }),
      (err) => dispatch({ type: "LOAD_DATA_FAILURE", err })
    );
}
 
// Wywołanie w komponencie Reacta
loadData(dispatch, userId);

Tworzymy tutaj kreator akcji, który w sposób asynchroniczny pobiera dane i po zakończeniu pobierania zwraca odpowiednią akcję – sukcesu bądź błędu. Na pierwszy rzut oka nie wygląda to najgorzej. Jednak jest jedna rzecz która przy większym projekcie będzie bardzo problematyczna. Mówimy tutaj o argumencie dispatch. Przy każdym wywołaniu kreatora akcji musimy przekazywać do niego funkcję dispatch aby poprawnie zwracać akcję.

Oczywiście nie trudno zgadnąć, że za chwilę tryumfalnie ogłoszę iż rozwiązaniem tego problemu jest właśnie redux-thunk , jednak najpierw spójrzmy jak powyższy przykład będzie wyglądał z użyciem thunka:

// Kreator akcji
function loadData(id) {
  return (dispatch) =>
    fetch(`http://example.com/${id}`) // tym zajmie się Redux Thunk
      .then((res) => res.json())
      .then(
        (data) => dispatch({ type: "LOAD_DATA_SUCCESS", data }),
        (err) => dispatch({ type: "LOAD_DATA_FAILURE", err })
      );
}
 
// Wywołanie w komponencie Reacta
dispatch(loadData(id)); // dispatch tak jak w przypadku normalnej synchronicznej akcji

Co się zmieniło? Teraz mamy tutaj funkcję zwracającą kolejną funkcję. Wpisuje się to dokładnie w poznaną już wcześniej definicję Thunka. Zauważmy, iż teraz jednak nie musimy podawać funkcji dispatch jako argumentu. Dispatchujemy akcję tak jak każdą inną standardową synchroniczną akcję. Mimo wszystko widzimy, że zwracana funkcja przyjmuje jako argument i wywołuje w swoim ciele dispatch. Jak?

Dotarliśmy w końca do tego czym tak naprawdę jest Redux Thunk. Jak widać cała praca tej biblioteki polega na wywołaniu funkcji zwracanej przez kreator akcji i podstawienie w miejsce pierwszego argumentu funkcji dispatch. Tak – to naprawdę wszystko co potrafi ta biblioteka.

„To niemożliwe” – pomyślał pewnie każdy podczas pierwszej styczności z Redux Thunk. Spójrzmy więc na poniższy snippet codu. Zanim przeczytasz dalszą część postu spróbuj domyśleć się co możemy osiągnąć za jego pomocą.

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) =>
    (next) =>
    (action) => {
      if (typeof action === "function") {
        return action(dispatch, getState, extraArgument);
      }
 
      return next(action);
    };
}
 
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
 
export default thunk;

Brawo – zgadza się. Powyższe 14 linijek tekstu to cała implementacja biblioteki redux-thunk (v2.3.0). Biorąc pod uwagę liczbę gwiazdek na Githubie w momencie tworzenia tego wpisu (14.4k) oznacza to, że każda linijka tego kodu jest warta ok. 1 tysiąca gwiazdek. Wynik dość imponujący.

Użycie ze stosem React / Redux

Sama implementacja omawianej biblioteki w naszej aplikacji jest równie prosta jak korzystanie z niej. Po dodaniu paczki redux-thunk do projektu musimy zarejestrować ją jako middleware podczas tworzenia redux store:

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers/index";
 
// redux >= 3.1.0
const store = createStore(rootReducer, applyMiddleware(thunk));

I to wszystko. Od teraz możemy korzystać ze wszystkich dobrodziejstw jakie dostarcza nam thunk. Dispatchujemy wszystkie akcje w naszej aplikacji w ten sam sposób, niezależnie od tego czy mamy akcję synchroniczną czy asynchroniczną. Akcją asynchroniczną zajmie się thunk. Akcja synchroniczna trafi do reducera (albo kolejnego middleware).

Jedna rzecz o której jeszcze nie wspomiałem. Jak można zauważ w implementacji biblioteki, zwracana z kreatora akcji funkcja oprócz argumentu dispatch przyjmuje argument getState. Dzięki temu w Thunku możemy najpierw sprawdzić stan aplikacji a następnie podjąć stosowne kroki. Może to się przydać na przykład w przypadku gdy chcemy sprawdzić czy dane które potrzebujemy nie znajdują się już w cache aplikacji. Wykonanie zapytania sieciowego w takim przypadku może okazać się zbędne.

Extra Argument

Począwszy od wersji 2.1.0, podczas dodawania Thunka jako middleware podczas tworzenia stanu, możemy podać mu tzw. extra argument, który będzie automatycznie dołączany podczas wywoływania zwracanej z kreatora akcji funkcji. Jako że Thunk w lwiej części przypadków wykorzystywany jest do pobieranie danych z API, to właśnie API client najczęściej jest podawany jako wspomniany już argument.

// konfiguracja store
const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument(api))
);
 
// Przykładowy Thunk wraz z api jako extra argument
const load =
  (roomId) =>
  async (
    dispatch, // dispatch zawsze jako pierwszy argument zwracanej funkcji
    getState, // funkcja do sprawdzenia aktualnego stanu
    api // extra argument
  ) => {
    dispatch(roomDetailsActions.load(roomId)); // to może być akcja synchroniczna
    try {
      const { data } = await api.roomsClient.getRoomDetails(roomId); // pobierzmy dane
      dispatch(roomDetailsActions.loadSuccess(data)); // kolejna akcja synchroniczna z pobranymi danymi jako payload
    } catch (error) {
      dispatch(
        roomDetailsActions.loadFailure({ error: "Room details load failed" })
      ); // akcja synchroniczna informyjąca o błędzie pobierania
    }
  };

Redux Toolkit

Przy okazji tego wpisu wspomnę również o bibliotece redux-toolkit. Nie jest ona wymagana do prawidłowego działania thunków, jednak w dużym stopniu redukuje nam boilerplate-y. Tworzenie store-a dla naszej aplikacji a następnie kolejnych akcji oraz odpowiadających im reducerów jest znacznie prostsze i bardziej intuicyjne. Toolkit został stworzony przez ten sam team, który rozwija Reduxa więc nie ma co się obawiać problemów z kompatybilnością. Moim zdaniem, Redux wraz z Redux Toolkit to tak naprawdę to czym Redux powinien być od samego początku. A dlaczego wspominam o tej bibliotece przy okazji tego wpisu?

It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes redux-thunk by default (…)

redux-toolkit Docs

Korzystając z Redux Toolkit mam zapewnione domyślne wsparcie dla thunków.

Tym optymistycznym akcentem zakończę już dzisiejszy wpis. Mam nadzieję, że teraz skorzystanie z Thunków oraz obsługi asynchroniczności w aplikacjach napisanych przy użyciu Reduxa oraz Reacta będzie dużo łatwiejsza oraz przyjemniejsza.

Masz uwagi lub sugestie do tego wpisu?

discord iconPrzejdź na Discord