Cours JavaScript et React · Module 2: Composants et props
La composition de composants
Dans cette leçon8
Une station spatiale ne se construit pas à partir d'un seul bloc de métal - elle se compose de modules amarrés ensemble de manière réfléchie. Le module laboratoire se raccorde au module d'habitation, qui se raccorde au module d'alimentation. Chacun peut fonctionner indépendamment, mais ce n'est qu'ensemble qu'ils forment un tout fonctionnel.
En React, la composition de composants fonctionne selon le même principe. Au lieu de construire un seul composant énorme, on crée des éléments plus petits et spécialisés que l'on connecte ensemble. C'est l'un des patterns de conception les plus importants en React et la clé pour créer des applications évolutives.
Children - le contenu interne d'un composant
La forme la plus simple de composition consiste à utiliser la prop spéciale children. Chaque composant React peut accepter des éléments imbriqués entre ses balises ouvrante et fermante :
1function SpaceModule({ title, children }) {
2 return (
3 <div className="space-module">
4 <div className="module-header">
5 <h3>{title}</h3>
6 <span className="status-light" />
7 </div>
8 <div className="module-content">
9 {children}
10 </div>
11 </div>
12 );
13}
14
15// Utilisation - on « amarre » n'importe quel contenu dans le module
16function SpaceStation() {
17 return (
18 <div className="station">
19 <SpaceModule title="Laboratoire">
20 <p>Recherche en microgravité</p>
21 <p>Expériences biologiques</p>
22 </SpaceModule>
23
24 <SpaceModule title="Centre de commandement">
25 <button>Navigation</button>
26 <button>Communication</button>
27 <button>Systèmes de survie</button>
28 </SpaceModule>
29 </div>
30 );
31}La prop children permet de créer des composants conteneurs qui ne savent pas à l'avance ce qu'ils afficheront. C'est comme construire des ports d'amarrage universels - ils peuvent accepter n'importe quel module.
Le pattern Slot - plusieurs points de composition
Parfois, un seul children ne suffit pas. On a besoin de plusieurs « endroits » dans un composant où l'on peut insérer différents contenus. En React, on obtient cela en passant des composants en tant que props ordinaires :
1function MissionPanel({ header, sidebar, content, footer }) {
2 return (
3 <div className="mission-panel">
4 <div className="panel-header">{header}</div>
5 <div className="panel-body">
6 <aside className="panel-sidebar">{sidebar}</aside>
7 <main className="panel-content">{content}</main>
8 </div>
9 <div className="panel-footer">{footer}</div>
10 </div>
11 );
12}
13
14// Chaque « emplacement » reçoit un composant différent
15function MissionDashboard() {
16 return (
17 <MissionPanel
18 header={<h1>Mission Apollo</h1>}
19 sidebar={<NavigationMenu />}
20 content={<MissionDetails missionId="apollo-11" />}
21 footer={<StatusBar status="active" />}
22 />
23 );
24}Ce pattern offre une flexibilité énorme - le composant MissionPanel définit la mise en page mais n'impose pas le contenu.
Container/Presentational - la séparation des responsabilités
L'un des patterns de composition classiques consiste à séparer les composants en deux types :
- Container - responsable de la logique : récupération des données, gestion de l'état, gestion des événements
- Presentational - responsable uniquement de l'apparence : rend les données reçues, ne gère pas d'état
Imagine cela comme la division du travail sur une station spatiale : l'ordinateur de bord (container) traite les données et prend les décisions, tandis que les panneaux d'affichage (presentational) montrent ces informations à l'équipage.
1// Composant presentational - affiche seulement les données
2function CrewList({ members, onSelectMember }) {
3 return (
4 <ul className="crew-list">
5 {members.map(member => (
6 <li key={member.id} onClick={() => onSelectMember(member.id)}>
7 <span>{member.name}</span>
8 <span className="role">{member.role}</span>
9 </li>
10 ))}
11 </ul>
12 );
13}
14
15// Composant container - gère les données et la logique
16function CrewListContainer() {
17 const [members, setMembers] = useState([]);
18 const [selectedId, setSelectedId] = useState(null);
19
20 useEffect(() => {
21 // Récupération des données de l'équipage depuis l'API
22 fetchCrewData().then(data => setMembers(data));
23 }, []);
24
25 const handleSelectMember = (id) => {
26 setSelectedId(id);
27 logActivity(`Membre d'équipage sélectionné : ${id}`);
28 };
29
30 return (
31 <CrewList
32 members={members}
33 onSelectMember={handleSelectMember}
34 />
35 );
36}Grâce à cette séparation, le composant CrewList est facile à tester et à réutiliser - il suffit de lui passer des données différentes.
Les Higher-Order Components (HOC)
Un Higher-Order Component (composant d'ordre supérieur) est une fonction qui prend un composant et renvoie un nouveau composant amélioré. Le nom vient des « fonctions d'ordre supérieur » en programmation fonctionnelle. Un HOC, c'est comme un module d'amélioration sur une station spatiale - tu le branches sur un module existant et il gagne de nouvelles capacités sans changer son intérieur.
1// HOC ajoutant une fonctionnalité de chargement de données
2function withMissionData(WrappedComponent) {
3 return function EnhancedComponent(props) {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 fetchMissionData(props.missionId)
9 .then(result => {
10 setData(result);
11 setLoading(false);
12 });
13 }, [props.missionId]);
14
15 if (loading) {
16 return <div className="loading">Chargement des données de mission...</div>;
17 }
18
19 return <WrappedComponent {...props} missionData={data} />;
20 };
21}
22
23// Composant simple affichant des données
24function MissionDetails({ missionData }) {
25 return (
26 <div>
27 <h2>{missionData.name}</h2>
28 <p>Destination : {missionData.destination}</p>
29 </div>
30 );
31}
32
33// Composant amélioré avec chargement automatique des données
34const MissionDetailsWithData = withMissionData(MissionDetails);
35
36// Utilisation - les données seront récupérées automatiquement
37<MissionDetailsWithData missionId="apollo-11" />La convention de nommage des HOC est le préfixe with - withMissionData, withAuth, withLoading. Cela permet de reconnaître facilement qu'on a affaire à un composant amélioré.
Les Render Props - passer une fonction de rendu
Le pattern render props consiste à passer une fonction à un composant qui lui indique quoi rendre. Cela offre une flexibilité énorme - le composant gère la logique, et celui qui l'utilise décide de l'apparence :
1function OrbitTracker({ satelliteId, render }) {
2 const [position, setPosition] = useState({ x: 0, y: 0, altitude: 400 });
3
4 useEffect(() => {
5 const interval = setInterval(() => {
6 // Simulation du changement de position en orbite
7 setPosition(prev => ({
8 x: prev.x + Math.random() * 2 - 1,
9 y: prev.y + Math.random() * 2 - 1,
10 altitude: 400 + Math.sin(Date.now() / 1000) * 10
11 }));
12 }, 1000);
13 return () => clearInterval(interval);
14 }, [satelliteId]);
15
16 // Appelle la fonction render avec les données
17 return render(position);
18}
19
20// Utilisation - on décide comment afficher les données de position
21function SatelliteMonitor() {
22 return (
23 <OrbitTracker
24 satelliteId="iss"
25 render={(position) => (
26 <div className="monitor">
27 <p>Position X : {position.x.toFixed(2)}</p>
28 <p>Position Y : {position.y.toFixed(2)}</p>
29 <p>Altitude : {position.altitude.toFixed(1)} km</p>
30 </div>
31 )}
32 />
33 );
34}Des atomes aux pages - la composition à plus grande échelle
La méthode Atomic Design décrit la même idée : une interface se construit à partir de parties de plus en plus grandes. D'abord les atomes (Button, Input, Label), avec eux les molécules (SearchBar = Input + Button), avec les molécules les organismes (Header = Logo + SearchBar + Nav), puis les templates (la mise en page sans contenu) et enfin les pages (un template rempli de vraies données).
Quand découper un composant ?
Le signal le plus important : le composant gère plusieurs choses sans rapport en même temps, par exemple la navigation, un formulaire et une liste. Le principe de responsabilité unique (Single Responsibility Principle) dit que chaque composant doit être responsable d'une seule partie de l'interface - comme un module de station qui n'a qu'une seule mission. Le nombre de lignes ou de props ne suffit pas à trancher.
Tu refactorises un grand composant en quatre étapes :
- Repère dans le JSX les sections indépendantes (par ex. l'en-tête, la liste de l'équipage, le panneau des systèmes).
- Détermine de quelles données (props) chaque section a besoin.
- Extrais chaque section dans un composant distinct.
- Combine les nouveaux composants dans le composant parent.
Les petits composants sont aussi plus faciles à tester. Un test unitaire vérifie un seul composant isolément (par ex. si CrewList affiche tous les membres de l'équipage), et un test d'intégration vérifie si plusieurs composants fonctionnent ensemble (par ex. si SpaceStation transmet les données à CrewList).
Quel pattern utiliser et quand ?
| Pattern | Cas d'usage |
|---|---|
children | Conteneurs simples, mises en page, cartes, panneaux |
| Slots (props) | Mises en page complexes avec plusieurs zones de contenu |
| Container/Presentational | Séparer la logique de la présentation |
| HOC | Partager de la logique entre plusieurs composants |
| Render Props | Rendu flexible avec accès à l'état |
De nos jours, de nombreux cas d'usage des HOC et des Render Props ont été remplacés par les custom hooks, que nous découvrirons dans les modules suivants. Cependant, connaître ces patterns est important, car tu les rencontreras dans du code et des bibliothèques existants.
Code de cette leçon : App.jsx
1import React from 'react';
2
3// =============================================
4// COMPOSITION DE COMPOSANTS
5// Patterns : children, wrapper components, HOC
6// =============================================
7
8// --- PATTERN 1 : Wrapper / Container avec children ---
9// Card est un composant conteneur qui enveloppe n'importe quel contenu
10function Card({ title, children, color = '#667eea' }) {
11 return (
12 <div style={{
13 border: `2px solid ${color}`,
14 borderRadius: '12px',
15 overflow: 'hidden',
16 marginBottom: '20px',
17 boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
18 }}>
19 {title && (
20 <div style={{
21 padding: '12px 20px',
22 backgroundColor: color,
23 color: 'white',
24 fontWeight: 'bold',
25 fontSize: '16px'
26 }}>
27 {title}
28 </div>
29 )}
30 <div style={{ padding: '20px' }}>
31 {children}
32 </div>
33 </div>
34 );
35}
36
37// Layout enveloppe toute la page et garantit une apparence cohérente
38function Layout({ header, children, footer }) {
39 return (
40 <div style={{
41 maxWidth: '800px',
42 margin: '0 auto',
43 minHeight: '100vh',
44 display: 'flex',
45 flexDirection: 'column',
46 fontFamily: 'Arial, sans-serif'
47 }}>
48 <header style={{
49 background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
50 padding: '24px',
51 color: 'white',
52 textAlign: 'center'
53 }}>
54 {header}
55 </header>
56 <main style={{ flex: 1, padding: '24px', backgroundColor: '#f8f9fa' }}>
57 {children}
58 </main>
59 {footer && (
60 <footer style={{
61 padding: '16px',
62 backgroundColor: '#1a1a2e',
63 color: '#888',
64 textAlign: 'center',
65 fontSize: '14px'
66 }}>
67 {footer}
68 </footer>
69 )}
70 </div>
71 );
72}
73
74// --- PATTERN 2 : Higher-Order Component (HOC) ---
75// Un HOC est une fonction qui prend un composant et renvoie un nouveau
76// composant amélioré avec une fonctionnalité supplémentaire.
77
78function withLogger(WrappedComponent, componentLabel) {
79 return function LoggedComponent(props) {
80 React.useEffect(() => {
81 console.log(
82 `[LOG] <${componentLabel}> monté avec les props :`,
83 props
84 );
85 return () => {
86 console.log(`[LOG] <${componentLabel}> démonté`);
87 };
88 }, []);
89
90 // On transmet toutes les props d'origine + des infos supplémentaires
91 return <WrappedComponent {...props} isLogged={true} />;
92 };
93}
94
95// Composant simple affichant une mission
96function MissionBadge({ name, status, priority, isLogged }) {
97 const statusColors = {
98 active: '#4caf50',
99 pending: '#ff9800',
100 completed: '#2196f3'
101 };
102
103 return (
104 <div style={{
105 display: 'flex',
106 alignItems: 'center',
107 justifyContent: 'space-between',
108 padding: '14px 18px',
109 backgroundColor: 'white',
110 borderRadius: '8px',
111 marginBottom: '10px',
112 boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
113 }}>
114 <div>
115 <strong style={{ fontSize: '16px' }}>{name}</strong>
116 <div style={{ color: '#888', fontSize: '13px', marginTop: '4px' }}>
117 Priorité : {priority}
118 </div>
119 </div>
120 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
121 {isLogged && (
122 <span style={{
123 fontSize: '11px',
124 color: '#888',
125 padding: '2px 6px',
126 border: '1px solid #ddd',
127 borderRadius: '4px'
128 }}>
129 LOGGED
130 </span>
131 )}
132 <span style={{
133 padding: '4px 12px',
134 borderRadius: '12px',
135 color: 'white',
136 backgroundColor: statusColors[status] || '#999',
137 fontSize: '12px',
138 fontWeight: 'bold'
139 }}>
140 {status.toUpperCase()}
141 </span>
142 </div>
143 </div>
144 );
145}
146
147// On crée une version améliorée de MissionBadge à l'aide du HOC
148const LoggedMissionBadge = withLogger(MissionBadge, 'MissionBadge');
149
150// --- APPLICATION PRINCIPALE ---
151function App() {
152 const missions = [
153 { id: 1, name: 'Reconnaissance du cratère Tycho', status: 'active', priority: 'Haute' },
154 { id: 2, name: 'Réparation des panneaux solaires', status: 'pending', priority: 'Moyenne' },
155 { id: 3, name: 'Analyse d\'échantillons de régolithe', status: 'completed', priority: 'Basse' },
156 ];
157
158 return (
159 <Layout
160 header={<h1 style={{ margin: 0 }}>Composition de composants</h1>}
161 footer="Panneau des missions spatiales - React Composition Patterns"
162 >
163 {/* Card enveloppe la liste des missions */}
164 <Card title="Missions actives (avec HOC withLogger)" color="#667eea">
165 <p style={{ color: '#555', marginTop: 0 }}>
166 Ouvre la console du navigateur (F12) - tu verras les logs du HOC
167 quand chaque composant se monte.
168 </p>
169 {missions.map((m) => (
170 <LoggedMissionBadge
171 key={m.id}
172 name={m.name}
173 status={m.status}
174 priority={m.priority}
175 />
176 ))}
177 </Card>
178
179 {/* Cartes imbriquées - composition dans la composition */}
180 <Card title="Composition imbriquée" color="#764ba2">
181 <p style={{ marginTop: 0 }}>
182 Une Card peut contenir d'autres Cards - c'est la puissance du pattern children :
183 </p>
184 <Card title="Carte interne A" color="#4caf50">
185 <p style={{ margin: 0 }}>N'importe quel contenu à l'intérieur.</p>
186 </Card>
187 <Card title="Carte interne B" color="#ff9800">
188 <p style={{ margin: 0 }}>Une autre carte imbriquée.</p>
189 </Card>
190 </Card>
191 </Layout>
192 );
193}
194
195export 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. Quand faut-il utiliser props.children dans un composant React ?
2. Comment React transmet-il les éléments imbriqués à l'intérieur d'un composant vers son implémentation ?
Tâches pratiques dans le jeu
- Tri vertical
Place les étapes de composition de composants, de la plus simple à la plus complexe :
- Éditeur de code
Crée un composant CompleteMoonBase composé de LunarModule, CrewQuarters et CommandCenter.
- Tri vertical
Place dans l'ordre la hiérarchie des composants, du plus externe au plus interne :
- Tri vertical
Place dans l'ordre le pattern de composition de composants (Atomic Design) :
- Clique dans l'ordre
Construis la syntaxe correcte pour exporter un composant React :