Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Obsługa API i pobieranie danych

Komunikacja z zewnętrznymi API jest jednym z najczęstszych przypadków użycia efektów ubocznych w aplikacjach React. W tym module nauczymy się, jak efektywnie pobierać, przetwarzać i zarządzać danymi z zewnętrznych źródeł.

Podstawy pobierania danych z API

W aplikacjach webowych, dane często pochodzą z zewnętrznych API, które dostarczają informacje w formacie JSON, XML lub innych. W naszej kosmicznej podróży, możemy wyobrazić sobie centrum kontroli misji pobierające dane z różnych satelitów, statków kosmicznych i stacji badawczych.

Prosty przykład pobierania danych z useEffect

1import React, { useState, useEffect } from 'react';
2
3function AstronautsList() {
4  const [astronauts, setAstronauts] = useState([]);
5  const [loading, setLoading] = useState(true);
6  const [error, setError] = useState(null);
7
8  useEffect(() => {
9    // Funkcja asynchroniczna do pobierania danych
10    async function fetchAstronauts() {
11      try {
12        setLoading(true);
13        // Pobieranie danych
14        const response = await fetch('https://api.spaceagency.com/astronauts');
15        
16        // Sprawdzenie czy odpowiedź jest OK
17        if (!response.ok) {
18          throw new Error(`HTTP Error: ${response.status}`);
19        }
20        
21        // Parsowanie odpowiedzi JSON
22        const data = await response.json();
23        
24        // Aktualizacja stanu
25        setAstronauts(data);
26        setError(null);
27      } catch (err) {
28        // Obsługa błędów
29        setError(`Nie udało się pobrać danych: ${err.message}`);
30        setAstronauts([]);
31      } finally {
32        // Oznaczenie zakończenia ładowania
33        setLoading(false);
34      }
35    }
36
37    // Wywołanie funkcji
38    fetchAstronauts();
39  }, []); // Pusta tablica zależności - wykonaj tylko raz
40
41  // Renderowanie na podstawie stanu
42  if (loading) return <div>Ładowanie danych...</div>;
43  if (error) return <div>Błąd: {error}</div>;
44
45  return (
46    <div>
47      <h2>Aktywni astronauci</h2>
48      <ul>
49        {astronauts.map(astronaut => (
50          <li key={astronaut.id}>
51            {astronaut.name} - {astronaut.role}
52          </li>
53        ))}
54      </ul>
55    </div>
56  );
57}

W powyższym przykładzie:

  1. Używamy
    useState
    do przechowywania danych, stanu ładowania i błędów
  2. Używamy
    useEffect
    z pustą tablicą zależności, aby pobrać dane tylko raz po zamontowaniu komponentu
  3. Wykorzystujemy
    async/await
    do obsługi asynchronicznego pobierania danych
  4. Obsługujemy różne stany (ładowanie, błąd, sukces) w interfejsie użytkownika

Zaawansowane techniki pobierania danych

1. Pobieranie danych z parametrami

Często musimy pobierać dane na podstawie parametrów, które mogą się zmieniać:

1function MissionDetails({ missionId }) {
2  const [mission, setMission] = useState(null);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    let isMounted = true; // Flaga do zapobiegania aktualizacji stanu po odmontowaniu
8    
9    async function fetchMission() {
10      try {
11        setLoading(true);
12        
13        const response = await fetch(
14          `https://api.spaceagency.com/missions/${missionId}`
15        );
16        
17        if (!response.ok) {
18          throw new Error(`HTTP Error: ${response.status}`);
19        }
20        
21        const data = await response.json();
22        
23        // Aktualizuj stan tylko jeśli komponent jest nadal zamontowany
24        if (isMounted) {
25          setMission(data);
26          setError(null);
27        }
28      } catch (err) {
29        if (isMounted) {
30          setError(`Nie udało się pobrać danych misji: ${err.message}`);
31          setMission(null);
32        }
33      } finally {
34        if (isMounted) {
35          setLoading(false);
36        }
37      }
38    }
39
40    fetchMission();
41    
42    // Funkcja czyszcząca
43    return () => {
44      isMounted = false; // Oznacz, że komponent został odmontowany
45    };
46  }, [missionId]); // Pobierz dane ponownie, gdy zmieni się missionId
47  
48  // Renderowanie
49  if (loading) return <div>Ładowanie danych misji...</div>;
50  if (error) return <div>Błąd: {error}</div>;
51  if (!mission) return <div>Nie znaleziono misji</div>;
52
53  return (
54    <div className="mission-details">
55      <h2>{mission.name}</h2>
56      <p>Status: {mission.status}</p>
57      <p>Data startu: {new Date(mission.launchDate).toLocaleDateString()}</p>
58      <p>Dowódca: {mission.commander}</p>
59      {/* Więcej szczegółów misji */}
60    </div>
61  );
62}

W tym przykładzie:

  1. Pobieramy dane na podstawie
    missionId
    , które może się zmieniać
  2. Używamy flagi
    isMounted
    do zapobiegania aktualizacji stanu po odmontowaniu komponentu
  3. Dodajemy funkcję czyszczącą, która ustawia
    isMounted = false
    przy odmontowaniu

2. Anulowanie żądań fetch

API Fetch nie ma wbudowanego mechanizmu anulowania, ale możemy użyć AbortController, aby przerwać żądanie:

1function SpaceWeather({ location }) {
2  const [weatherData, setWeatherData] = useState(null);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    // Utwórz AbortController do anulowania żądania
8    const abortController = new AbortController();
9    const signal = abortController.signal;
10
11    async function fetchWeatherData() {
12      try {
13        setLoading(true);
14        
15        const response = await fetch(
16          `https://api.spaceagency.com/weather/${location}`,
17          { signal } // Przypisanie sygnału do żądania
18        );
19        
20        if (!response.ok) {
21          throw new Error(`HTTP Error: ${response.status}`);
22        }
23        
24        const data = await response.json();
25        setWeatherData(data);
26        setError(null);
27      } catch (err) {
28        // Ignoruj błędy związane z anulowaniem
29        if (err.name === 'AbortError') {
30          console.log('Żądanie pogody zostało anulowane');
31        } else {
32          setError(`Nie udało się pobrać danych pogodowych: ${err.message}`);
33          setWeatherData(null);
34        }
35      } finally {
36        setLoading(false);
37      }
38    }
39
40    fetchWeatherData();
41    
42    // Funkcja czyszcząca anuluje żądanie, jeśli komponent
43    // zostanie odmontowany przed zakończeniem pobierania
44    return () => {
45      abortController.abort();
46    };
47  }, [location]);
48
49  // Renderowanie
50  if (loading) return <div>Ładowanie danych pogodowych...</div>;
51  if (error) return <div>Błąd: {error}</div>;
52  if (!weatherData) return <div>Brak danych pogodowych</div>;
53
54  return (
55    <div className="space-weather">
56      <h2>Pogoda kosmiczna: {location}</h2>
57      <p>Promieniowanie: {weatherData.radiation} mSv</p>
58      <p>Aktywność słoneczna: {weatherData.solarActivity}</p>
59      <p>Temperatura: {weatherData.temperature}°C</p>
60    </div>
61  );
62}

W tym przykładzie:

  1. Używamy
    AbortController
    do tworzenia sygnału, który może przerwać żądanie Fetch
  2. Przekazujemy sygnał do opcji żądania Fetch
  3. W funkcji czyszczącej wywołujemy
    abortController.abort()
    , aby anulować trwające żądanie
  4. Obsługujemy specjalnie błąd
    AbortError
    , który jest rzucany, gdy żądanie zostało anulowane

3. Obsługa wielu równoczesnych zapytań

Czasami potrzebujemy pobrać dane z kilku źródeł jednocześnie:

1function MissionDashboard({ missionId }) {
2  const [missionData, setMissionData] = useState({
3    details: null,
4    crew: [],
5    telemetry: null
6  });
7  const [loading, setLoading] = useState(true);
8  const [error, setError] = useState(null);
9
10  useEffect(() => {
11    let isMounted = true;
12    
13    async function fetchMissionData() {
14      try {
15        setLoading(true);
16        
17        // Pobieranie danych równolegle za pomocą Promise.all
18        const [detailsResponse, crewResponse, telemetryResponse] = await Promise.all([
19          fetch(`https://api.spaceagency.com/missions/${missionId}`),
20          fetch(`https://api.spaceagency.com/missions/${missionId}/crew`),
21          fetch(`https://api.spaceagency.com/missions/${missionId}/telemetry`)
22        ]);
23        
24        // Sprawdzenie odpowiedzi
25        if (!detailsResponse.ok || !crewResponse.ok || !telemetryResponse.ok) {
26          throw new Error('Jedno lub więcej żądań zakończyło się błędem');
27        }
28        
29        // Parsowanie danych równolegle
30        const [details, crew, telemetry] = await Promise.all([
31          detailsResponse.json(),
32          crewResponse.json(),
33          telemetryResponse.json()
34        ]);
35        
36        if (isMounted) {
37          setMissionData({ details, crew, telemetry });
38          setError(null);
39        }
40      } catch (err) {
41        if (isMounted) {
42          setError(`Nie udało się pobrać danych misji: ${err.message}`);
43        }
44      } finally {
45        if (isMounted) {
46          setLoading(false);
47        }
48      }
49    }
50
51    fetchMissionData();
52    
53    return () => {
54      isMounted = false;
55    };
56  }, [missionId]);
57
58  // Renderowanie
59  if (loading) return <div>Ładowanie danych misji...</div>;
60  if (error) return <div>Błąd: {error}</div>;
61
62  const { details, crew, telemetry } = missionData;
63
64  return (
65    <div className="mission-dashboard">
66      <h1>{details.name} - Panel kontrolny</h1>
67      
68      <section className="mission-details">
69        <h2>Szczegóły misji</h2>
70        <p>Status: {details.status}</p>
71        <p>Cel: {details.objective}</p>
72      </section>
73      
74      <section className="crew-info">
75        <h2>Załoga</h2>
76        <ul>
77          {crew.map(member => (
78            <li key={member.id}>
79              {member.name} - {member.role}
80            </li>
81          ))}
82        </ul>
83      </section>
84      
85      <section className="telemetry-data">
86        <h2>Telemetria</h2>
87        <p>Tlen: {telemetry.oxygen}%</p>
88        <p>Paliwo: {telemetry.fuel}%</p>
89        <p>Prędkość: {telemetry.velocity} km/h</p>
90      </section>
91    </div>
92  );
93}

W tym przykładzie:

  1. Używamy
    Promise.all()
    do równoległego wykonywania wielu żądań
  2. Gromadzimy wszystkie dane w jednym stanie
    missionData
  3. Obsługujemy wszystkie dane w jednym komponencie

Dobre praktyki i wzorce

1. Wyodrębnianie logiki pobierania danych

Aby poprawić czytelność i utrzymanie kodu, warto wyodrębnić logikę pobierania danych poza komponent:

1// api.js - Serwisy API
2const API_BASE_URL = 'https://api.spaceagency.com';
3
4export async function fetchAstronauts() {
5  const response = await fetch(`${API_BASE_URL}/astronauts`);
6  if (!response.ok) {
7    throw new Error(`HTTP Error: ${response.status}`);
8  }
9  return response.json();
10}
11
12export async function fetchMission(missionId) {
13  const response = await fetch(`${API_BASE_URL}/missions/${missionId}`);
14  if (!response.ok) {
15    throw new Error(`HTTP Error: ${response.status}`);
16  }
17  return response.json();
18}
19
20// Komponent z wyodrębnioną logiką API
21function AstronautsList() {
22  const [astronauts, setAstronauts] = useState([]);
23  const [loading, setLoading] = useState(true);
24  const [error, setError] = useState(null);
25
26  useEffect(() => {
27    let isMounted = true;
28    
29    async function getAstronauts() {
30      try {
31        setLoading(true);
32        const data = await fetchAstronauts();
33        
34        if (isMounted) {
35          setAstronauts(data);
36          setError(null);
37        }
38      } catch (err) {
39        if (isMounted) {
40          setError(`Nie udało się pobrać danych: ${err.message}`);
41          setAstronauts([]);
42        }
43      } finally {
44        if (isMounted) {
45          setLoading(false);
46        }
47      }
48    }
49
50    getAstronauts();
51    
52    return () => {
53      isMounted = false;
54    };
55  }, []);
56
57  // Renderowanie
58  //
59}

2. Tworzenie własnych hooków do pobierania danych

Dla powtarzalnych operacji pobierania danych, najlepszym rozwiązaniem jest stworzenie własnego hooka:

1// useFetch.js - Własny hook do pobierania danych
2function useFetch(url, options = {}) {
3  const [data, setData] = useState(null);
4  const [loading, setLoading] = useState(true);
5  const [error, setError] = useState(null);
6  
7  useEffect(() => {
8    let isMounted = true;
9    const abortController = new AbortController();
10    const signal = abortController.signal;
11    
12    async function fetchData() {
13      try {
14        setLoading(true);
15        
16        const response = await fetch(url, {
17          ...options,
18          signal,
19        });
20        
21        if (!response.ok) {
22          throw new Error(`HTTP Error: ${response.status}`);
23        }
24        
25        const result = await response.json();
26        
27        if (isMounted) {
28          setData(result);
29          setError(null);
30        }
31      } catch (err) {
32        if (err.name !== 'AbortError' && isMounted) {
33          setError(`Nie udało się pobrać danych: ${err.message}`);
34          setData(null);
35        }
36      } finally {
37        if (isMounted) {
38          setLoading(false);
39        }
40      }
41    }
42    
43    fetchData();
44    
45    return () => {
46      isMounted = false;
47      abortController.abort();
48    };
49  }, [url, JSON.stringify(options)]);
50  
51  return { data, loading, error };
52}
53
54// Użycie własnego hooka
55function PlanetInfo({ planetId }) {
56  const { data: planet, loading, error } = useFetch(
57    `https://api.spaceagency.com/planets/${planetId}`
58  );
59  
60  if (loading) return <div>Ładowanie...</div>;
61  if (error) return <div>Błąd: {error}</div>;
62  if (!planet) return <div>Nie znaleziono planety</div>;
63  
64  return (
65    <div className="planet-card">
66      <h2>{planet.name}</h2>
67      <p>Typ: {planet.type}</p>
68      <p>Średnica: {planet.diameter} km</p>
69      <p>Odległość od Słońca: {planet.distanceFromSun} mln km</p>
70    </div>
71  );
72}

3. Bardziej zaawansowany hook do pobierania danych z pamięcią podręczną i mechanizmem ponownych prób

1// useDataFetching.js
2function useDataFetching(fetchFn, dependencies = [], options = {}) {
3  const {
4    initialData = null,
5    cacheKey = null,
6    maxRetries = 3,
7    retryDelay = 1000,
8  } = options;
9  
10  const [data, setData] = useState(initialData);
11  const [loading, setLoading] = useState(true);
12  const [error, setError] = useState(null);
13  
14  // Użycie useRef do śledzenia prób
15  const retriesRef = useRef(0);
16  
17  // Sprawdzenie cache przy pierwszym renderowaniu
18  useEffect(() => {
19    if (cacheKey) {
20      const cachedData = localStorage.getItem(cacheKey);
21      if (cachedData) {
22        try {
23          setData(JSON.parse(cachedData));
24          setLoading(false);
25        } catch (e) {
26          console.error('Błąd parsowania danych z cache:', e);
27        }
28      }
29    }
30  }, [cacheKey]);
31  
32  // Główna logika pobierania danych
33  useEffect(() => {
34    let isMounted = true;
35    const abortController = new AbortController();
36    
37    async function fetchData() {
38      try {
39        setLoading(true);
40        
41        const result = await fetchFn(abortController.signal);
42        
43        if (isMounted) {
44          setData(result);
45          setError(null);
46          
47          // Zapisanie do cache, jeśli dostępne
48          if (cacheKey) {
49            localStorage.setItem(cacheKey, JSON.stringify(result));
50          }
51          
52          // Zresetuj licznik prób
53          retriesRef.current = 0;
54        }
55      } catch (err) {
56        if (!isMounted) return;
57        
58        if (err.name === 'AbortError') {
59          console.log('Żądanie anulowane');
60          return;
61        }
62        
63        // Logika ponownych prób
64        if (retriesRef.current < maxRetries) {
65          console.log(`Próba ${retriesRef.current + 1} z ${maxRetries} nie powiodła się. Ponowienie za ${retryDelay}ms`);
66          
67          setTimeout(() => {
68            if (isMounted) {
69              retriesRef.current += 1;
70              fetchData(); // Ponowna próba
71            }
72          }, retryDelay * Math.pow(2, retriesRef.current)); // Wykładniczy backoff
73          
74          return;
75        }
76        
77        // Po wyczerpaniu wszystkich prób
78        setError(`Nie udało się pobrać danych: ${err.message}`);
79        setData(initialData);
80      } finally {
81        if (isMounted) {
82          setLoading(false);
83        }
84      }
85    }
86    
87    fetchData();
88    
89    return () => {
90      isMounted = false;
91      abortController.abort();
92    };
93  }, [...dependencies]);
94  
95  // Funkcja do ręcznego odświeżania danych
96  const refetch = useCallback(async () => {
97    setLoading(true);
98    retriesRef.current = 0;
99    
100    try {
101      const abortController = new AbortController();
102      const result = await fetchFn(abortController.signal);
103      setData(result);
104      setError(null);
105      
106      if (cacheKey) {
107        localStorage.setItem(cacheKey, JSON.stringify(result));
108      }
109    } catch (err) {
110      setError(`Nie udało się odświeżyć danych: ${err.message}`);
111    } finally {
112      setLoading(false);
113    }
114  }, [fetchFn, cacheKey]);
115  
116  return { data, loading, error, refetch };
117}
118
119// Przykład użycia
120function MarsMissions() {
121  // Funkcja do pobrania danych
122  const fetchMissions = useCallback(async (signal) => {
123    const response = await fetch('https://api.spaceagency.com/mars/missions', { signal });
124    if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
125    return response.json();
126  }, []);
127  
128  // Użycie zaawansowanego hooka
129  const { 
130    data: missions, 
131    loading, 
132    error, 
133    refetch 
134  } = useDataFetching(fetchMissions, [], {
135    initialData: [],
136    cacheKey: 'mars-missions-cache',
137    maxRetries: 2
138  });
139  
140  return (
141    <div className="mars-missions">
142      <div className="header">
143        <h2>Misje na Marsa</h2>
144        <button onClick={refetch} disabled={loading}>
145          {loading ? 'Odświeżanie...' : 'Odśwież'}
146        </button>
147      </div>
148      
149      {loading && missions.length === 0 && <div>Ładowanie misji...</div>}
150      {error && <div className="error">Błąd: {error}</div>}
151      
152      <ul className="missions-list">
153        {missions.map(mission => (
154          <li key={mission.id} className={`mission-item ${mission.status}`}>
155            <h3>{mission.name}</h3>
156            <p>Start: {new Date(mission.launchDate).toLocaleDateString()}</p>
157            <p>Status: {mission.status}</p>
158          </li>
159        ))}
160      </ul>
161      
162      {missions.length === 0 && !loading && !error && (
163        <p>Nie znaleziono żadnych misji.</p>
164      )}
165    </div>
166  );
167}

W tym zaawansowanym przykładzie:

  1. Używamy własnego hooka
    useDataFetching
    z obsługą ponownych prób, pamięcią podręczną i funkcją ręcznego odświeżania
  2. Implementujemy wykładniczy backoff przy ponawianiu prób (każda kolejna próba czeka dłużej)
  3. Wykorzystujemy parametry, aby dostosować zachowanie hooka do różnych przypadków użycia
  4. Obsługujemy zarówno początkowe ładowanie, jak i ręczne odświeżanie danych

Obsługa błędów przy pobieraniu danych

Poza podstawową obsługą błędów w komponentach, warto zaimplementować bardziej zaawansowane techniki:

1. Komponent granicy błędów (Error Boundary)

Granice błędów pozwalają na elegancką obsługę nieoczekiwanych błędów:

1// ErrorBoundary.js
2class ErrorBoundary extends React.Component {
3  constructor(props) {
4    super(props);
5    this.state = { hasError: false, error: null, errorInfo: null };
6  }
7
8  static getDerivedStateFromError(error) {
9    // Aktualizacja stanu, aby następny render pokazał UI awaryjne
10    return { hasError: true };
11  }
12
13  componentDidCatch(error, errorInfo) {
14    // Możesz też zalogować błąd do serwisu monitoringu
15    console.error('Przechwycono błąd:', error, errorInfo);
16    this.setState({ error, errorInfo });
17    
18    // Wysyłanie błędu do serwisu raportowania
19    // logErrorToService(error, errorInfo);
20  }
21
22  render() {
23    if (this.state.hasError) {
24      // Możesz wyrenderować dowolny UI awaryjny
25      return (
26        <div className="error-boundary">
27          <h2>Coś poszło nie tak!</h2>
28          <p>Wystąpił błąd podczas renderowania komponentu.</p>
29          <details>
30            <summary>Szczegóły błędu</summary>
31            <p>{this.state.error && this.state.error.toString()}</p>
32            <p>Komponenty: {this.state.errorInfo && this.state.errorInfo.componentStack}</p>
33          </details>
34          <button onClick={() => this.setState({ hasError: false })}>
35            Spróbuj ponownie
36          </button>
37        </div>
38      );
39    }
40
41    return this.props.children;
42  }
43}
44
45// Użycie error boundary dla komponentu
46function App() {
47  return (
48    <div className="app">
49      <header>Centrum kontroli misji</header>
50      
51      <ErrorBoundary>
52        <MissionDashboard missionId="mars-2030" />
53      </ErrorBoundary>
54      
55      <ErrorBoundary>
56        <AstronautsList />
57      </ErrorBoundary>
58      
59      <footer>Space Agency © 2023</footer>
60    </div>
61  );
62}

2. Strategie ponownych prób

Gdy API jest niestabilne, warto implementować strategie automatycznych ponownych prób:

1async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
2  let retries = 0;
3  
4  while (retries < maxRetries) {
5    try {
6      const response = await fetch(url, options);
7      
8      if (!response.ok) {
9        throw new Error(`HTTP Error: ${response.status}`);
10      }
11      
12      return await response.json();
13    } catch (err) {
14      retries++;
15      
16      if (retries >= maxRetries) {
17        throw err; // Wszystkie próby nieudane, przekaż błąd dalej
18      }
19      
20      console.log(`Próba ${retries} z ${maxRetries} nieudana. Ponowienie za ${delay}ms`);
21      
22      // Odczekaj przed kolejną próbą (wykładniczy backoff)
23      await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, retries - 1)));
24    }
25  }
26}

3. Obsługa problemów z połączeniem

Warto również obsługiwać problemy z połączeniem internetowym:

1// Hook do monitorowania stanu połączenia
2function useOnlineStatus() {
3  const [isOnline, setIsOnline] = useState(navigator.onLine);
4  
5  useEffect(() => {
6    function handleOnline() {
7      setIsOnline(true);
8    }
9    
10    function handleOffline() {
11      setIsOnline(false);
12    }
13    
14    window.addEventListener('online', handleOnline);
15    window.addEventListener('offline', handleOffline);
16    
17    return () => {
18      window.removeEventListener('online', handleOnline);
19      window.removeEventListener('offline', handleOffline);
20    };
21  }, []);
22  
23  return isOnline;
24}
25
26// Użycie w komponencie
27function DataFetchingComponent() {
28  const isOnline = useOnlineStatus();
29  const { data, loading, error, refetch } = useFetch('https://api.example.com/data');
30  
31  if (!isOnline) {
32    return (
33      <div className="offline-message">
34        <h3>Brak połączenia z internetem</h3>
35        <p>Sprawdź połączenie i spróbuj ponownie.</p>
36      </div>
37    );
38  }
39  
40  if (loading) return <div>Ładowanie...</div>;
41  if (error) return <div>Błąd: {error}</div>;
42  
43  return (
44    <div>
45      <h2>Dane</h2>
46      {/* Renderowanie danych */}
47    </div>
48  );
49}

Symulacja API podczas developmentu

Podczas opracowywania aplikacji, rzeczywiste API może być niedostępne lub niegotowe. W takim przypadku warto zasymulować API:

1. Prosta symulacja z opóźnieniem

1// mockApi.js
2const DELAY = 1000; // Symulacja opóźnienia sieci
3
4export function mockFetch(data, shouldFail = false, failureRate = 0) {
5  return new Promise((resolve, reject) => {
6    setTimeout(() => {
7      // Symulacja losowego błędu
8      if (shouldFail || Math.random() < failureRate) {
9        reject(new Error('Symulowany błąd API'));
10        return;
11      }
12      
13      resolve(data);
14    }, DELAY);
15  });
16}
17
18// Przykładowe dane
19const MOCK_ASTRONAUTS = [
20  { id: 1, name: 'Jan Kowalski', role: 'Dowódca' },
21  { id: 2, name: 'Anna Nowak', role: 'Pilot' },
22  { id: 3, name: 'Piotr Wiśniewski', role: 'Inżynier pokładowy' }
23];
24
25export function getAstronauts() {
26  return mockFetch(MOCK_ASTRONAUTS, false, 0.2);
27}

2. Bardziej rozbudowana symulacja z MSW (Mock Service Worker)

MSW to biblioteka, która pozwala symulować żądania API na poziomie przeglądarki:

1// mockServiceWorker.js
2import { setupWorker, rest } from 'msw';
3
4// Dane do symulacji
5const astronauts = [
6  { id: 1, name: 'Jan Kowalski', role: 'Dowódca' },
7  { id: 2, name: 'Anna Nowak', role: 'Pilot' },
8  { id: 3, name: 'Piotr Wiśniewski', role: 'Inżynier pokładowy' }
9];
10
11const missions = [
12  { 
13    id: 'mars-2030', 
14    name: 'Mars 2030', 
15    status: 'Planowana',
16    launchDate: '2030-05-15',
17    commander: 'Jan Kowalski'
18  },
19  { 
20    id: 'iss-2023', 
21    name: 'ISS Expedition 70', 
22    status: 'W trakcie',
23    launchDate: '2023-09-01',
24    commander: 'Anna Nowak'
25  }
26];
27
28// Definiowanie handlerów
29export const handlers = [
30  // Pobieranie wszystkich astronautów
31  rest.get('https://api.spaceagency.com/astronauts', (req, res, ctx) => {
32    return res(
33      ctx.delay(1000),
34      ctx.status(200),
35      ctx.json(astronauts)
36    );
37  }),
38  
39  // Pobieranie astronauty po ID
40  rest.get('https://api.spaceagency.com/astronauts/:id', (req, res, ctx) => {
41    const { id } = req.params;
42    const astronaut = astronauts.find(a => a.id === parseInt(id));
43    
44    if (!astronaut) {
45      return res(
46        ctx.delay(500),
47        ctx.status(404),
48        ctx.json({ error: 'Astronauta nie znaleziony' })
49      );
50    }
51    
52    return res(
53      ctx.delay(500),
54      ctx.status(200),
55      ctx.json(astronaut)
56    );
57  }),
58  
59  // Pobieranie misji
60  rest.get('https://api.spaceagency.com/missions', (req, res, ctx) => {
61    return res(
62      ctx.delay(800),
63      ctx.status(200),
64      ctx.json(missions)
65    );
66  }),
67  
68  // Pobieranie misji po ID
69  rest.get('https://api.spaceagency.com/missions/:id', (req, res, ctx) => {
70    const { id } = req.params;
71    const mission = missions.find(m => m.id === id);
72    
73    if (!mission) {
74      return res(
75        ctx.delay(500),
76        ctx.status(404),
77        ctx.json({ error: 'Misja nie znaleziona' })
78      );
79    }
80    
81    return res(
82      ctx.delay(700),
83      ctx.status(200),
84      ctx.json(mission)
85    );
86  })
87];
88
89// Inicjalizacja service workera
90export const worker = setupWorker(...handlers);

Następnie w pliku wejściowym aplikacji (np.

index.js
):

1// index.js
2import React from 'react';
3import { createRoot } from 'react-dom/client';
4import App from './App';
5
6// Warunkowo inicjalizuj mock API tylko w środowisku deweloperskim
7if (process.env.NODE_ENV === 'development') {
8  const { worker } = require('./mockServiceWorker');
9  worker.start();
10}
11
12const root = createRoot(document.getElementById('root'));
13root.render(<App />);

Podsumowanie

Pobieranie danych z API jest fundamentalną częścią większości aplikacji React. Klucz do sukcesu to:

  1. Organizacja kodu - wydzielenie logiki API od komponentów
  2. Własne hooki - tworzenie abstrakcji dla powtarzających się operacji pobierania danych
  3. Obsługa wszystkich stanów - ładowanie, sukces, błąd, offline
  4. Czyszczenie zasobów - anulowanie żądań, zapobieganie aktualizacji stanu po odmontowaniu
  5. Strategie retry - automatyczne ponawianie nieudanych żądań
  6. Testowanie - symulacja API podczas developmentu i testów

Pamiętaj, że dobra obsługa pobierania danych znacząco wpływa na odczucia użytkownika. Nawet jeśli API jest wolne, odpowiednio zaprojektowany interfejs z informacjami o stanie ładowania, animacjami i obsługą błędów może sprawić, że aplikacja będzie wydawać się szybsza i bardziej niezawodna.

Ir a CodeWorlds