Cours JavaScript et React · Module 5: useEffect et hooks personnalisés
Gestion des API et récupération de données
Dans cette leçon6
Communiquer avec des API externes est l'un des cas d'usage les plus courants des effets de bord dans les applications React. Dans ce module, nous allons apprendre à récupérer, traiter et gérer efficacement les données provenant de sources externes.
Les bases de la récupération de données depuis une API
Dans les applications web, les données proviennent souvent d'API externes qui fournissent des informations au format JSON, XML ou autre. Dans notre voyage cosmique, nous pouvons imaginer un centre de contrôle de mission récupérant des données auprès de divers satellites, vaisseaux et stations de recherche.
Exemple simple de récupération de données avec 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 // Fonction asynchrone pour récupérer les données
10 async function fetchAstronauts() {
11 try {
12 setLoading(true);
13 // Récupération des données
14 const response = await fetch('https://api.spaceagency.com/astronauts');
15
16 // Vérifier si la réponse est OK
17 if (!response.ok) {
18 throw new Error(`Erreur HTTP : ${response.status}`);
19 }
20
21 // Analyser la réponse JSON
22 const data = await response.json();
23
24 // Mettre à jour l'état
25 setAstronauts(data);
26 setError(null);
27 } catch (err) {
28 // Gestion des erreurs
29 setError(`Échec de la récupération des données : ${err.message}`);
30 setAstronauts([]);
31 } finally {
32 // Marquer le chargement comme terminé
33 setLoading(false);
34 }
35 }
36
37 // Appeler la fonction
38 fetchAstronauts();
39 }, []); // Tableau de dépendances vide - exécuter une seule fois
40
41 // Rendu basé sur l'état
42 if (loading) return <div>Chargement des données...</div>;
43 if (error) return <div>Erreur : {error}</div>;
44
45 return (
46 <div>
47 <h2>Astronautes actifs</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}Dans l'exemple ci-dessus :
- Nous utilisons
useStatepour stocker les données, l'état de chargement et les erreurs - Nous utilisons
useEffectavec un tableau de dépendances vide pour récupérer les données une seule fois après le montage - Nous utilisons
async/awaitpour gérer la récupération asynchrone des données - Nous gérons les différents états (chargement, erreur, succès) dans l'interface utilisateur
Techniques avancées de récupération de données
1. Récupérer des données avec des paramètres
Souvent, nous devons récupérer des données en fonction de paramètres qui peuvent changer :
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; // Indicateur pour empêcher les mises à jour d'état après démontage
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(`Erreur HTTP : ${response.status}`);
19 }
20
21 const data = await response.json();
22
23 // Mettre à jour l'état uniquement si le composant est toujours monté
24 if (isMounted) {
25 setMission(data);
26 setError(null);
27 }
28 } catch (err) {
29 if (isMounted) {
30 setError(`Échec de la récupération des données de mission : ${err.message}`);
31 setMission(null);
32 }
33 } finally {
34 if (isMounted) {
35 setLoading(false);
36 }
37 }
38 }
39
40 fetchMission();
41
42 // Fonction de nettoyage
43 return () => {
44 isMounted = false; // Marquer que le composant a été démonté
45 };
46 }, [missionId]); // Re-récupérer les données lorsque missionId change
47
48 // Rendu
49 if (loading) return <div>Chargement des données de mission...</div>;
50 if (error) return <div>Erreur : {error}</div>;
51 if (!mission) return <div>Mission introuvable</div>;
52
53 return (
54 <div className="mission-details">
55 <h2>{mission.name}</h2>
56 <p>Statut : {mission.status}</p>
57 <p>Date de lancement : {new Date(mission.launchDate).toLocaleDateString()}</p>
58 <p>Commandant : {mission.commander}</p>
59 {/* Plus de détails sur la mission */}
60 </div>
61 );
62}Dans cet exemple :
- Nous récupérons les données en fonction de
missionId, qui peut changer - Nous utilisons un indicateur
isMountedpour empêcher les mises à jour d'état après démontage - Nous ajoutons une fonction de nettoyage qui met
isMounted = falseau démontage
2. Annuler des requêtes fetch
L'API Fetch n'a pas de mécanisme d'annulation intégré, mais nous pouvons utiliser AbortController pour interrompre une requête :
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 // Créer un AbortController pour annuler la requête
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 } // Assigner le signal à la requête
18 );
19
20 if (!response.ok) {
21 throw new Error(`Erreur HTTP : ${response.status}`);
22 }
23
24 const data = await response.json();
25 setWeatherData(data);
26 setError(null);
27 } catch (err) {
28 // Ignorer les erreurs d'annulation
29 if (err.name === 'AbortError') {
30 console.log('La requête météo a été annulée');
31 } else {
32 setError(`Échec de la récupération des données météo : ${err.message}`);
33 setWeatherData(null);
34 }
35 } finally {
36 setLoading(false);
37 }
38 }
39
40 fetchWeatherData();
41
42 // La fonction de nettoyage annule la requête si le composant
43 // est démonté avant la fin de la récupération
44 return () => {
45 abortController.abort();
46 };
47 }, [location]);
48
49 // Rendu
50 if (loading) return <div>Chargement des données météo...</div>;
51 if (error) return <div>Erreur : {error}</div>;
52 if (!weatherData) return <div>Aucune donnée météo disponible</div>;
53
54 return (
55 <div className="space-weather">
56 <h2>Météo spatiale : {location}</h2>
57 <p>Radiation : {weatherData.radiation} mSv</p>
58 <p>Activité solaire : {weatherData.solarActivity}</p>
59 <p>Température : {weatherData.temperature}°C</p>
60 </div>
61 );
62}Dans cet exemple :
- Nous utilisons
AbortControllerpour créer un signal qui peut interrompre une requête Fetch - Nous passons le signal aux options de la requête Fetch
- Dans la fonction de nettoyage, nous appelons
abortController.abort()pour annuler une requête en cours - Nous gérons spécifiquement l'
AbortErrorqui est levée lorsqu'une requête est annulée
3. Gérer plusieurs requêtes simultanées
Parfois, nous devons récupérer des données depuis plusieurs sources en même temps :
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 // Récupérer les données en parallèle avec 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 // Vérifier les réponses
25 if (!detailsResponse.ok || !crewResponse.ok || !telemetryResponse.ok) {
26 throw new Error('Une ou plusieurs requêtes ont échoué');
27 }
28
29 // Analyser les données en parallèle
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(`Échec de la récupération des données de mission : ${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 // Rendu
59 if (loading) return <div>Chargement des données de mission...</div>;
60 if (error) return <div>Erreur : {error}</div>;
61
62 const { details, crew, telemetry } = missionData;
63
64 return (
65 <div className="mission-dashboard">
66 <h1>{details.name} - Panneau de contrôle</h1>
67
68 <section className="mission-details">
69 <h2>Détails de la mission</h2>
70 <p>Statut : {details.status}</p>
71 <p>Objectif : {details.objective}</p>
72 </section>
73
74 <section className="crew-info">
75 <h2>Équipage</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>Télémétrie</h2>
87 <p>Oxygène : {telemetry.oxygen}%</p>
88 <p>Carburant : {telemetry.fuel}%</p>
89 <p>Vitesse : {telemetry.velocity} km/h</p>
90 </section>
91 </div>
92 );
93}Dans cet exemple :
- Nous utilisons
Promise.all()pour exécuter plusieurs requêtes en parallèle - Nous rassemblons toutes les données dans un seul état
missionData - Nous gérons toutes les données dans un seul composant
Bonnes pratiques et modèles
1. Extraire la logique de récupération de données
Pour améliorer la lisibilité et la maintenance du code, il vaut la peine d'extraire la logique de récupération de données en dehors du composant :
1// api.js - Services 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(`Erreur HTTP : ${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(`Erreur HTTP : ${response.status}`);
16 }
17 return response.json();
18}
19
20// Composant avec logique API extraite
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(`Échec de la récupération des données : ${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 // Rendu
58 //
59}2. Créer des hooks personnalisés pour la récupération de données
Pour les opérations de récupération de données répétitives, la meilleure solution est de créer un hook personnalisé :
1// useFetch.js - Hook personnalisé pour la récupération de données
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(`Erreur HTTP : ${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(`Échec de la récupération des données : ${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// Utilisation du hook personnalisé
55function PlanetInfo({ planetId }) {
56 const { data: planet, loading, error } = useFetch(
57 `https://api.spaceagency.com/planets/${planetId}`
58 );
59
60 if (loading) return <div>Chargement...</div>;
61 if (error) return <div>Erreur : {error}</div>;
62 if (!planet) return <div>Planète introuvable</div>;
63
64 return (
65 <div className="planet-card">
66 <h2>{planet.name}</h2>
67 <p>Type : {planet.type}</p>
68 <p>Diamètre : {planet.diameter} km</p>
69 <p>Distance au Soleil : {planet.distanceFromSun} millions de km</p>
70 </div>
71 );
72}3. Hook de récupération de données plus avancé avec cache et mécanisme de réessai
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 // Utiliser useRef pour suivre les tentatives
15 const retriesRef = useRef(0);
16
17 // Vérifier le cache au premier rendu
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('Erreur d\'analyse des données en cache :', e);
27 }
28 }
29 }
30 }, [cacheKey]);
31
32 // Logique principale de récupération de données
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 // Sauvegarder dans le cache si disponible
48 if (cacheKey) {
49 localStorage.setItem(cacheKey, JSON.stringify(result));
50 }
51
52 // Réinitialiser le compteur de tentatives
53 retriesRef.current = 0;
54 }
55 } catch (err) {
56 if (!isMounted) return;
57
58 if (err.name === 'AbortError') {
59 console.log('Requête annulée');
60 return;
61 }
62
63 // Logique de réessai
64 if (retriesRef.current < maxRetries) {
65 console.log(`La tentative ${retriesRef.current + 1} sur ${maxRetries} a échoué. Nouvel essai dans ${retryDelay}ms`);
66
67 setTimeout(() => {
68 if (isMounted) {
69 retriesRef.current += 1;
70 fetchData(); // Réessayer
71 }
72 }, retryDelay * Math.pow(2, retriesRef.current)); // Backoff exponentiel
73
74 return;
75 }
76
77 // Après épuisement de toutes les tentatives
78 setError(`Échec de la récupération des données : ${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 // Fonction pour rafraîchir manuellement les données
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(`Échec du rafraîchissement des données : ${err.message}`);
111 } finally {
112 setLoading(false);
113 }
114 }, [fetchFn, cacheKey]);
115
116 return { data, loading, error, refetch };
117}
118
119// Exemple d'utilisation
120function MarsMissions() {
121 // Fonction pour récupérer les données
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(`Erreur HTTP : ${response.status}`);
125 return response.json();
126 }, []);
127
128 // Utilisation du hook avancé
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>Missions martiennes</h2>
144 <button onClick={refetch} disabled={loading}>
145 {loading ? 'Rafraîchissement...' : 'Rafraîchir'}
146 </button>
147 </div>
148
149 {loading && missions.length === 0 && <div>Chargement des missions...</div>}
150 {error && <div className="error">Erreur : {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>Lancement : {new Date(mission.launchDate).toLocaleDateString()}</p>
157 <p>Statut : {mission.status}</p>
158 </li>
159 ))}
160 </ul>
161
162 {missions.length === 0 && !loading && !error && (
163 <p>Aucune mission trouvée.</p>
164 )}
165 </div>
166 );
167}Dans cet exemple avancé :
- Nous utilisons un hook personnalisé
useDataFetchingavec prise en charge des réessais, du cache et du rafraîchissement manuel - Nous implémentons un backoff exponentiel pour les réessais (chaque tentative suivante attend plus longtemps)
- Nous utilisons des paramètres pour adapter le comportement du hook à différents cas d'usage
- Nous gérons à la fois le chargement initial et le rafraîchissement manuel des données
Gérer les erreurs lors de la récupération de données
Au-delà de la gestion de base des erreurs dans les composants, il vaut la peine d'implémenter des techniques plus avancées :
1. Le composant Error Boundary
Les error boundaries permettent de gérer élégamment les erreurs inattendues :
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 // Mettre à jour l'état pour que le prochain rendu affiche l'UI de secours
10 return { hasError: true };
11 }
12
13 componentDidCatch(error, errorInfo) {
14 // Tu peux aussi journaliser l'erreur vers un service de surveillance
15 console.error('Erreur interceptée :', error, errorInfo);
16 this.setState({ error, errorInfo });
17
18 // Envoyer l'erreur à un service de reporting
19 // logErrorToService(error, errorInfo);
20 }
21
22 render() {
23 if (this.state.hasError) {
24 // Tu peux afficher n'importe quelle UI de secours
25 return (
26 <div className="error-boundary">
27 <h2>Quelque chose s'est mal passé !</h2>
28 <p>Une erreur s'est produite lors du rendu du composant.</p>
29 <details>
30 <summary>Détails de l'erreur</summary>
31 <p>{this.state.error && this.state.error.toString()}</p>
32 <p>Composants : {this.state.errorInfo && this.state.errorInfo.componentStack}</p>
33 </details>
34 <button onClick={() => this.setState({ hasError: false })}>
35 Réessayer
36 </button>
37 </div>
38 );
39 }
40
41 return this.props.children;
42 }
43}
44
45// Utilisation d'une error boundary pour un composant
46function App() {
47 return (
48 <div className="app">
49 <header>Centre de contrôle de mission</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. Stratégies de réessai
Lorsqu'une API est instable, il vaut la peine d'implémenter des stratégies de réessai automatiques :
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(`Erreur HTTP : ${response.status}`);
10 }
11
12 return await response.json();
13 } catch (err) {
14 retries++;
15
16 if (retries >= maxRetries) {
17 throw err; // Toutes les tentatives ont échoué, propager l'erreur
18 }
19
20 console.log(`La tentative ${retries} sur ${maxRetries} a échoué. Nouvel essai dans ${delay}ms`);
21
22 // Attendre avant la prochaine tentative (backoff exponentiel)
23 await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, retries - 1)));
24 }
25 }
26}3. Gérer les problèmes de connectivité
Il vaut aussi la peine de gérer les problèmes de connexion internet :
1// Hook pour surveiller l'état de la connexion
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// Utilisation dans un composant
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>Pas de connexion internet</h3>
35 <p>Vérifie ta connexion et réessaie.</p>
36 </div>
37 );
38 }
39
40 if (loading) return <div>Chargement...</div>;
41 if (error) return <div>Erreur : {error}</div>;
42
43 return (
44 <div>
45 <h2>Données</h2>
46 {/* Affichage des données */}
47 </div>
48 );
49}Simuler une API pendant le développement
Pendant le développement de l'application, l'API réelle peut être indisponible ou pas encore prête. Dans ce cas, il vaut la peine de simuler l'API :
1. Simulation simple avec délai
1// mockApi.js
2const DELAY = 1000; // Simulation du délai réseau
3
4export function mockFetch(data, shouldFail = false, failureRate = 0) {
5 return new Promise((resolve, reject) => {
6 setTimeout(() => {
7 // Simulation d'une erreur aléatoire
8 if (shouldFail || Math.random() < failureRate) {
9 reject(new Error('Erreur API simulée'));
10 return;
11 }
12
13 resolve(data);
14 }, DELAY);
15 });
16}
17
18// Données d'exemple
19const MOCK_ASTRONAUTS = [
20 { id: 1, name: 'John Smith', role: 'Commandant' },
21 { id: 2, name: 'Anna Johnson', role: 'Pilote' },
22 { id: 3, name: 'Peter Williams', role: 'Ingénieur de vol' }
23];
24
25export function getAstronauts() {
26 return mockFetch(MOCK_ASTRONAUTS, false, 0.2);
27}2. Simulation plus sophistiquée avec MSW (Mock Service Worker)
MSW est une bibliothèque qui simule les requêtes API au niveau du navigateur :
1// mockServiceWorker.js
2import { setupWorker, rest } from 'msw';
3
4// Données de simulation
5const astronauts = [
6 { id: 1, name: 'John Smith', role: 'Commandant' },
7 { id: 2, name: 'Anna Johnson', role: 'Pilote' },
8 { id: 3, name: 'Peter Williams', role: 'Ingénieur de vol' }
9];
10
11const missions = [
12 {
13 id: 'mars-2030',
14 name: 'Mars 2030',
15 status: 'Planifiée',
16 launchDate: '2030-05-15',
17 commander: 'John Smith'
18 },
19 {
20 id: 'iss-2023',
21 name: 'ISS Expédition 70',
22 status: 'En cours',
23 launchDate: '2023-09-01',
24 commander: 'Anna Johnson'
25 }
26];
27
28// Définition des gestionnaires
29export const handlers = [
30 // Récupérer tous les astronautes
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 // Récupérer un astronaute par 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: 'Astronaute introuvable' })
49 );
50 }
51
52 return res(
53 ctx.delay(500),
54 ctx.status(200),
55 ctx.json(astronaut)
56 );
57 }),
58
59 // Récupérer les missions
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 // Récupérer une mission par 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: 'Mission introuvable' })
78 );
79 }
80
81 return res(
82 ctx.delay(700),
83 ctx.status(200),
84 ctx.json(mission)
85 );
86 })
87];
88
89// Initialiser le service worker
90export const worker = setupWorker(...handlers);Puis dans le fichier d'entrée de ton application (ex. index.js) :
1// index.js
2import React from 'react';
3import ReactDOM from 'react-dom';
4import App from './App';
5
6// Initialiser conditionnellement l'API simulée uniquement en environnement de développement
7if (process.env.NODE_ENV === 'development') {
8 const { worker } = require('./mockServiceWorker');
9 worker.start();
10}
11
12ReactDOM.render(<App />, document.getElementById('root'));Résumé
Récupérer des données depuis des API est une partie fondamentale de la plupart des applications React. La clé du succès, c'est :
- L'organisation du code - séparer la logique API des composants
- Les hooks personnalisés - créer des abstractions pour les opérations de récupération répétitives
- La gestion de tous les états - chargement, succès, erreur, hors ligne
- Le nettoyage des ressources - annuler les requêtes, empêcher les mises à jour d'état après démontage
- Les stratégies de réessai - réessayer automatiquement les requêtes échouées
- Les tests - simuler les API pendant le développement et les tests
Souviens-toi qu'une bonne gestion de la récupération de données a un impact significatif sur l'expérience utilisateur. Même si l'API est lente, une interface bien conçue avec des informations sur l'état de chargement, des animations et une gestion des erreurs peut donner l'impression que l'application est plus rapide et plus fiable.
Code de cette leçon : App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Custom hook pour récupérer des données
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 // Simulation d'un délai réseau
18 await new Promise(resolve => setTimeout(resolve, 1500));
19
20 // Simulation de données provenant de l'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 - annulation de la requête
35 return () => controller.abort();
36 }, [url]);
37
38 return { data, loading, error };
39}
40
41// Simulation d'une réponse API
42function getMockData(url) {
43 const endpoints = {
44 '/api/planets': [
45 { id: 1, name: 'Mars', distance: '225M km', status: 'Colonisation' },
46 { id: 2, name: 'Jupiter', distance: '628M km', status: 'Exploration' },
47 { id: 3, name: 'Saturne', distance: '1.2B km', status: 'Observation' },
48 { id: 4, name: 'Uranus', distance: '2.7B km', status: 'Planification' },
49 ],
50 '/api/missions': [
51 { id: 1, name: 'Apollo 25', crew: 6, status: 'En cours' },
52 { id: 2, name: 'Artemis 12', crew: 4, status: 'Préparation' },
53 { id: 3, name: 'Europa Clipper', crew: 0, status: 'Automatique' },
54 ],
55 '/api/astronauts': [
56 { id: 1, name: 'Anne Martin', role: 'Capitaine', missions: 5 },
57 { id: 2, name: 'Jean Dupont', role: 'Ingénieur', missions: 3 },
58 { id: 3, name: 'Marie Bernard', role: 'Scientifique', missions: 7 },
59 ],
60 };
61 return endpoints[url] || [];
62}
63
64// Composant affichant les planètes
65function PlanetsList() {
66 const { data, loading, error } = useFetch('/api/planets');
67
68 if (loading) return <div className="loading">Chargement des planètes...</div>;
69 if (error) return <div className="error">Erreur : {error}</div>;
70
71 return (
72 <div className="data-card">
73 <h2>Planètes</h2>
74 <ul>
75 {data?.map(planet => (
76 <li key={planet.id}>
77 <strong>{planet.name}</strong>
78 <span className="detail">Distance : {planet.distance}</span>
79 <span className="status">{planet.status}</span>
80 </li>
81 ))}
82 </ul>
83 </div>
84 );
85}
86
87// Composant affichant les missions
88function MissionsList() {
89 const { data, loading, error } = useFetch('/api/missions');
90
91 if (loading) return <div className="loading">Chargement des missions...</div>;
92 if (error) return <div className="error">Erreur : {error}</div>;
93
94 return (
95 <div className="data-card">
96 <h2>Missions</h2>
97 <ul>
98 {data?.map(mission => (
99 <li key={mission.id}>
100 <strong>{mission.name}</strong>
101 <span className="detail">Équipage : {mission.crew}</span>
102 <span className="status">{mission.status}</span>
103 </li>
104 ))}
105 </ul>
106 </div>
107 );
108}
109
110// Composant affichant les astronautes
111function AstronautsList() {
112 const { data, loading, error } = useFetch('/api/astronauts');
113
114 if (loading) return <div className="loading">Chargement des astronautes...</div>;
115 if (error) return <div className="error">Erreur : {error}</div>;
116
117 return (
118 <div className="data-card">
119 <h2>Astronautes</h2>
120 <ul>
121 {data?.map(astronaut => (
122 <li key={astronaut.id}>
123 <strong>{astronaut.name}</strong>
124 <span className="detail">Rôle : {astronaut.role}</span>
125 <span className="status">Missions : {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>Centre de données cosmiques</h1>
139 <button className="refresh-btn" onClick={() => setRefreshKey(k => k + 1)}>
140 Actualiser toutes les données
141 </button>
142 <div className="data-grid">
143 <PlanetsList />
144 <MissionsList />
145 <AstronautsList />
146 </div>
147 </div>
148 );
149}
150
151export default App;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la principale différence entre useEffect(() => {}, []) et componentDidMount ?
2. Que se passe-t-il lorsqu'on ne passe PAS de tableau de dépendances à useEffect (pas de second argument) ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
États : [data, setData], [loading, setLoading], [error, setError]. useEffect : setLoading(true), fetch('/api/satellite'), setData(result), setLoading(false). Try/catch pour les erreurs. Affiche : loading ? 'Chargement...' : data !
- Tri horizontal
Ordonne la syntaxe correcte de useEffect avec un tableau de dépendances :
- Éditeur de code
Système de communication WebSocket
- Tri vertical
Ordonne les phases du cycle de vie d'un composant React :
- Éditeur de code
Système de raccourcis clavier globaux