Cours JavaScript et React · Module 5: useEffect et hooks personnalisés

Gestion des API et récupération de données

16 min de lecture
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 :

  1. Nous utilisons useState pour stocker les données, l'état de chargement et les erreurs
  2. Nous utilisons useEffect avec un tableau de dépendances vide pour récupérer les données une seule fois après le montage
  3. Nous utilisons async/await pour gérer la récupération asynchrone des données
  4. 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 :

  1. Nous récupérons les données en fonction de missionId, qui peut changer
  2. Nous utilisons un indicateur isMounted pour empêcher les mises à jour d'état après démontage
  3. Nous ajoutons une fonction de nettoyage qui met isMounted = false au 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 :

  1. Nous utilisons AbortController pour créer un signal qui peut interrompre une requête Fetch
  2. Nous passons le signal aux options de la requête Fetch
  3. Dans la fonction de nettoyage, nous appelons abortController.abort() pour annuler une requête en cours
  4. Nous gérons spécifiquement l'AbortError qui 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 :

  1. Nous utilisons Promise.all() pour exécuter plusieurs requêtes en parallèle
  2. Nous rassemblons toutes les données dans un seul état missionData
  3. 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é :

  1. Nous utilisons un hook personnalisé useDataFetching avec prise en charge des réessais, du cache et du rafraîchissement manuel
  2. Nous implémentons un backoff exponentiel pour les réessais (chaque tentative suivante attend plus longtemps)
  3. Nous utilisons des paramètres pour adapter le comportement du hook à différents cas d'usage
  4. 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 :

  1. L'organisation du code - séparer la logique API des composants
  2. Les hooks personnalisés - créer des abstractions pour les opérations de récupération répétitives
  3. La gestion de tous les états - chargement, succès, erreur, hors ligne
  4. Le nettoyage des ressources - annuler les requêtes, empêcher les mises à jour d'état après démontage
  5. Les stratégies de réessai - réessayer automatiquement les requêtes échouées
  6. 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. 1. Quelle est la principale différence entre useEffect(() => {}, []) et componentDidMount ?

  2. 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

Articles utiles