Cours Vibe coding avec l'IA · Module 3: Routing et composants
Props et composition de composants - Des composants réutilisables
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é (
PostHeaderpeut servir ailleurs) - Code plus facile à lire
- Flexibilité (tu peux remplacer
PostActionspar 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 variantesPostCard- encapsule la logique d'un seul postPirateFeed- 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. Qu'est-ce que la prop spéciale children dans un composant ?
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 :