Zastanówmy się w jaki sposób możemy lekko odkurzyć nasz komponent napisany w React, aby pewną cześć logiki związaną z pobieraniem danych wydzielić do oddzielnego pliku i zapewnić ponowną używalność kodu…

Dlaczego warto stosować takie podejście?


Przede wszystkim zapewniamy podział kodu na pewne obszary, nie mieszamy logiki wyświetlania danych z pobieraniem danych. Zapewniamy możliwość ponownego użycia danego fragmentu kodu. Dodatkowo też, wymuszenie trzymania się pewnych zasad w ramach zespołu złożonego z kilku developerów, pozwoli na utrzymanie czystszego kodu…

Na początek zacznijmy od stworzenia aplikacji:

create-react-app api_call
cd api_call

W naszym przypadku żądania do API będziemy wykonywać poprzez bibliotekę axios, którą należy zainstalować.

npm install axios

Teraz pozostaje nam uruchomić aplikację

npm start

Dane będziemy pobierać z otwartego API:
https://pokeapi.co/

Stwórzmy komponent, który będzie pobierał dane i je wyświetlał…

import React from 'react';
import axios from 'axios/index';

class Pokemon extends React.Component {

    state = {
        pokemonList: null
    }

    componentDidMount() {
        // fetch data from API    
    }

    render() {
        if (this.state.pokemonList === null) {
            return (<div>Loading...</div>);
        }

        return (
            <div>
                Display pokemon list...
            </div>
        );
    }
}
export default Pokemon;

Kolejnym krokiem musi być przygotowanie żądania do API, które pobierze dane i przypisze je do state pokemonList. W tym przykładzie pomijamy w ogóle obsługę błędów, bo ten element będzie na pewno poruszony w innym artykule.

Modyfikujemy zawartość componentDidMount()

componentDidMount() {
    // In real life, API URL should be declared in another file :)
    axios.get('https://pokeapi.co/api/v2/ability/')
        .then((response) => {
            this.setState(() => ({
                pokemonList: response.data
            }));
        })
        .catch((error) => {
            // error handling
        });
}

Musimy jeszcze wyświetlić listę, więc w metodzie render() dodajemy prosty Array.map() https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Array/map

render() {
    if (this.state.pokemonList === null) {
        return (<div>Loading...</div>);
    }

    return (
        <div>
            {
                this.state.pokemonList.results.map((pokemon) => {
                    return (
                        <div name={pokemon.name}>{pokemon.name}</div>
                    )
                })
            }
        </div>
    );
}

Ok. Wszystko działa, komponent pobiera dane, lista jest renderowana. Mamy tutaj jednak sytuację, w której komponent zarówno pobiera dane jak i wyświetla je.
Czas na wydzielenie logiki związanej z pobieraniem danych do oddzielnego pliku.

Struktura folderów i nazewnictwo

Na pewno struktura folderów będzie tematem oddzielnego artykułu, jednak teraz skupimy się na czymś zupełnie innym.

W naszym prostym przykładzie utworzymy folder api, a w nim plik PokemonAPI.js
Przenosimy teraz część, zadeklarowaną w componentDidMount() to naszego pliku PokemonAPI.js, który będzie wyglądał tak:

import axios from 'axios';

export const getPokemonList = () => {
    // UWAGA! Konfiguracja URL do API powinna byc wyniesiona
    // do zewnetrznego pliku...
    return axios.get('https://pokeapi.co/api/v2/ability/')
        .then((response) => {
            return response.data;
        })
        .catch((error) => {
            return error;
        });
}

W pliku Pokemon.js, który jest naszym komponentem importujemy plik z API

import * as PokemonAPI from '../api/PokemonAPI';

i w componentDidMount() odwołujemy się do metody getPokemonList() poprzez

PokemonAPI.getPokemonList()

W związku że axios.get()jest asynchroniczne i zwraca nam Promise, bezpośrednie przypisanie danych zwróconych przez getPokemonList() poprzez setState() nie zadziała.

// to nie działa, ponieważ getPokemonList() zwraca Promise
this.setState(() => ({
    pokemonList: PokemonAPI.getPokemonList()
}));

Z pomocą przychodzi nam jednak mechanizm Promise chaining ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#Chaining), który umożliwia wywołanie kilka razy metody .then(), które nastąpią po sobie (oczywiście jeżeli nie wystąpi błąd).
Metoda getPokemonList() zawiera w sobie fragment .then() – zdefiniowany w PokemonAPI.js, który zostanie wykonany po otrzymaniu poprawnej odpowiedzi z serwera.
Następnie zostanie wywołana metoda .then() z pliku Pokemon.js, w metodzie componentDidMount() i dane zostaną przypisane do state.

componentDidMount() {
    PokemonAPI.getPokemonList()
        .then((pokemonList) => {
            this.setState(() => ({
                pokemonList: pokemonList
            }));
        });
}

Innym sposobem, aby uniknąć ponownego .then(), będzie użycie async/await, ale to zostawiam na później… 🙂

Jak widzicie logika związana z pobieraniem danych z API została wydzielona do zewnętrznego pliku i teraz w całej aplikacji możemy z niej korzystać w wielu miejscach.
Za chwilę ktoś pewnie powie, że warto jeszcze posprzątać komponent, bowiem w metodzie render() występuje .map() i to też można przenieść do innego komponentu…
Tak, zgadza się. W następnym artykule dotyczącym React, skupimy się na czymś, co nazywa się Container Component – link do artykułu: https://www.magicweb.pl/programowanie/frontend/react/container-component/

Repozytorium

Mimo, że projekt nie jest skomplikowany, możecie zobaczyć jak wygląda to finalnie.
https://github.com/magicwebpl/react-api-call-external-file

Zadanie domowe

Przygotuj proszę inny request do API, który zostanie umieszczony w oddzielnym pliku, a następnie wykorzystany w kilku komponentach. Dodatkowo możesz pomyśleć o obsłudze błędów, wykorzystując do tego .catch() oraz Promise chaining przedstawiony przy okazji getPokemonList() oraz .then(). Możesz też zapoznać się z async/await, aby uniknąć ponownego .then() i bezpośrednio po wywołaniu metody z pliku API, przypisać dane.

Źródła:
Obraz: https://unsplash.com/photos/ahi73ZN5P0Y