Asynchroniczność w Redux: redux-thunk

Ten wpis jest 28 częścią z 29 w kursie React.js

Do tej pory dane z API pobierałem po prostu w komponencie App, a po przyjściu odpowiedzi wysysyłałem odpowiednią akcję (contactsFetched). To działało. Ale wymyśliłem sobie nową funkcję w aplikacji: Możliwość parametryzowania zapytań do API. Posłuży mi do tego nowy komponent. Jak teraz mam wykonywać zapytania do API? Przekazywać coś do store, a to coś wpłynie na App, który wykona zapytanie i zwróci dane znowu do store? Nie brzmi za dobrze. Ale jest lepszy sposób: Poznaj redux-thunk!

redux-thunk

Może najpierw krótko: Czym jest redux-thunk? Jest to dodatek (a konkretnie middleware) do Reduksa, który pozwala na wysyłanie akcji, które są funkcjami. Takie akcje nie trafiają do Twoich reducerów. Ich zadaniem jest wyemitowanie kolejnych akcji — jednej lub kilku, po pewnym czasie, asynchronicznie. Przykładowo:

store.dispatch({ type: 'INCREMENT' }); // (1)

store.dispatch(function (dispatch) {
  dispatch({ type: 'INCREMENT' }); // (2)
  setTimeout(() => dispatch({ type: 'INCREMENT' }), 1000); // (3)
  setTimeout(() => dispatch({ type: 'INCREMENT' }), 2000); // (3)
});

Tutaj od razu wysyłam pierwszą akcję (1) — to mniej więcej to samo co robiłaś do tej pory przez mapDispatchToProps, tylko maksymalnie uprościłem ten kod.

Skupmy się jednak na thunku! Następnie do dispatch przekazuję funkcję — to nie działałoby bez redux-thunk! W tej funkcji (2) natychmiast wywołuję kolejny INCREMENT, a następnie, asynchronicznie, jeszcze dwa kolejne (3). Zobacz to na żywo:

See the Pen React Redux Thunk Type of Web by Michał Miszczyszyn (@mmiszy) on CodePen.

Jeśli w demie powyżej widzisz od razu „Licznik: 5” to otwórz je w nowej karcie. Licznik zacznie się od 2 (po dwóch synchronicznych akcjach), a następnie po sekundzie wskoczy 3, po kolejnej 4 i potem 5.

Zapytania do API w redux-thunk

A więc jak wykonać zapytanie do API w redux-thunk? Bardzo łatwo 😉 W ten sposób:

const contactsFetched = contacts => ({ // (4)
  type: "FETCH_CONTACTS_SUCCESS",
  contacts
});

export const fetchContacts = () => (dispatch) => { // (5)
  fetch("https://myapi.local/contacts)
    .then(res => res.json())
    .then(json => dispatch(contactsFetched(json.results)));
};

Tutaj widzisz standardowy action creator, taki jak w poprzednim artykule (4). Posłuży on do poinformowania aplikacji o tym, że dane już zostały pobrane — dokładnie tak jak było wcześniej. Zmianą jest przeniesienie samego fetch do action creatora fetchContacts poniżej (5). Dzięki redux-thunk możliwe stało się wywołanie fetch, a potem wywołanie kolejnej akcji gdy nadejdą dane. Super, prawda?

W praktyce sama funkcja do pobierania danych z API byłaby wyniesiona do osobnego pliku — za warstwę abstrakcji. Dzięki temu kod byłby testowalny i łatwy do modyfikacji. Wtedy ten fragment wyglądałby jakoś tak:

export const fetchContacts = () => (dispatch) => {
  ContactsApi.getAll().then(contacts => dispatch(contactsFetched(contacts)));
};

Dodaj to do aplikacji

To zaimplementuj nową funkcję w appce. Dodaj select, którym będzie można sparametryzować zapytanie. Wyobraź sobie, że tym selectem możesz przełączyć czy chcesz widzieć listę wszystkich kontaktów, tylko ulubionych kontaktów, czy tych nielubianych 😉 W naszym API zasymulujemy to przez podane parametru seed.

Założenia są takie:

  • wszystko to co mamy do tej pory nadal działa:
    • aplikacja się otwiera,
    • kontakty się automatycznie wczytują,
    • filtrowanie po imionach działa,
  • dodatkowo: po wybraniu seeda kontakty się przeładowują,
  • filtrowanie nadal działa niezależnie od seeda

Wszystko jasne? Zaczynam od kodu z poprzedniego wpisu: github.com/mmiszy/typeofweb-kurs-react/tree/contacts-list-3-redux

redux-thunk i fetch

Modyfikuję więc punkt startowy naszej appki, czyli plik App.jsx. Zamiast zapytania do API, wywoła on po prostu odpowiednią akcję, która już zajmie się resztą:

// App.jsx
componentDidMount() {
  this.props.fetchContacts() // tutaj był wcześniej fetch
}

Poza tym w samym App.jsx nic więcej nie zmienia! Napisz teraz komponent SeedPicker i potrzebne akcje.

SeedPicker

Tak, jak opisałem wcześniej, SeedPicker ma być zwykłym select-em z kilkoma predefiniowanymi wartościami do wyboru. Zmiana wartości ma skutkować wysłaniem akcji.

// SeedPicker.jsx
class SeedPicker extends React.Component {
  render() {
    return (
      <div className="field">
        <select
          className="ui dropdown fluid"
          onChange={this.handleSeedChange} // (6)
          value={this.props.seed}
        >
          <option value="default-seed">Default seed</option>
          <option value="one-seed">One seed</option>
          <option value="another-seed">Another seed</option>
        </select>
      </div>
    );
  }

  handleSeedChange = e => {
    this.props.changeSeedAndFetch(e.currentTarget.value); // (7)
  };
}

Oto ten komponent 😉 Przy zmianie wartości, wywoływana jest metoda (6), która wywoła z kolei funkcję przekazaną jako props (7). Ta funkcja zostanie dostarczona przez connectreact-redux i wyśle akcję — dokładnie tak jak do tej pory:

// SeedPicker.jsx
const mapStateToProps = (state) => { // (8)
  return {
    seed: state.seed
  };
};
const mapDispatchToProps = { changeSeedAndFetch }; // (9)

export const SeedPickerContainer = connect(mapStateToProps, mapDispatchToProps)( // (10)
  SeedPicker
);

Standardowe nazewnictwo funkcji i obiektów: w mapStateToProps tworzysz potrzebny props seed (8). Do mapDispatchToProps przekazujesz akcję, którą za chwilę stworzysz (9). Gotowy komponent SeedPickerContainer to wynik wowyołania funkcji connect (10).

Nowe akcje

Istniejące już akcje searchContactscontactsFetched pozostają bez zmian. Pojawia się kilka nowych:

  • changeSeed
  • fetchContacts
  • changeSeedAndFetch

Po kolei:

changeSeed

export const changeSeed = seed => ({
  type: "CHANGE_SEED",
  seed
});

Analogiczna akcja do searchContacts, przekazujemy tekst, nic więcej się nie dzieje.

fetchContacts

export const fetchContacts = () => (dispatch, getState) => { // (11)
  fetch(
    "https://randomuser.me/api/?format=json&results=10&seed=" +
      encodeURIComponent(getState().seed)
  )
    .then(res => res.json())
    .then(json => dispatch(contactsFetched(json.results)));
};

Tutaj robi się ciekawiej! Jest to akcja działająca dzięki redux-thunk — funkcja.

Zwróć uwagę na to, że zwracana funkcja przyjmuje dwa argumenty — dispatchgetState (11). Ten drugi jest przydatny, gdy działanie akcji ma zależeć od wartości zapisanych w storze — w tym przypadku tak jest, gdyż potrzebujesz parametru seed do zapytania. Dalej wykonywany jest po prostu fetch z odpowiednim adresem (+ seed!), a po przyjściu danych wysyłana jest kolejna akcja — contactsFetched (już istniejąca).

changeSeedAndFetch

export const changeSeedAndFetch = seed => dispatch => {
  dispatch(changeSeed(seed)); // (12)
  dispatch(fetchContacts()); // (13)
};

Teraz dopiero zrobiło się ciekawie 😉 Ta akcja robi 2 rzeczy: Zmienia seed (dzięki akcji changeSeed (12)), a następnie inicjuje ponowne pobranie kontaktów (już z nowym seedem — (13)). Jak widzisz, nowe akcje mogą korzystać z już istniejących: komponować je i wywoływać w różnej kolejności, także asynchronicznie.

reducer

Jeszcze jedna formalność: Brakujący reducer dla pola seed:

export const seed = (state = 'default-seed', action) => {
  switch (action.type) {
    case 'CHANGE_SEED':
      return action.seed;
    default:
      return state
  }
}

Jeśli widzisz bliźniacze podobieństwo tego reducera do reducera contactsSearch i wydaje Ci się to zbędną duplikacją kodu to… masz rację. Ten problem rozwiązuje się używając tzw. Higher-Order Reducers. Poświęcę temu pojęciu osobny wpis.

Efekt

Zobacz tutaj:

Kod znajdziesz jak zwykle na moim GitHubie: github.com/mmiszy/typeofweb-kurs-react/tree/contacts-list-4-redux

Podsumowanie

Wygląda dobrze, prawda? Umiesz już tworzyć asynchroniczne akcje, wywoływać je jedna po drugiej i reagować na zmiany. Świetna robota!

Jeśli chcesz na bieżąco dowiadywać się o kolejnych częściach kursu React.js to koniecznie śledź mnie na Facebooku i zapisz się na newsletter.

Ćwiczenie

Ćwiczenie: Zrefaktoruj plik z akcjami tak, aby nie było w nim żadnego wywołania fetch — tylko abstrakcje. Stwórz plik o nazwie ContactsApi i tam umieść funkcję do pobierania kontaktów.