Kurs JavaScript i React · Moduł 11: Pobieranie danych i API

Axios i biblioteki HTTP

6 min czytania
W tej lekcji7

Twój statek wysyła już zapytania do Centrum Dowodzenia przez fetch(), ale szybko zauważasz powtarzalną robotę: przy każdym zapytaniu ręcznie sprawdzasz response.ok, ręcznie parsujesz JSON, w każdym pliku przepisujesz ten sam adres serwera i ten sam token. Wbudowany fetch() to podstawowy nadajnik radiowy. Gdy flota rośnie, potrzebujemy systemu komunikacji z filtrami, limitem czasu i jednym centrum konfiguracji. Wtedy sięgamy po Axios, jedną z najpopularniejszych bibliotek HTTP dla JavaScript (aktualna linia to Axios 1.x).

Dlaczego Axios zamiast fetch?

Najłatwiej zobaczyć różnicę na tym samym zapytaniu napisanym na dwa sposoby. Zwróć uwagę, ile linii potrzebuje fetch(), żeby zachować się bezpiecznie:

1// FETCH - wymaga ręcznej obsługi
2const response = await fetch(url);
3if (!response.ok) throw new Error('Błąd'); // fetch NIE rzuca błędów dla 4xx/5xx!
4const data = await response.json(); // trzeba ręcznie parsować JSON
5
6// AXIOS - prostsze i bezpieczniejsze
7const { data } = await axios.get(url); // automatyczny JSON, rzuca błędy dla 4xx/5xx

Obie wersje pobierają te same dane, a serwer nie widzi żadnej różnicy. Zmienia się tylko to, ile pracy wykonujesz Ty: fetch() odrzuca Promise wyłącznie przy błędzie sieci, więc odpowiedź 404 trzeba wyłapać samodzielnie, a Axios domyślnie traktuje każdy status spoza zakresu 2xx jako błąd.

Kluczowe różnice zebrane w jednej tabeli:

Cechafetch()Axios
Parsowanie JSONRęczne (.json())Automatyczne
Błędy HTTPNie rzuca dla 4xx/5xxRzuca automatycznie
InterceptoryBrakWbudowane
TimeoutBrak opcji, ale jest AbortSignal.timeout(ms)Opcja timeout
AnulowanieAbortControllerAbortController (signal), CancelToken jest przestarzały
TransformacjaRęcznaWbudowana

Instalacja i podstawy Axios

Axios instalujesz jak każdą paczkę, a potem wywołujesz metody nazwane tak jak metody HTTP. Obiekt odpowiedzi ma pole data z gotowymi danymi, status z kodem i headers z nagłówkami:

1// Instalacja: npm install axios
2
3import axios from 'axios';
4
5// GET - pobranie danych
6const response = await axios.get(
7  'https://api.space-center.com/planets'
8);
9console.log(response.data);   // dane (już sparsowane!)
10console.log(response.status); // 200
11console.log(response.headers); // nagłówki
12
13// POST - wysłanie danych (automatyczna serializacja JSON)
14const newMission = await axios.post(
15  'https://api.space-center.com/missions',
16  { name: 'Alpha Centauri', crew: 5 }
17);
18
19// PUT, DELETE
20await axios.put(url, updatedData);
21await axios.delete(url);

Przy axios.post przekazujesz zwykły obiekt, a Axios sam zamienia go na JSON i ustawia nagłówek Content-Type. Sam adres serwera wciąż jednak powtarza się w każdej linii, i to naprawimy teraz.

Tworzenie instancji API

Metoda axios.create() tworzy osobnego klienta z własną konfiguracją: bazowym adresem (baseURL), limitem czasu w milisekundach (timeout) i domyślnymi nagłówkami. To jak zaprogramowanie radiostacji na stałą częstotliwość Centrum Dowodzenia:

1// Tworzymy klienta API z domyślną konfiguracją
2const spaceApi = axios.create({
3  baseURL: 'https://api.space-center.com',
4  timeout: 10000, // 10 sekund
5  headers: {
6    'Content-Type': 'application/json',
7    'X-Station-Id': 'ISS-2150',
8  },
9});
10
11// Teraz używamy krótszych ścieżek
12const planets = await spaceApi.get('/planets');
13const missions = await spaceApi.get('/missions');
14const newMission = await spaceApi.post('/missions', {
15  name: 'Deep Space Survey',
16  destination: 'Proxima Centauri',
17});

Od teraz wystarczą krótkie ścieżki jak /planets. Globalny obiekt axios nie został zmieniony, więc inne części aplikacji mogą mieć własne instancje dla innych serwerów. Polecam jedną instancję na każde API, trzymaną w osobnym pliku.

Interceptory - filtry komunikacji

Interceptor to funkcja, którą Axios uruchamia dla każdego zapytania przed wysłaniem albo dla każdej odpowiedzi przed oddaniem jej do Twojego kodu. Dzięki temu token dopisujesz w jednym miejscu, a nie w stu komponentach:

1// Interceptor zapytań - dodaje token autoryzacji
2spaceApi.interceptors.request.use(config => {
3  const token = localStorage.getItem('authToken');
4  if (token) {
5    config.headers.Authorization = `Bearer ${token}`;
6  }
7  console.log('Wysyłam zapytanie:', config.url);
8  return config;
9});

Interceptor zapytań musi zwrócić config, inaczej zapytanie nie wyruszy. Drugi filtr działa na drodze powrotnej: przyjmuje funkcję dla sukcesu i funkcję dla błędu, więc możesz globalnie zareagować na wygaśniętą sesję (401) albo zbyt wiele zapytań (429):

1// Interceptor odpowiedzi - obsługa błędów
2spaceApi.interceptors.response.use(
3  response => {
4    // Sukces - zwróć dane
5    return response;
6  },
7  error => {
8    // Błąd - obsłuż globalnie
9    if (error.response?.status === 401) {
10      console.log('Sesja wygasła - przekierowanie do logowania');
11      window.location.href = '/login';
12    }
13    if (error.response?.status === 429) {
14      console.log('Zbyt wiele zapytań - spróbuj później');
15    }
16    return Promise.reject(error);
17  }
18);

Promise.reject(error) na końcu jest ważne: bez niego błąd zostałby "połknięty", a komponent myślałby, że wszystko poszło dobrze.

Transformacja zapytań i odpowiedzi

Opcje transformRequest i transformResponse pozwalają przerobić dane tuż przed wysłaniem i tuż po odebraniu. Tutaj dopisujemy znacznik czasu do każdego zapytania i pole fetchedAt do każdej odpowiedzi:

1const api = axios.create({
2  baseURL: 'https://api.space-center.com',
3  // Transformacja przed wysłaniem
4  transformRequest: [(data) => {
5    // Dodaj timestamp do każdego zapytania
6    return JSON.stringify({
7      ...data,
8      timestamp: Date.now(),
9      source: 'space-station',
10    });
11  }],
12  // Transformacja po odebraniu
13  transformResponse: [(data) => {
14    const parsed = JSON.parse(data);
15    // Dodaj pole "fetchedAt" do każdej odpowiedzi
16    return { ...parsed, fetchedAt: new Date().toISOString() };
17  }],
18});

Uwaga: własna tablica zastępuje domyślne transformacje Axios, które normalnie zamieniają obiekt na JSON i parsują odpowiedź. Dlatego sami wołamy tu JSON.stringify i JSON.parse. Do prostych dopisków wygodniejsze bywają interceptory, bo nie wyłączają niczego domyślnego.

Obsługa błędów w Axios

Skoro Axios rzuca błędy dla kodów 4xx i 5xx, obiekt błędu mówi, na którym etapie coś się zepsuło. error.response istnieje, gdy serwer odpowiedział, error.request gdy zapytanie wyszło, ale odpowiedź nie wróciła:

1async function fetchPlanet(id) {
2  try {
3    const { data } = await spaceApi.get(
4      `/planets/${id}`
5    );
6    return data;
7  } catch (error) {
8    if (error.response) {
9      // Serwer odpowiedział z kodem błędu (4xx, 5xx)
10      console.error('Status:', error.response.status);
11      console.error('Dane:', error.response.data);
12    } else if (error.request) {
13      // Zapytanie wysłane, ale brak odpowiedzi (timeout, sieć)
14      console.error('Brak odpowiedzi od serwera');
15    } else {
16      // Błąd w konfiguracji zapytania
17      console.error('Błąd konfiguracji:', error.message);
18    }
19    throw error;
20  }
21}

Status 404 znajdziesz więc w error.response.status, a nie w error.message. Na końcu rzucamy błąd dalej, żeby komponent mógł pokazać komunikat.

Axios w komponencie React

W komponencie łączymy wszystko: instancję, stany ładowania i błędu oraz anulowanie zapytania, gdy komponent znika z ekranu. W starszym kodzie zobaczysz do tego CancelToken:

1// PRZESTARZAŁE (od Axios 0.22) - spotkasz to w starszych projektach
2const source = axios.CancelToken.source();
3spaceApi.get('/people/', { cancelToken: source.token });
4source.cancel('Komponent odmontowany');

Dokumentacja Axios oznacza CancelToken jako przestarzały, więc w nowym kodzie używamy standardowego AbortController i opcji signal, tej samej, której używa fetch():

1import React, { useState, useEffect } from 'react';
2import axios from 'axios';
3
4const spaceApi = axios.create({
5  baseURL: 'https://swapi.dev/api',
6  timeout: 8000,
7});
8
9function CrewList() {
10  const [crew, setCrew] = useState([]);
11  const [loading, setLoading] = useState(true);
12  const [error, setError] = useState(null);
13
14  useEffect(() => {
15    const controller = new AbortController();
16
17    async function loadCrew() {
18      try {
19        const { data } = await spaceApi.get('/people/', {
20          signal: controller.signal,
21        });
22        setCrew(data.results);
23      } catch (err) {
24        if (!axios.isCancel(err)) {
25          setError(err.message);
26        }
27      } finally {
28        setLoading(false);
29      }
30    }
31
32    loadCrew();
33    return () => controller.abort();
34  }, []);
35
36  if (loading) return <p>Skanowanie załogi...</p>;
37  if (error) return <p>Błąd: {error}</p>;
38
39  return (
40    <ul>
41      {crew.map(member => (
42        <li key={member.name}>{member.name}</li>
43      ))}
44    </ul>
45  );
46}

axios.isCancel(err) rozpoznaje anulowanie (Axios zgłasza je jako CanceledError), więc nie pokazujemy go jako błędu. Reszta komponentu się nie zmieniła: stany i warunkowe renderowanie wyglądają tak samo jak przy fetch(). Szczegółowo zajmiemy się nimi w następnej lekcji, a anulowaniem w lekcji o race conditions.

Pamiętaj: Axios to radiostacja mostka kapitańskiego, jedna konfiguracja, wspólne filtry i czytelne błędy dla całej floty.

Kod do tej lekcji: App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Symulacja Axios API (w prawdziwym projekcie: import axios from 'axios')
4const axios = {
5  create: (config) => {
6    const instance = {
7      defaults: config,
8      interceptors: {
9        request: { handlers: [], use: (fn) => instance.interceptors.request.handlers.push(fn) },
10        response: { handlers: [], use: (ok, err) => instance.interceptors.response.handlers.push({ ok, err }) },
11      },
12      get: async (path) => {
13        const url = config.baseURL + path;
14        // Uruchom interceptory zapytan
15        let reqConfig = { url, headers: { ...config.headers } };
16        for (const handler of instance.interceptors.request.handlers) {
17          reqConfig = handler(reqConfig);
18        }
19        console.log('[Axios Request]', reqConfig.url, reqConfig.headers);
20
21        const response = await fetch(url, {
22          headers: reqConfig.headers,
23          signal: reqConfig.signal,
24        });
25        if (!response.ok) {
26          const error = new Error(`HTTP ${response.status}`);
27          error.response = { status: response.status, data: null };
28          // Uruchom error interceptory
29          for (const handler of instance.interceptors.response.handlers) {
30            if (handler.err) handler.err(error);
31          }
32          throw error;
33        }
34        const data = await response.json();
35        const result = { data, status: response.status, headers: response.headers };
36        // Uruchom success interceptory
37        for (const handler of instance.interceptors.response.handlers) {
38          if (handler.ok) handler.ok(result);
39        }
40        return result;
41      },
42    };
43    return instance;
44  },
45};
46
47// Tworzymy instancje API
48const spaceApi = axios.create({
49  baseURL: 'https://swapi.dev/api',
50  timeout: 10000,
51  headers: { 'X-Station-Id': 'ISS-2150' },
52});
53
54// Interceptor zapytan - logowanie
55spaceApi.interceptors.request.use(config => {
56  console.log('[Interceptor] Dodaje timestamp do zapytania');
57  config.headers['X-Request-Time'] = new Date().toISOString();
58  return config;
59});
60
61// Interceptor odpowiedzi
62spaceApi.interceptors.response.use(
63  response => { console.log('[Interceptor] Sukces:', response.status); },
64  error => { console.log('[Interceptor] Blad:', error.response?.status); }
65);
66
67function AxiosDemo() {
68  const [planets, setPlanets] = useState([]);
69  const [loading, setLoading] = useState(true);
70  const [error, setError] = useState(null);
71  const [logs, setLogs] = useState([]);
72
73  useEffect(() => {
74    async function loadPlanets() {
75      try {
76        setLogs(prev => [...prev, 'Wysylam zapytanie przez Axios...']);
77        const { data } = await spaceApi.get('/planets/');
78        setPlanets(data.results);
79        setLogs(prev => [...prev, 'Dane pobrane! (automatyczny JSON)']);
80      } catch (err) {
81        setError(err.message);
82        setLogs(prev => [...prev, 'Axios rzucil blad: ' + err.message]);
83      } finally {
84        setLoading(false);
85      }
86    }
87    loadPlanets();
88  }, []);
89
90  return (
91    <div className="app">
92      <h1>Axios - Biblioteka HTTP</h1>
93      <div className="logs">
94        <h3>Logi interceptorow:</h3>
95        {logs.map((log, i) => <p key={i} className="log">{log}</p>)}
96      </div>
97      {loading && <div className="loading"><div className="spinner"></div></div>}
98      {error && <div className="error"><p>{error}</p></div>}
99      {!loading && !error && (
100        <div className="planet-list">
101          {planets.map(planet => (
102            <div key={planet.name} className="planet-card">
103              <h3>{planet.name}</h3>
104              <p><strong>Klimat:</strong> {planet.climate}</p>
105              <p><strong>Populacja:</strong> {planet.population}</p>
106            </div>
107          ))}
108        </div>
109      )}
110    </div>
111  );
112}
113
114export default AxiosDemo;

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaka jest kluczowa różnica między Axios a wbudowanym fetch() w obsłudze błędów HTTP?

  2. 2. Do czego służą interceptory (interceptors) w Axios?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Pobieranie danych z Axios

  • Klikanie w kolejności

    Kliknij elementy w kolejności tworzenia skonfigurowanej instancji Axios:

  • Układanie w pionie

    Ułóż kroki dodawania interceptora zapytań w Axios:

Przydatne artykuły