Cours JavaScript et React · Module 2: Composants et props

La composition de composants

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

  1. Container - responsable de la logique : récupération des données, gestion de l'état, gestion des événements
  2. 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 :

  1. 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).
  2. Détermine de quelles données (props) chaque section a besoin.
  3. Extrais chaque section dans un composant distinct.
  4. 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 ?

PatternCas d'usage
childrenConteneurs simples, mises en page, cartes, panneaux
Slots (props)Mises en page complexes avec plusieurs zones de contenu
Container/PresentationalSéparer la logique de la présentation
HOCPartager de la logique entre plusieurs composants
Render PropsRendu 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. 1. Quand faut-il utiliser props.children dans un composant React ?

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

Articles utiles