Cours Vibe coding avec l'IA · Module 3: Routing et composants

Props et composition de composants - Des composants réutilisables

13 min de lecture
Dans cette leçon12

Il est temps d'apprendre à construire des composants réutilisables - ceux que tu peux utiliser à de nombreux endroits avec des données différentes.

Que sont les props ?

Les props (propriétés) sont des données passées à un composant depuis son parent.

Exemple sans props

Commençons par une carte dont toutes les données sont écrites en dur dans le JSX :

1function PirateCard() {
2  return (
3    <div className="border p-4 rounded-lg">
4      <h2 className="text-xl font-bold">Jack Sparrow</h2>
5      <p>Niveau : 10</p>
6      <p>Navire : Black Pearl</p>
7    </div>
8  );
9}

Problème : on ne peut afficher que Jack Sparrow. Et les autres pirates ?

Le même composant avec des props

Maintenant, les mêmes données viennent de l'extérieur. L'interface PirateCardProps décrit ce dont la carte a besoin, et le JSX insère les valeurs entre accolades :

1interface PirateCardProps {
2  name: string;
3  level: number;
4  ship: string;
5}
6
7function PirateCard({ name, level, ship }: PirateCardProps) {
8  return (
9    <div className="border p-4 rounded-lg">
10      <h2 className="text-xl font-bold">{name}</h2>
11      <p>Niveau : {level}</p>
12      <p>Navire : {ship}</p>
13    </div>
14  );
15}

Utilisation :

1<div>
2  <PirateCard name="Jack Sparrow" level={10} ship="Black Pearl" />
3  <PirateCard name="Anne Bonny" level={8} ship="Revenge" />
4  <PirateCard name="Barbe-Noire" level={12} ship="Queen Anne's Revenge" />
5</div>

Et voilà ! Un seul composant, plusieurs pirates.

Props - Différents types de données

1. Props String

Tu transmets du texte entre guillemets droits, exactement comme la valeur d'un attribut HTML :

1<PirateCard name="Jack Sparrow" />

2. Props Number

Les nombres s'écrivent toujours entre accolades. Écrire level="10" transmettrait la chaîne "10", et non un nombre :

1<PirateCard level={10} />

3. Props Boolean

Un booléen se transmet aussi entre accolades, et pour true il existe une écriture plus courte où le nom de la prop suffit :

1<div>
2  <PirateCard isActive={true} />
3
4  {/* Raccourci - si true, tu peux omettre la valeur */}
5  <PirateCard isActive />
6</div>

4. Props Array

Un tableau se transmet aussi entre accolades, et à l'intérieur tu écris un tableau JavaScript ordinaire :

1<PirateCard skills={["Navigation", "Combat", "Diplomatie"]} />

5. Props Object

Pour un objet, on écrit des doubles accolades : les accolades extérieures marquent une expression JavaScript, et les intérieures sont l'objet lui-même :

1<PirateCard
2  pirate={{
3    name: "Jack",
4    level: 10,
5    ship: "Black Pearl"
6  }}
7/>

6. Props Function (callbacks)

Tu peux aussi transmettre une fonction. Le composant l'appelle quand quelque chose se produit, par exemple au clic sur un bouton :

1<PirateCard
2  onLevelUp={() => console.log("Niveau supérieur !")}
3  onDelete={(id) => deletePirate(id)}
4/>

La prop Children - Des composants avec du contenu

children est une prop spéciale - tout ce qui se trouve entre les balises du composant.

Exemple - un composant Card

La carte reçoit son titre dans la prop title, et tout le reste du contenu via children :

1interface CardProps {
2  title: string;
3  children: React.ReactNode;
4}
5
6function Card({ title, children }: CardProps) {
7  return (
8    <div className="border rounded-lg p-6 bg-white shadow-lg">
9      <h2 className="text-2xl font-bold mb-4">{title}</h2>
10      <div className="text-gray-700">
11        {children}
12      </div>
13    </div>
14  );
15}

Utilisation :

1<div>
2  <Card title="Jack Sparrow">
3    <p>Capitaine du Black Pearl</p>
4    <p>Niveau : 10</p>
5    <button>Voir le profil</button>
6  </Card>
7
8  <Card title="Anne Bonny">
9    <img src="/anne.jpg" />
10    <p>Navigatrice et combattante</p>
11  </Card>
12</div>

children peut être :

  • Du texte
  • Des éléments JSX
  • D'autres composants
  • Des tableaux d'éléments
  • Null/undefined (rien)

Default Props - Des valeurs par défaut

Tu peux définir des valeurs par défaut pour les props :

1interface ButtonProps {
2  text: string;
3  variant?: "primary" | "secondary";  // Optionnel
4  disabled?: boolean;
5}
6
7function Button({
8  text,
9  variant = "primary",  // Valeur par défaut
10  disabled = false
11}: ButtonProps) {
12  return (
13    <button
14      disabled={disabled}
15      className={`btn btn-${variant}`}
16    >
17      {text}
18    </button>
19  );
20}
21
22// Utilisation
23<div>
24  <Button text="Clique-moi" />  {/* variant = "primary" (par défaut) */}
25  <Button text="Secondaire" variant="secondary" />
26  <Button text="Désactivé" disabled />
27</div>

Spread Props - Passer plusieurs props

Tu peux passer plusieurs props d'un coup grâce à l'opérateur spread :

1const pirateData = {
2  name: "Jack Sparrow",
3  level: 10,
4  ship: "Black Pearl",
5  isActive: true
6};
7
8<div>
9  {/* Au lieu de : */}
10  <PirateCard
11    name={pirateData.name}
12    level={pirateData.level}
13    ship={pirateData.ship}
14    isActive={pirateData.isActive}
15  />
16
17  {/* Utilise le spread : */}
18  <PirateCard {...pirateData} />
19</div>

Les deux sont équivalents !

Composition de composants - Construire avec des briques

La composition consiste à construire des interfaces complexes à partir de composants simples.

Exemple - un composant Post

On découpe un post en trois petits composants : un en-tête, le contenu et les actions. Ensuite, on les assemble dans un seul composant Post :

1// Petits composants (atomiques)
2function PostHeader({ author, timestamp }: { author: string; timestamp: string }) {
3  return (
4    <div className="flex items-center gap-3 mb-3">
5      <div className="w-10 h-10 bg-blue-500 rounded-full" />
6      <div>
7        <p className="font-bold">{author}</p>
8        <p className="text-sm text-gray-500">{timestamp}</p>
9      </div>
10    </div>
11  );
12}
13
14function PostContent({ content }: { content: string }) {
15  return <p className="text-gray-800 mb-4">{content}</p>;
16}
17
18function PostActions({ likes, onLike, onShare }: {
19  likes: number;
20  onLike: () => void;
21  onShare: () => void;
22}) {
23  return (
24    <div className="flex gap-4">
25      <button onClick={onLike} className="text-blue-600">
26        {likes}
27      </button>
28      <button onClick={onShare} className="text-blue-600">
29        Partager
30      </button>
31    </div>
32  );
33}
34
35// Composition - on assemble à partir de petits composants
36function Post({
37  author,
38  timestamp,
39  content,
40  likes,
41  onLike,
42  onShare
43}: {
44  author: string;
45  timestamp: string;
46  content: string;
47  likes: number;
48  onLike: () => void;
49  onShare: () => void;
50}) {
51  return (
52    <div className="border rounded-lg p-4 bg-white shadow">
53      <PostHeader author={author} timestamp={timestamp} />
54      <PostContent content={content} />
55      <PostActions likes={likes} onLike={onLike} onShare={onShare} />
56    </div>
57  );
58}

Avantages de la composition :

  • Tests plus simples des petits composants
  • Réutilisabilité (PostHeader peut servir ailleurs)
  • Code plus facile à lire
  • Flexibilité (tu peux remplacer PostActions par un autre composant)

Props Drilling - Problème et solution

Le props drilling, c'est passer des props à travers plusieurs niveaux de composants.

Problème

Les données de l'utilisateur se trouvent dans App, mais seul un composant tout en bas de l'arbre en a besoin. En chemin, chaque composant doit les transmettre :

1function App() {
2  const [user, setUser] = useState({ name: "Jack", level: 10 });
3
4  return <Dashboard user={user} />;
5}
6
7function Dashboard({ user }: { user: User }) {
8  return <Sidebar user={user} />;
9}
10
11function Sidebar({ user }: { user: User }) {
12  return <UserProfile user={user} />;
13}
14
15function UserProfile({ user }: { user: User }) {
16  return <div>{user.name}</div>;
17}

Problème : user traverse Dashboard et Sidebar juste pour atteindre UserProfile. Eux-mêmes n'utilisent pas user.

Solution 1 : la composition de composants

Au lieu de faire passer user par chaque niveau, App place directement UserProfile dans Sidebar. Dashboard et Sidebar se contentent d'afficher leurs children :

1function App() {
2  const [user, setUser] = useState({ name: "Jack", level: 10 });
3
4  return (
5    <Dashboard>
6      <Sidebar>
7        <UserProfile user={user} />
8      </Sidebar>
9    </Dashboard>
10  );
11}
12
13function Dashboard({ children }: { children: React.ReactNode }) {
14  return <div className="dashboard">{children}</div>;
15}
16
17function Sidebar({ children }: { children: React.ReactNode }) {
18  return <aside className="sidebar">{children}</aside>;
19}

Maintenant Dashboard et Sidebar n'ont plus besoin de connaître user !

Solution 2 : la Context API (sujet abordé plus tard)

Quand le props drilling est inévitable, utilise la Context API - tu passeras les données « dans les airs » directement au composant qui en a besoin.

Tu découvriras la Context API dans le cours React (monde 2, module 4), dans la leçon « Context API ».

Compound Components - Un pattern avancé

Les compound components sont des composants qui travaillent ensemble en partageant un état.

Exemple - un composant Tabs

Le composant Tabs garde l'onglet actif dans un état et le partage avec ses sous-composants grâce au contexte (React.createContext et React.useContext). Pour l'instant, il suffit de savoir que le contexte transmet des données vers le bas de l'arbre sans props :

1interface TabsContextType {
2  activeTab: string;
3  setActiveTab: (tab: string) => void;
4}
5
6const TabsContext = React.createContext<TabsContextType | null>(null);
7
8function Tabs({ defaultTab, children }: {
9  defaultTab: string;
10  children: React.ReactNode
11}) {
12  const [activeTab, setActiveTab] = useState(defaultTab);
13
14  return (
15    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
16      <div className="tabs">
17        {children}
18      </div>
19    </TabsContext.Provider>
20  );
21}
22
23function TabList({ children }: { children: React.ReactNode }) {
24  return <div className="flex border-b">{children}</div>;
25}
26
27function Tab({ value, children }: { value: string; children: React.ReactNode }) {
28  const context = React.useContext(TabsContext);
29  if (!context) throw new Error("Tab must be used within Tabs");
30
31  const { activeTab, setActiveTab } = context;
32  const isActive = activeTab === value;
33
34  return (
35    <button
36      onClick={() => setActiveTab(value)}
37      className={`px-4 py-2 ${isActive ? 'border-b-2 border-blue-600 font-bold' : ''}`}
38    >
39      {children}
40    </button>
41  );
42}
43
44function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
45  const context = React.useContext(TabsContext);
46  if (!context) throw new Error("TabPanel must be used within Tabs");
47
48  const { activeTab } = context;
49  if (activeTab !== value) return null;
50
51  return <div className="p-4">{children}</div>;
52}
53
54// Export en tant que compound component
55Tabs.List = TabList;
56Tabs.Tab = Tab;
57Tabs.Panel = TabPanel;
58
59// Utilisation - une API super lisible !
60<Tabs defaultTab="posts">
61  <Tabs.List>
62    <Tabs.Tab value="posts">Posts</Tabs.Tab>
63    <Tabs.Tab value="crew">Équipage</Tabs.Tab>
64    <Tabs.Tab value="ships">Navires</Tabs.Tab>
65  </Tabs.List>
66
67  <Tabs.Panel value="posts">
68    <PostsList />
69  </Tabs.Panel>
70  <Tabs.Panel value="crew">
71    <CrewList />
72  </Tabs.Panel>
73  <Tabs.Panel value="ships">
74    <ShipsList />
75  </Tabs.Panel>
76</Tabs>

Avantages des compound components :

  • API flexible
  • Code d'utilisation lisible
  • Les composants partagent un état sans props drilling
  • Tu peux composer librement

C'est un pattern avancé - tu n'es pas obligé de l'utiliser tout de suite, mais c'est bon à connaître !

Le pattern Render Props - De la flexibilité

Les render props sont une fonction passée en prop qui retourne du JSX.

Exemple - un traqueur de souris

Le composant MouseTracker suit la position du curseur, mais ne dessine rien lui-même. C'est la fonction passée dans la prop render qui décide de l'apparence :

1interface MousePosition {
2  x: number;
3  y: number;
4}
5
6function MouseTracker({ render }: { render: (pos: MousePosition) => React.ReactNode }) {
7  const [position, setPosition] = useState({ x: 0, y: 0 });
8
9  useEffect(() => {
10    const handleMove = (e: MouseEvent) => {
11      setPosition({ x: e.clientX, y: e.clientY });
12    };
13
14    window.addEventListener('mousemove', handleMove);
15    return () => window.removeEventListener('mousemove', handleMove);
16  }, []);
17
18  return render(position);
19}
20
21// Utilisation
22<div>
23  <MouseTracker
24    render={(pos) => (
25      <div>
26        Position de la souris : {pos.x}, {pos.y}
27      </div>
28    )}
29  />
30
31  <MouseTracker
32    render={(pos) => (
33      <div
34        style={{
35          position: 'fixed',
36          left: pos.x,
37          top: pos.y
38        }}
39      >
40        Toi
41      </div>
42    )}
43  />
44</div>

Les render props te permettent de réutiliser la logique tout en changeant le rendu !

Note : aujourd'hui, on utilise plus souvent des custom hooks à la place des render props.

Validation des props avec TypeScript

TypeScript impose des props correctes - c'est génial !

Exemple d'erreur

Quand tu transmets trop peu de props ou une valeur du mauvais type, TypeScript signale une erreur avant même que tu lances l'application :

1interface PirateCardProps {
2  name: string;
3  level: number;
4  ship: string;
5}
6
7<div>
8  {/* Erreur : la propriété ship est manquante */}
9  <PirateCard name="Jack" level={10} />
10
11  {/* Erreur : le type string ne correspond pas au type number */}
12  <PirateCard name="Jack" level="10" ship="Pearl" />
13
14  {/* OK */}
15  <PirateCard name="Jack" level={10} ship="Pearl" />
16</div>

Props optionnelles

Un champ avec un point d'interrogation peut être omis, donc le composant doit vérifier si la valeur est bien arrivée :

1interface CardProps {
2  title: string;
3  subtitle?: string;  // Optionnel
4  footer?: React.ReactNode;  // Optionnel
5}
6
7function Card({ title, subtitle, footer }: CardProps) {
8  return (
9    <div className="card">
10      <h2>{title}</h2>
11      {subtitle && <p>{subtitle}</p>}  {/* Affiché seulement s'il existe */}
12      {footer && <div className="footer">{footer}</div>}
13    </div>
14  );
15}

Exemple pratique - Le fil des pirates

Construisons un fil de posts en utilisant la composition :

1// Types
2interface Post {
3  id: number;
4  author: string;
5  content: string;
6  likes: number;
7  timestamp: string;
8}
9
10// Petits composants réutilisables
11function Avatar({ name }: { name: string }) {
12  return (
13    <div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
14      {name[0]}
15    </div>
16  );
17}
18
19function Button({
20  children,
21  onClick,
22  variant = "primary"
23}: {
24  children: React.ReactNode;
25  onClick: () => void;
26  variant?: "primary" | "secondary";
27}) {
28  const styles = variant === "primary"
29    ? "bg-blue-600 hover:bg-blue-700 text-white"
30    : "bg-gray-200 hover:bg-gray-300 text-gray-800";
31
32  return (
33    <button
34      onClick={onClick}
35      className={`px-4 py-2 rounded-lg font-medium transition ${styles}`}
36    >
37      {children}
38    </button>
39  );
40}
41
42// Composant Post Card
43function PostCard({ post, onLike, onDelete }: {
44  post: Post;
45  onLike: (id: number) => void;
46  onDelete: (id: number) => void;
47}) {
48  return (
49    <div className="bg-white border rounded-lg p-6 shadow-sm">
50      {/* En-tête */}
51      <div className="flex items-start gap-3 mb-4">
52        <Avatar name={post.author} />
53        <div className="flex-1">
54          <h3 className="font-bold">{post.author}</h3>
55          <p className="text-sm text-gray-500">{post.timestamp}</p>
56        </div>
57        <button
58          onClick={() => onDelete(post.id)}
59          className="text-gray-400 hover:text-red-600"
60        >
61          Supprimer
62        </button>
63      </div>
64
65      {/* Contenu */}
66      <p className="text-gray-800 mb-4">{post.content}</p>
67
68      {/* Actions */}
69      <div className="flex gap-2">
70        <Button onClick={() => onLike(post.id)}>
71          {post.likes}
72        </Button>
73        <Button variant="secondary" onClick={() => alert("Partager !")}>
74          Partager
75        </Button>
76      </div>
77    </div>
78  );
79}
80
81// Composant Feed
82function PirateFeed() {
83  const [posts, setPosts] = useState<Post[]>([
84    {
85      id: 1,
86      author: "Jack Sparrow",
87      content: "Je viens de trouver le trésor de l'Isla de Muerta !",
88      likes: 42,
89      timestamp: "il y a 2 heures"
90    },
91    {
92      id: 2,
93      author: "Anne Bonny",
94      content: "En route pour de nouvelles aventures. Qui rejoint l'équipage ?",
95      likes: 27,
96      timestamp: "il y a 5 heures"
97    }
98  ]);
99
100  const handleLike = (id: number) => {
101    setPosts(posts.map(post =>
102      post.id === id
103        ? { ...post, likes: post.likes + 1 }
104        : post
105    ));
106  };
107
108  const handleDelete = (id: number) => {
109    setPosts(posts.filter(post => post.id !== id));
110  };
111
112  return (
113    <div className="max-w-2xl mx-auto p-4 space-y-4">
114      <h1 className="text-3xl font-bold mb-6">Fil de Kraken's Call</h1>
115
116      {posts.map(post => (
117        <PostCard
118          key={post.id}
119          post={post}
120          onLike={handleLike}
121          onDelete={handleDelete}
122        />
123      ))}
124    </div>
125  );
126}

Voici un exemple de bonne composition de composants :

  • Avatar - réutilisable (peut servir partout)
  • Button - réutilisable avec des variantes
  • PostCard - encapsule la logique d'un seul post
  • PirateFeed - gère toute la liste

Récapitulatif

  • Props - passer des données aux composants
  • children - prop spéciale pour le contenu
  • Default props - valeurs par défaut
  • Spread props - {...data} passe tout
  • Composition de composants - construire à partir de petits composants
  • Props drilling - le problème du passage de props à travers plusieurs niveaux
  • Compound components - des composants partageant un état
  • Render props - des fonctions comme props qui retournent du JSX
  • TypeScript - impose des types de props corrects

Prochaines étapes : tu découvriras la Context API (la solution au props drilling) dans le cours React (monde 2, module 4). Tu connais déjà les Server Components grâce à la leçon sur les composants.

Juste en dessous, tu trouveras un fil construit avec des composants Post. Chaque post reçoit ses données via le spread {...post}, comme la carte de pirate dans l'exemple avec pirateData.

À bientôt !

Code de cette leçon : App.jsx
1// Composant Post façon Twitter
2function Post({ author, avatar, content, timestamp, likes }) {
3  return (
4    <div className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow mb-4">
5      {/* En-tête */}
6      <div className="flex items-center gap-3 mb-4">
7        <div className="text-4xl">{avatar}</div>
8        <div className="flex-1">
9          <h3 className="font-bold text-gray-900">{author}</h3>
10          <p className="text-sm text-gray-500">{timestamp}</p>
11        </div>
12      </div>
13
14      {/* Contenu */}
15      <p className="text-gray-800 mb-4 leading-relaxed">{content}</p>
16
17      {/* Actions */}
18      <div className="flex items-center gap-6 text-gray-500 border-t pt-4">
19        <button className="flex items-center gap-2 hover:text-red-500 transition-colors">
20          <span className="text-sm font-medium">{likes}</span>
21        </button>
22        <button className="flex items-center gap-2 hover:text-blue-500 transition-colors">
23          <span className="text-sm font-medium">Répondre</span>
24        </button>
25        <button className="flex items-center gap-2 hover:text-green-500 transition-colors">
26          <span className="text-sm font-medium">Partager</span>
27        </button>
28      </div>
29    </div>
30  );
31}
32
33export default function App() {
34  const posts = [
35    {
36      id: 1,
37      author: 'Capitaine Barbe-Rousse',
38      avatar: 'CB',
39      content: 'On vient de découvrir une nouvelle île sur la carte ! À en croire les vieux registres, il pourrait y avoir un trésor énorme là-bas. Qui me suit ?',
40      timestamp: 'il y a 2 heures',
41      likes: 42
42    },
43    {
44      id: 2,
45      author: 'Morgan Cœur-Noir',
46      avatar: 'MC',
47      content: 'Nouvelle route commerciale ouverte ! On peut maintenant naviguer vers Tortuga sans se soucier de la patrouille. Ça va être une belle journée !',
48      timestamp: 'il y a 5 heures',
49      likes: 28
50    },
51    {
52      id: 3,
53      author: 'Polly Piaille',
54      avatar: 'PP',
55      content: 'Kraken ! Kraken ! J\'ai vu un kraken géant au sud ! Évitez ces eaux, pirates !',
56      timestamp: 'il y a 8 heures',
57      likes: 156
58    }
59  ];
60
61  return (
62    <div className="min-h-screen bg-gray-100 p-8">
63      <div className="max-w-2xl mx-auto">
64        <h1 className="text-3xl font-bold mb-6">Fil de Kraken's Call</h1>
65        {posts.map(post => (
66          <Post key={post.id} {...post} />
67        ))}
68      </div>
69    </div>
70  );
71}

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. Qu'est-ce que la prop spéciale children dans un composant ?

  2. 2. Que fait <PirateCard {...pirateData} /> ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne les éléments qui transmettent toutes les données via le spread :

Articles utiles