Cours JavaScript et React · Module 11: Récupération de données et API
Les états de chargement, d'erreur et de données vides
Dans cette leçon4
Imagine un panneau sur la passerelle qui s'éteint tout simplement après l'envoi d'une requête. Tu ne sais pas si les données sont en route, si la transmission a été coupée ou si le Centre de contrôle n'a rien à transmettre. L'utilisateur de ton application ressent exactement la même chose face à un écran vide. Chaque requête vers un serveur se termine dans l'une de quatre situations : les données sont en route (chargement), le transfert a échoué (erreur), le centre n'a aucune donnée (résultat vide) ou les données sont arrivées (succès). Chacune a besoin de sa propre vue.
Le pattern Loading/Error/Data
Le pattern le plus populaire en React repose sur trois variables d'état issues de useState : data pour le résultat, loading comme drapeau « on attend » et error pour le message d'erreur. On envoie la requête dans useEffect, et un bloc try/catch/finally garantit que chaque chemin met à jour le bon état. D'abord, la partie qui récupère les données :
1import React, { useState, useEffect } from 'react';
2
3function StarshipCatalog() {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 async function fetchStarships() {
10 try {
11 setLoading(true);
12 setError(null);
13
14 const response = await fetch(
15 'https://swapi.dev/api/starships/'
16 );
17
18 if (!response.ok) {
19 throw new Error(`HTTP Error: ${response.status}`);
20 }
21
22 const result = await response.json();
23 setData(result.results);
24 } catch (err) {
25 setError(err.message);
26 } finally {
27 setLoading(false);
28 }
29 }
30
31 fetchStarships();
32 }, []);loading démarre à true, car la requête part juste après le premier rendu. Le bloc finally s'exécute après un succès comme après une erreur, donc le drapeau de chargement ne reste jamais « bloqué ». Remarque aussi le throw manuel pour !response.ok : sans lui, une réponse 500 arriverait dans setData comme un succès.
La seconde moitié du même composant décide quoi afficher. L'ordre des vérifications compte : d'abord le chargement, puis l'erreur, puis le résultat vide, et seulement à la fin les données :
1 // État de chargement
2 if (loading) {
3 return <div className="loading">Scan du secteur...</div>;
4 }
5
6 // État d'erreur
7 if (error) {
8 return <div className="error">Erreur de communication : {error}</div>;
9 }
10
11 // État de données vides
12 if (!data || data.length === 0) {
13 return <div className="empty">Aucun vaisseau dans ce secteur</div>;
14 }
15
16 // Données prêtes à être affichées
17 return (
18 <div>
19 <h2>Vaisseaux détectés : {data.length}</h2>
20 {data.map(ship => (
21 <div key={ship.name}>
22 <h3>{ship.name}</h3>
23 <p>Modèle : {ship.model}</p>
24 </div>
25 ))}
26 </div>
27 );
28}Grâce aux return anticipés, le dernier fragment ne s'exécute que lorsque les données existent vraiment, donc data.map n'explose pas sur null. Si tu vérifiais le résultat vide avant le chargement, l'utilisateur verrait brièvement « Aucun vaisseau » avant même l'arrivée des données.
Rendu conditionnel des états
Des textes bruts fonctionnent, mais chaque état mérite son propre petit composant. Tout comme un vaisseau est fait de modules, une interface est faite de briques que tu peux réutiliser dans toute ta galaxie d'applications :
1function LoadingSpinner() {
2 return (
3 <div className="spinner-container">
4 <div className="spinner"></div>
5 <p>Chargement des données du centre de commande...</p>
6 </div>
7 );
8}
9
10function ErrorMessage({ message, onRetry }) {
11 return (
12 <div className="error-container">
13 <h3>Connexion perdue !</h3>
14 <p>{message}</p>
15 <button onClick={onRetry}>
16 Réessayer
17 </button>
18 </div>
19 );
20}
21
22function EmptyState() {
23 return (
24 <div className="empty-container">
25 <p>Aucune donnée dans ce quadrant de la galaxie</p>
26 <p>Essaie de scanner un autre secteur</p>
27 </div>
28 );
29}ErrorMessage reçoit une prop onRetry, une fonction appelée au clic sur le bouton. Le composant lui-même ne sait pas comment relancer la requête, et c'est tant mieux : c'est le parent qui en décide.
Mécanisme de réessai (retry)
Pour que le bouton « Réessayer » ait quelque chose à appeler, la fonction de récupération doit être accessible en dehors de l'effet. On la remonte donc d'un niveau, dans le corps du composant :
1function SpaceData() {
2 const [data, setData] = useState(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 const fetchData = async () => {
7 try {
8 setLoading(true);
9 setError(null);
10 const response = await fetch(
11 'https://swapi.dev/api/people/'
12 );
13 if (!response.ok) throw new Error('Erreur réseau');
14 const result = await response.json();
15 setData(result.results);
16 } catch (err) {
17 setError(err.message);
18 } finally {
19 setLoading(false);
20 }
21 };
22
23 useEffect(() => {
24 fetchData();
25 }, []);
26
27 if (loading) return <LoadingSpinner />;
28 if (error) return <ErrorMessage message={error} onRetry={fetchData} />;
29 if (!data || data.length === 0) return <EmptyState />;
30
31 return (
32 <ul>
33 {data.map(person => (
34 <li key={person.name}>{person.name}</li>
35 ))}
36 </ul>
37 );
38}Différence clé : la fonction fetchData est définie en dehors de useEffect, ce qui permet de l'appeler à nouveau depuis le bouton « Réessayer ». La logique de récupération n'a pas changé, seul son emplacement a changé. Le plugin ESLint react-hooks peut signaler ici que fetchData manque dans les dépendances. Tant que la fonction n'utilise pas de props ni d'état qui changent, l'effet fonctionne quand même correctement.
Skeleton Loading
Un spinner dit « attends », mais pas quoi. Un squelette d'interface, des rectangles gris à la place des futures cartes, montre la forme du contenu et fait moins « sauter » la page quand les données arrivent :
1function SkeletonCard() {
2 return (
3 <div className="skeleton-card">
4 <div className="skeleton-title"></div>
5 <div className="skeleton-text"></div>
6 <div className="skeleton-text short"></div>
7 </div>
8 );
9}
10
11function PlanetCatalog() {
12 const [planets, setPlanets] = useState(null);
13 const [loading, setLoading] = useState(true);
14
15 // ... fetch logic ...
16
17 if (loading) {
18 return (
19 <div>
20 <SkeletonCard />
21 <SkeletonCard />
22 <SkeletonCard />
23 </div>
24 );
25 }
26
27 return (
28 <div>
29 {planets.map(planet => (
30 <PlanetCard key={planet.name} planet={planet} />
31 ))}
32 </div>
33 );
34}La logique de récupération est identique à la précédente (d'où le simple commentaire), seule la vue du chargement change. Je te recommande les squelettes pour les listes et les cartes, et de garder le spinner pour les actions courtes, comme l'envoi d'un formulaire.
Une bonne gestion des états, c'est la différence entre une application professionnelle et un projet amateur. Dans la prochaine leçon, tu verras ce qui se passe quand plusieurs requêtes font la course, et comment AbortController protège ces mêmes trois états contre les données obsolètes.
Retiens : un pilote voit toujours l'état de ses systèmes sur le panneau, donc ton utilisateur doit toujours savoir, lui aussi, s'il attend, si quelque chose a cassé ou si rien n'a été trouvé.
Code de cette leçon : App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Composants d'états
4function LoadingSpinner() {
5 return (
6 <div className="state-container loading">
7 <div className="spinner"></div>
8 <p>Scan du secteur spatial...</p>
9 </div>
10 );
11}
12
13function ErrorMessage({ message, onRetry }) {
14 return (
15 <div className="state-container error">
16 <h3>Connexion avec le centre de contrôle perdue !</h3>
17 <p>{message}</p>
18 <button className="retry-btn" onClick={onRetry}>
19 Relancer le scan
20 </button>
21 </div>
22 );
23}
24
25function EmptyState() {
26 return (
27 <div className="state-container empty">
28 <h3>Secteur vide</h3>
29 <p>Aucun vaisseau détecté dans ce quadrant</p>
30 </div>
31 );
32}
33
34function StarshipCatalog() {
35 const [data, setData] = useState(null);
36 const [loading, setLoading] = useState(true);
37 const [error, setError] = useState(null);
38
39 const fetchStarships = async () => {
40 try {
41 setLoading(true);
42 setError(null);
43
44 const response = await fetch('https://swapi.dev/api/starships/');
45
46 if (!response.ok) {
47 throw new Error(`Erreur HTTP : ${response.status}`);
48 }
49
50 const result = await response.json();
51 setData(result.results);
52 } catch (err) {
53 setError(err.message);
54 } finally {
55 setLoading(false);
56 }
57 };
58
59 useEffect(() => {
60 fetchStarships();
61 }, []);
62
63 if (loading) return <LoadingSpinner />;
64 if (error) return <ErrorMessage message={error} onRetry={fetchStarships} />;
65 if (!data || data.length === 0) return <EmptyState />;
66
67 return (
68 <div className="catalog">
69 <h1>Catalogue de vaisseaux spatiaux</h1>
70 <p>{data.length} vaisseaux détectés</p>
71 <div className="ship-grid">
72 {data.map(ship => (
73 <div key={ship.name} className="ship-card">
74 <h3>{ship.name}</h3>
75 <p><strong>Modèle :</strong> {ship.model}</p>
76 <p><strong>Constructeur :</strong> {ship.manufacturer}</p>
77 <p><strong>Vitesse :</strong> {ship.max_atmosphering_speed}</p>
78 <p><strong>Équipage :</strong> {ship.crew}</p>
79 </div>
80 ))}
81 </div>
82 </div>
83 );
84}
85
86export default StarshipCatalog;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. Dans quel ordre faut-il vérifier les états dans un composant qui récupère des données ?
2. Combien de variables d'état (useState) avons-nous besoin dans le pattern standard de data fetching en React ?
Tâches pratiques dans le jeu
- Éditeur de code
États de chargement, d'erreur et vide
- Tri horizontal
Ordonne la syntaxe async/await pour récupérer des données JSON depuis une API :
- Éditeur de code
Réessayer après une erreur