Cours JavaScript et React · Module 16: Gestion des erreurs et Suspense
UI de repli et skeleton loaders
Dans cette leçon5
Imagine que tu ouvres le panneau de navigation et que, pendant deux secondes, tu vois un écran noir et vide. Le système a-t-il planté ? Le vaisseau répond-il seulement ? Au bout d'un moment, tout le panneau surgit d'un coup et décale les boutons que tu visais justement. Ce n'est pas une panne, et pourtant l'équipage perd confiance dans la station. Dans l'espace, lorsqu'un module de la station s'initialise, l'équipage voit des indicateurs de progression et des contours holographiques de la future interface. Dans React, nous avons les skeleton loaders et l'UI de repli - des éléments d'interface fictifs qui informent l'utilisateur d'un chargement en cours.
Pourquoi l'UI de repli est-elle importante ?
Un bon chargement, ce n'est pas qu'un simple spinner. L'utilisateur doit avoir le sentiment qu'il se passe quelque chose et savoir à quoi s'attendre. Un fallback qui occupe autant de place que le contenu cible empêche aussi la mise en page de sauter quand les données arrivent enfin.
Niveaux de qualité du chargement (du pire au meilleur)
Compare quatre versions du même Suspense. Seule la prop fallback change, c'est-à-dire ce que React affichera tant que le composant à l'intérieur ne sera pas prêt :
1// 1. Rien (le pire) - l'utilisateur ne sait pas ce qui se passe
2<Suspense fallback={null}>
3
4// 2. Texte simple
5<Suspense fallback={<p>Chargement...</p>}>
6
7// 3. Spinner/Loader
8<Suspense fallback={<Spinner />}>
9
10// 4. Skeleton loader (le meilleur)
11<Suspense fallback={<ContentSkeleton />}>La mécanique de chargement est identique dans chaque variante, seul le message pour l'équipage diffère. Pour être précis : Suspense est un composant intégré de React qui affiche le fallback tant que quelque chose à l'intérieur "attend". Selon react.dev, il réagit aux composants chargés paresseusement via React.lazy() et aux promesses lues avec le hook use, et non à n'importe quel fetch dans un effet.
Les skeleton loaders
Un skeleton loader est une UI fictive qui imite la forme et la disposition du contenu cible. L'utilisateur voit un "squelette" de la page qui se remplit progressivement de contenu réel. C'est comme l'hologramme d'un module affiché avant que le module ne s'amarre physiquement à la station.
Skeleton de base en CSS
Le squelette d'une carte se compose de simples éléments div vides. Chacun reçoit une classe qui décrit sa forme et la classe d'animation commune skeleton-pulse :
1function CardSkeleton() {
2 return (
3 <div className="skeleton-card">
4 <div className="skeleton-image skeleton-pulse" />
5 <div className="skeleton-title skeleton-pulse" />
6 <div className="skeleton-text skeleton-pulse" />
7 <div className="skeleton-text skeleton-pulse short" />
8 </div>
9 );
10}Le composant n'a ni état ni props, car sa seule tâche est d'occuper la place. Tout l'effet visuel est assuré par le CSS. Le gradient linear-gradient crée une bande plus claire, background-size: 200% 100% rend le fond deux fois plus large que l'élément, et la règle @keyframes le déplace de droite à gauche :
1.skeleton-pulse {
2 background: linear-gradient(
3 90deg,
4 #e0e0e0 25%,
5 #f0f0f0 50%,
6 #e0e0e0 75%
7 );
8 background-size: 200% 100%;
9 animation: pulse 1.5s infinite;
10 border-radius: 4px;
11}
12
13@keyframes pulse {
14 0% { background-position: 200% 0; }
15 100% { background-position: -200% 0; }
16}
17
18.skeleton-image {
19 width: 100%;
20 height: 200px;
21 margin-bottom: 12px;
22}
23
24.skeleton-title {
25 width: 60%;
26 height: 24px;
27 margin-bottom: 8px;
28}
29
30.skeleton-text {
31 width: 100%;
32 height: 16px;
33 margin-bottom: 6px;
34}
35
36.skeleton-text.short {
37 width: 40%;
38}Les dimensions des classes .skeleton-image, .skeleton-title et .skeleton-text correspondent à la vraie carte, donc rien ne sautera après le chargement. La classe .short raccourcit la dernière ligne, car un vrai paragraphe se termine rarement pile au bord.
Des skeletons pour différents types de contenu
Un seul squelette ne convient pas à tout. La liste de l'équipage ne ressemble pas au tableau de bord des statistiques, donc nous construisons des formes distinctes.
Liste d'éléments
Le squelette de liste accepte la prop count, et Array.from({ length: count }) crée un tableau de cette longueur sur lequel nous pouvons itérer :
1function ListSkeleton({ count = 3 }) {
2 return (
3 <div className="list-skeleton">
4 {Array.from({ length: count }).map((_, i) => (
5 <div key={i} className="list-item-skeleton">
6 <div className="avatar-skeleton skeleton-pulse" />
7 <div className="info-skeleton">
8 <div className="skeleton-pulse name-skeleton" />
9 <div className="skeleton-pulse detail-skeleton" />
10 </div>
11 </div>
12 ))}
13 </div>
14 );
15}Ici, l'index comme key ne pose pas de problème, car les éléments ne changent jamais d'ordre et n'ont pas d'état propre.
Tableau de bord à plusieurs sections
Tu assembles les grands squelettes à partir de plus petits, exactement comme un vrai tableau de bord se compose de cartes et de listes :
1function DashboardSkeleton() {
2 return (
3 <div className="dashboard-skeleton">
4 <div className="stats-row">
5 <StatCardSkeleton />
6 <StatCardSkeleton />
7 <StatCardSkeleton />
8 </div>
9 <div className="main-content">
10 <ChartSkeleton />
11 <ListSkeleton count={5} />
12 </div>
13 </div>
14 );
15}La grille est la même que dans le tableau de bord cible, seules les briques à l'intérieur changent.
Modèles de chargement
Chargement progressif
Charge d'abord le contenu important, le reste se charge en arrière-plan. Chaque section moins importante reçoit son propre Suspense, donc elle ne bloque pas les autres :
1function MissionDashboard() {
2 return (
3 <div>
4 {/* Contenu critique - pas de lazy loading */}
5 <MissionStatus />
6
7 {/* Moins critique - lazy loading avec skeleton */}
8 <Suspense fallback={<ChartSkeleton />}>
9 <MissionChart />
10 </Suspense>
11
12 {/* Le moins important - se charge en dernier */}
13 <Suspense fallback={<ListSkeleton count={5} />}>
14 <MissionHistory />
15 </Suspense>
16 </div>
17 );
18}Nous supposons ici que MissionChart et MissionHistory sont des composants créés avec React.lazy(). MissionStatus est importé normalement, donc il apparaît immédiatement et n'a pas de fallback propre.
États de chargement en ligne
Tu n'as pas toujours besoin de Suspense. Pour des données chargées de manière asynchrone dans un effet, utilise des états de chargement, car Suspense ne sait rien d'un fetch appelé dans useEffect :
1function CrewMember({ id }) {
2 const [member, setMember] = useState(null);
3 const [loading, setLoading] = useState(true);
4
5 useEffect(() => {
6 setLoading(true); // Nouvel id - on affiche de nouveau le skeleton
7 fetchCrewMember(id)
8 .then(setMember)
9 .finally(() => setLoading(false));
10 }, [id]);
11
12 if (loading) return <MemberSkeleton />;
13
14 return (
15 <div className="crew-member">
16 <img src={member.avatar} alt={member.name} />
17 <h3>{member.name}</h3>
18 <p>{member.role}</p>
19 </div>
20 );
21}La ligne setLoading(true) au début de l'effet fait en sorte qu'après un changement d'id, le squelette revienne, au lieu d'afficher un instant le membre d'équipage précédent. finally désactive le chargement aussi bien après un succès qu'après une erreur. Tu ajouteras la gestion de l'erreur elle-même dans la prochaine leçon.
Bonnes pratiques
- Le skeleton doit refléter la disposition du contenu - mêmes taille et forme
- Animation de pulsation - donne le sentiment qu'il se passe quelque chose
- Évite le scintillement - si le chargement prend moins de 300 ms, n'affiche pas de skeletons
- Utilise des skeletons différents pour différents types de contenu
- Conserve les proportions - le skeleton doit occuper le même espace que le contenu réel
Pour le point 3, React t'aide en partie lui-même : selon react.dev, il révèle le contenu suspendu au plus une fois toutes les 300 ms, donc les limites prêtes dans cette fenêtre apparaissent ensemble. Mon conseil : commence toujours par un squelette, et garde le spinner pour les actions courtes dans les boutons. Dans la prochaine leçon, tu combineras les squelettes avec les limites d'erreur, pour qu'un module qui ne se charge pas ne laisse pas de trou vide.
Souviens-toi : un bon squelette est l'hologramme d'un module, qui montre à l'équipage la forme de ce qui va bientôt s'amarrer.
Code de cette leçon : App.jsx
1import React, { useState, useEffect } from 'react';
2import './styles.css';
3
4// Skeleton pour une carte de mission
5function MissionCardSkeleton() {
6 return (
7 <div className="card skeleton-card">
8 <div className="skeleton-img skeleton-pulse" />
9 <div className="skeleton-h skeleton-pulse" />
10 <div className="skeleton-p skeleton-pulse" />
11 <div className="skeleton-p skeleton-pulse short" />
12 </div>
13 );
14}
15
16// Skeleton pour la liste des membres d'équipage
17function CrewListSkeleton({ count = 3 }) {
18 return (
19 <div className="crew-skeleton">
20 {Array.from({ length: count }).map((_, i) => (
21 <div key={i} className="crew-item-skel">
22 <div className="avatar-skel skeleton-pulse" />
23 <div className="crew-info-skel">
24 <div className="skeleton-name skeleton-pulse" />
25 <div className="skeleton-role skeleton-pulse" />
26 </div>
27 </div>
28 ))}
29 </div>
30 );
31}
32
33// Vraie carte de mission
34function MissionCard({ mission }) {
35 return (
36 <div className="card">
37 <div className="card-icon">{mission.icon}</div>
38 <h3>{mission.name}</h3>
39 <p>{mission.description}</p>
40 <span className={`badge ${mission.status}`}>{mission.status}</span>
41 </div>
42 );
43}
44
45// Vraie liste de l'équipage
46function CrewList({ members }) {
47 return (
48 <div className="crew-list">
49 {members.map(m => (
50 <div key={m.id} className="crew-item">
51 <div className="avatar">{m.avatar}</div>
52 <div>
53 <strong>{m.name}</strong>
54 <p className="role">{m.role}</p>
55 </div>
56 </div>
57 ))}
58 </div>
59 );
60}
61
62export default function App() {
63 const [missions, setMissions] = useState(null);
64 const [crew, setCrew] = useState(null);
65
66 useEffect(() => {
67 // Simulation du chargement des missions (plus rapide)
68 setTimeout(() => {
69 setMissions([
70 { id: 1, icon: '', name: 'Exploration de Mars', description: 'Étude de la surface', status: 'active' },
71 { id: 2, icon: '', name: 'Reconnaissance de Jupiter', description: 'Analyse de l\'atmosphère', status: 'pending' },
72 ]);
73 }, 1200);
74
75 // Simulation du chargement de l'équipage (plus lent)
76 setTimeout(() => {
77 setCrew([
78 { id: 1, avatar: '', name: 'Capitaine Nova', role: 'Commandant' },
79 { id: 2, avatar: '', name: 'Dr. Stellar', role: 'Scientifique' },
80 { id: 3, avatar: '', name: 'Tech Orion', role: 'Ingénieur' },
81 ]);
82 }, 2500);
83 }, []);
84
85 return (
86 <div className="app">
87 <h1>Skeleton Loaders</h1>
88 <p className="subtitle">Observe comment les skeletons se transforment en contenu</p>
89
90 <section>
91 <h2>Missions</h2>
92 {missions ? (
93 <div className="cards-grid">
94 {missions.map(m => <MissionCard key={m.id} mission={m} />)}
95 </div>
96 ) : (
97 <div className="cards-grid">
98 <MissionCardSkeleton />
99 <MissionCardSkeleton />
100 </div>
101 )}
102 </section>
103
104 <section>
105 <h2>Équipage</h2>
106 {crew ? <CrewList members={crew} /> : <CrewListSkeleton count={3} />}
107 </section>
108 </div>
109 );
110}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. Pourquoi un skeleton loader est-il meilleur qu'un simple spinner ?
Tâches pratiques dans le jeu
- Éditeur de code
Implémente MissionCardSkeleton
- Tri vertical
Ordonne les niveaux de qualité du chargement du pire au meilleur :
- Clique dans l'ordre
Clique les éléments dans l'ordre pour construire une animation de pulsation skeleton :