Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Obsługa API i pobieranie danych
W tej lekcji6
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:
- Używamy
useStatedo przechowywania danych, stanu ładowania i błędów - Używamy
useEffectz pustą tablicą zależności, aby pobrać dane tylko raz po zamontowaniu komponentu - Wykorzystujemy
async/awaitdo obsługi asynchronicznego pobierania danych - 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:
- Pobieramy dane na podstawie
missionId, które może się zmieniać - Używamy flagi
isMounteddo zapobiegania aktualizacji stanu po odmontowaniu komponentu - Dodajemy funkcję czyszczącą, która ustawia
isMounted = falseprzy 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:
- Używamy
AbortControllerdo tworzenia sygnału, który może przerwać żądanie Fetch - Przekazujemy sygnał do opcji żądania Fetch
- W funkcji czyszczącej wywołujemy
abortController.abort(), aby anulować trwające żądanie - 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:
- Używamy
Promise.all()do równoległego wykonywania wielu żądań - Gromadzimy wszystkie dane w jednym stanie
missionData - 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:
- Używamy własnego hooka
useDataFetchingz obsługą ponownych prób, pamięcią podręczną i funkcją ręcznego odświeżania - Implementujemy wykładniczy backoff przy ponawianiu prób (każda kolejna próba czeka dłużej)
- Wykorzystujemy parametry, aby dostosować zachowanie hooka do różnych przypadków użycia
- 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:
- Organizacja kodu - wydzielenie logiki API od komponentów
- Własne hooki - tworzenie abstrakcji dla powtarzających się operacji pobierania danych
- Obsługa wszystkich stanów - ładowanie, sukces, błąd, offline
- Czyszczenie zasobów - anulowanie żądań, zapobieganie aktualizacji stanu po odmontowaniu
- Strategie retry - automatyczne ponawianie nieudanych żądań
- 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.
Kod do tej lekcji: App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Custom hook do pobierania danych
4function useFetch(url, options = {}) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 const controller = new AbortController();
11
12 const fetchData = async () => {
13 setLoading(true);
14 setError(null);
15
16 try {
17 // Symulacja opóźnienia sieciowego
18 await new Promise(resolve => setTimeout(resolve, 1500));
19
20 // Symulacja danych z API
21 const mockData = getMockData(url);
22 setData(mockData);
23 } catch (err) {
24 if (err.name !== 'AbortError') {
25 setError(err.message);
26 }
27 } finally {
28 setLoading(false);
29 }
30 };
31
32 fetchData();
33
34 // Cleanup - anulowanie żądania
35 return () => controller.abort();
36 }, [url]);
37
38 return { data, loading, error };
39}
40
41// Symulacja odpowiedzi API
42function getMockData(url) {
43 const endpoints = {
44 '/api/planets': [
45 { id: 1, name: 'Mars', distance: '225M km', status: 'Kolonizacja' },
46 { id: 2, name: 'Jowisz', distance: '628M km', status: 'Eksploracja' },
47 { id: 3, name: 'Saturn', distance: '1.2B km', status: 'Obserwacja' },
48 { id: 4, name: 'Uran', distance: '2.7B km', status: 'Planowanie' },
49 ],
50 '/api/missions': [
51 { id: 1, name: 'Apollo 25', crew: 6, status: 'W toku' },
52 { id: 2, name: 'Artemis 12', crew: 4, status: 'Przygotowanie' },
53 { id: 3, name: 'Europa Clipper', crew: 0, status: 'Automatyczna' },
54 ],
55 '/api/astronauts': [
56 { id: 1, name: 'Anna Kowalska', role: 'Kapitan', missions: 5 },
57 { id: 2, name: 'Jan Nowak', role: 'Inżynier', missions: 3 },
58 { id: 3, name: 'Maria Wiśniewska', role: 'Naukowiec', missions: 7 },
59 ],
60 };
61 return endpoints[url] || [];
62}
63
64// Komponent wyświetlający planety
65function PlanetsList() {
66 const { data, loading, error } = useFetch('/api/planets');
67
68 if (loading) return <div className="loading">Ładowanie planet...</div>;
69 if (error) return <div className="error">Błąd: {error}</div>;
70
71 return (
72 <div className="data-card">
73 <h2>Planety</h2>
74 <ul>
75 {data?.map(planet => (
76 <li key={planet.id}>
77 <strong>{planet.name}</strong>
78 <span className="detail">Dystans: {planet.distance}</span>
79 <span className="status">{planet.status}</span>
80 </li>
81 ))}
82 </ul>
83 </div>
84 );
85}
86
87// Komponent wyświetlający misje
88function MissionsList() {
89 const { data, loading, error } = useFetch('/api/missions');
90
91 if (loading) return <div className="loading">Ładowanie misji...</div>;
92 if (error) return <div className="error">Błąd: {error}</div>;
93
94 return (
95 <div className="data-card">
96 <h2>Misje</h2>
97 <ul>
98 {data?.map(mission => (
99 <li key={mission.id}>
100 <strong>{mission.name}</strong>
101 <span className="detail">Załoga: {mission.crew}</span>
102 <span className="status">{mission.status}</span>
103 </li>
104 ))}
105 </ul>
106 </div>
107 );
108}
109
110// Komponent wyświetlający astronautów
111function AstronautsList() {
112 const { data, loading, error } = useFetch('/api/astronauts');
113
114 if (loading) return <div className="loading">Ładowanie astronautów...</div>;
115 if (error) return <div className="error">Błąd: {error}</div>;
116
117 return (
118 <div className="data-card">
119 <h2>Astronauci</h2>
120 <ul>
121 {data?.map(astronaut => (
122 <li key={astronaut.id}>
123 <strong>{astronaut.name}</strong>
124 <span className="detail">Rola: {astronaut.role}</span>
125 <span className="status">Misje: {astronaut.missions}</span>
126 </li>
127 ))}
128 </ul>
129 </div>
130 );
131}
132
133function App() {
134 const [refreshKey, setRefreshKey] = useState(0);
135
136 return (
137 <div className="app" key={refreshKey}>
138 <h1>Centrum Danych Kosmicznych</h1>
139 <button className="refresh-btn" onClick={() => setRefreshKey(k => k + 1)}>
140 Odśwież wszystkie dane
141 </button>
142 <div className="data-grid">
143 <PlanetsList />
144 <MissionsList />
145 <AstronautsList />
146 </div>
147 </div>
148 );
149}
150
151export default App;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaka jest główna różnica między useEffect(() => {}, []) a componentDidMount?
2. Co się stanie, gdy NIE przekażemy tablicy zależności do useEffect (brak drugiego argumentu)?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stany: [data, setData], [loading, setLoading], [error, setError]. useEffect: setLoading(true), fetch('/api/satellite'), setData(result), setLoading(false). Try/catch dla błędów. Wyświetl: loading ? 'Ładowanie...' : data!
- Układanie w poziomie
Ułóż poprawną składnię useEffect z tablicą zależności:
- Edytor kodu
System komunikacji WebSocket
- Układanie w pionie
Uporządkuj fazy życia komponentu React:
- Edytor kodu
System globalnych skrótów klawiszowych