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:
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/typeofweb/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 connect z react-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 wywołania funkcji connect (10).
Nowe akcje
Istniejące już akcje searchContacts i contactsFetched 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 — dispatch i getState (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/typeofweb/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! zapisz się na szkolenie z React i Redux.
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.
