Cours Vibe coding avec l'IA · Module 3: Routing et composants
Les composants React - Construire l'interface
Dans cette leçon11
Le fil de Kraken's Call doit afficher 50 posts de l'équipage. Chaque post a la même allure : un avatar, un auteur, un contenu et un bouton J'aime. Tu pourrais copier le même morceau de JSX cinquante fois, mais alors chaque correction, par exemple une nouvelle couleur de bouton, signifierait cinquante modifications et cinquante occasions de te tromper.
React résout ce problème avec les composants. Pense au sceau du capitaine : il n'y a qu'un seul sceau, mais il peut laisser autant d'empreintes que tu veux sur les lettres. Tu écris le composant Post une seule fois, puis tu l'appliques dans le fil autant de fois que tu as de posts.
Ce que tu vas apprendre :
- créer des composants et les utiliser dans le JSX comme tes propres balises,
- transmettre des données aux composants avec les props et les décrire avec une interface TypeScript,
- construire un composant
Postdans un fichier séparé et l'importer dans une page, - distinguer les Server Components des Client Components et savoir quand ajouter
'use client', - utiliser
children, le rendu conditionnel et les listes créées avecmap().
Qu'est-ce qu'un composant ?
Un composant est une fonction qui retourne du JSX, c'est-à-dire une description de l'apparence d'une partie de l'interface. Le nom d'un composant commence toujours par une majuscule. Voici le composant le plus simple, un message d'accueil pour un nouveau membre de l'équipage :
1function WelcomeMessage() {
2 return (
3 <div className="bg-blue-100 p-4 rounded">
4 <h2 className="text-xl font-bold">Bienvenue à bord !</h2>
5 <p>Prêt à envoyer des messages ?</p>
6 </div>
7 );
8}La fonction ne prend aucun argument et retourne toujours le même bloc : un titre et un paragraphe enveloppés dans une div. Un composant retourne du JSX, ou null quand il n'a rien à afficher. React ne sait pas afficher un simple objet de données comme { name: 'Barbe-Rousse' } et signale une erreur. La définition seule n'affiche encore rien, car il faut utiliser le composant dans un autre composant.
Utiliser un composant
Tu places un composant dans le JSX comme une balise HTML ordinaire, mais avec le nom du composant à la place du nom de la balise. La page d'accueil peut même utiliser le message d'accueil trois fois de suite :
1export default function Home() {
2 return (
3 <div>
4 <WelcomeMessage />
5 <WelcomeMessage />
6 <WelcomeMessage />
7 </div>
8 );
9}React reconnaît un composant à sa majuscule : <WelcomeMessage /> est ta fonction, alors que <div> est une balise HTML. Trois utilisations donnent trois instances indépendantes du même message. Pour l'instant, elles sont toutes identiques, car le composant ne reçoit aucune donnée.
Props - Passer des données
Un vrai équipage est fait de personnes différentes : un capitaine, une navigatrice et un perroquet chargé des communications. Les données qui distinguent les copies d'un composant se transmettent avec les props. Elles fonctionnent comme les arguments d'une fonction : le composant les reçoit dans un seul objet, et la syntaxe { name, role, avatar } en extrait directement chaque champ :
1function PirateCard({ name, role, avatar }: {
2 name: string;
3 role: string;
4 avatar: string;
5}) {
6 return (
7 <div className="bg-white p-6 rounded-lg shadow">
8 <div className="text-4xl mb-2">{avatar}</div>
9 <h3 className="text-xl font-bold">{name}</h3>
10 <p className="text-gray-600">{role}</p>
11 </div>
12 );
13}Le type après les deux-points indique à TypeScript quelles props la carte attend : trois textes (string). Dans le JSX, tu insères la valeur d'une prop entre accolades, par exemple {name}.
Utiliser avec des props
Le parent transmet les props comme des attributs HTML. Le même composant affiche trois cartes différentes, car il reçoit chaque fois des données différentes :
1export default function Crew() {
2 return (
3 <div className="grid grid-cols-3 gap-4">
4 <PirateCard
5 name="Capitaine Barbe-Rousse"
6 role="Capitaine du navire"
7 avatar="CB"
8 />
9 <PirateCard
10 name="Morgan Cœur-Noir"
11 role="Navigatrice"
12 avatar="MC"
13 />
14 <PirateCard
15 name="Polly Piaille"
16 role="Perroquet de communication"
17 avatar="PP"
18 />
19 </div>
20 );
21}Ici, l'avatar se résume à des initiales, par exemple CB pour le capitaine Barbe-Rousse. Retiens que les props sont en lecture seule : la carte affiche name, mais ne le modifie jamais. Si des données doivent changer pendant que l'application tourne, il te faut un état, que tu découvriras dans la prochaine leçon.
Les props TypeScript - Des types pour plus de sûreté
Une description des props écrite directement dans les arguments devient vite longue. Mieux vaut la déplacer dans une interface (interface), c'est-à-dire une description nommée de la forme d'un objet. Grâce à elle, une faute de frappe ou une prop manquante provoque une erreur dans l'éditeur et pendant npm run build, et non une carte vide dans l'application terminée :
1interface PirateCardProps {
2 name: string;
3 role: string;
4 avatar: string;
5 description?: string; // Champ optionnel
6}
7
8function PirateCard({ name, role, avatar, description }: PirateCardProps) {
9 return (
10 <div className="bg-white p-6 rounded-lg shadow">
11 <div className="text-4xl mb-2">{avatar}</div>
12 <h3 className="text-xl font-bold">{name}</h3>
13 <p className="text-gray-600">{role}</p>
14 {description && (
15 <p className="text-sm text-gray-500 mt-2">{description}</p>
16 )}
17 </div>
18 );
19}Le point d'interrogation dans description?: string indique un champ optionnel : le parent peut l'omettre. C'est pourquoi la carte affiche la description de façon conditionnelle, avec {description && (...)}, et quand il n'y a pas de description, React n'affiche tout simplement pas le paragraphe supplémentaire. L'interface ne change rien au fonctionnement du composant dans le navigateur, car les types disparaissent après la compilation. Elle vérifie seulement que les données transmises respectent le contrat.
Le composant Post - Un post façon Twitter
Place à l'élément le plus important de Kraken's Call : le post. On range les composants réutilisables dans des fichiers séparés, dans le dossier components/. Le fichier components/Post.tsx exporte le composant par défaut (export default) pour qu'une page puisse l'importer :
1// components/Post.tsx
2interface PostProps {
3 author: string;
4 avatar: string;
5 content: string;
6 timestamp: string;
7 likes: number;
8}
9
10export default function Post({
11 author,
12 avatar,
13 content,
14 timestamp,
15 likes
16}: PostProps) {
17 return (
18 <div className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
19 {/* En-tête */}
20 <div className="flex items-center gap-3 mb-4">
21 <div className="text-3xl">{avatar}</div>
22 <div>
23 <h3 className="font-bold text-gray-900">{author}</h3>
24 <p className="text-sm text-gray-500">{timestamp}</p>
25 </div>
26 </div>
27
28 {/* Contenu */}
29 <p className="text-gray-800 mb-4">{content}</p>
30
31 {/* Actions */}
32 <div className="flex items-center gap-6 text-gray-500">
33 <button className="flex items-center gap-2 hover:text-red-500">
34 {likes}
35 </button>
36 <button className="flex items-center gap-2 hover:text-blue-500">
37 Répondre
38 </button>
39 <button className="flex items-center gap-2 hover:text-green-500">
40 Partager
41 </button>
42 </div>
43 </div>
44 );
45}Un post comporte trois parties : un en-tête avec l'avatar et l'auteur, le contenu et une barre d'actions. Le nombre de J'aime se trouve dans un bouton, et l'apparence vient des classes Tailwind dans className, par exemple bg-white p-6 rounded-lg shadow.
Utiliser le composant Post
La page app/page.tsx importe le post et l'insère dans le fil. Le chemin @/components/Post utilise un alias : @/ désigne le dossier racine du projet (create-next-app configure ce raccourci dans tsconfig.json), donc l'import s'écrit de la même façon depuis n'importe quel endroit de app/ :
1// app/page.tsx
2import Post from '@/components/Post';
3
4export default function Feed() {
5 return (
6 <div className="max-w-2xl mx-auto p-8 space-y-4">
7 <Post
8 author="Capitaine Barbe-Rousse"
9 avatar="CB"
10 content="On vient de découvrir une nouvelle île ! La carte pointe vers un trésor énorme"
11 timestamp="il y a 2 heures"
12 likes={42}
13 />
14 <Post
15 author="Morgan Cœur-Noir"
16 avatar="MC"
17 content="Qui veut rejoindre l'expédition vers le nord ? On cherche des marins courageux !"
18 timestamp="il y a 5 heures"
19 likes={18}
20 />
21 </div>
22 );
23}Remarque likes={42} : les nombres se transmettent entre accolades. Écrire likes="42" transmettrait un texte, or l'interface PostProps exige un nombre, donc TypeScript signalerait aussitôt une erreur.
Organiser les composants
Quand un projet grandit, il vaut la peine de suivre une organisation de dossiers éprouvée. Un projet Next.js typique ressemble à ceci :
1app/
2├── page.tsx → Pages
3├── layout.tsx
4└── profile/
5 └── page.tsx
6
7components/ → Composants réutilisables
8├── Post.tsx
9├── Navbar.tsx
10├── Button.tsx
11└── Avatar.tsx
12
13lib/ → Fonctions utilitaires
14└── utils.ts
15
16types/ → Types TypeScript
17└── index.tsLe dossier app/ contient les pages et les layouts, components/ les composants réutilisables, lib/ les fonctions utilitaires et types/ les types TypeScript partagés. Ainsi, chaque pirate de l'équipage sait où chercher le code.
Client Components vs Server Components
Dans Next.js, le code d'un composant peut s'exécuter à deux endroits : sur le serveur ou dans le navigateur. Dans l'App Router, les pages et les layouts sont par défaut des Server Components.
Server Component (par défaut)
- Rendu sur le serveur, le navigateur reçoit le résultat déjà prêt
- Ne peut utiliser ni état ni effets (useState, useEffect), ni les API du navigateur (window, localStorage)
- N'envoie pas son propre code JavaScript au navigateur, donc la page est plus légère
- Peut récupérer des données directement, par exemple depuis une base de données
Un tel composant n'a besoin d'aucune directive. Une simple fonction suffit, et tu peux y récupérer directement des données :
1// Server Component (par défaut)
2export default function ServerPost() {
3 // On peut récupérer des données ici
4 return <div>Post</div>;
5}Client Component
- S'exécute dans le navigateur : Next.js le rend d'abord en HTML, puis React y branche l'état et la gestion des événements
- Peut utiliser les hooks (useState, useEffect)
- Est interactif (onClick, onChange)
- Nécessite
'use client'en haut du fichier
La directive 'use client' trace une frontière : le fichier qui la contient et tout ce qu'il importe se retrouvent dans le JavaScript envoyé au navigateur. Le bouton J'aime doit retenir le nombre de clics, il doit donc être un Client Component :
1'use client';
2
3import { useState } from 'react';
4
5export default function LikeButton() {
6 const [likes, setLikes] = useState(0);
7
8 return (
9 <button onClick={() => setLikes(likes + 1)}>
10 {likes}
11 </button>
12 );
13}La ligne import { useState } from 'react' importe le hook d'état, et onClick réagit au clic. On les verra en détail dans la prochaine leçon.
Règle : utilise les Server Components par défaut et ajoute 'use client' uniquement aux petites parties interactives, comme LikeButton. Je te recommande cette approche : la page du fil reste légère, et seul le code du bouton s'exécute dans le navigateur.
Post interactif avec un bouton J'aime
Réunissons les deux idées dans un post qui compte lui-même ses J'aime. Le composant reçoit le nombre initial de J'aime dans la prop initialLikes et garde la valeur actuelle dans un état :
1'use client';
2
3import { useState } from 'react';
4
5interface PostProps {
6 author: string;
7 avatar: string;
8 content: string;
9 initialLikes: number;
10}
11
12export default function InteractivePost({
13 author,
14 avatar,
15 content,
16 initialLikes
17}: PostProps) {
18 const [likes, setLikes] = useState(initialLikes);
19 const [isLiked, setIsLiked] = useState(false);
20
21 const handleLike = () => {
22 if (isLiked) {
23 setLikes(likes - 1);
24 } else {
25 setLikes(likes + 1);
26 }
27 setIsLiked(!isLiked);
28 };
29
30 return (
31 <div className="bg-white p-6 rounded-lg shadow">
32 <div className="flex items-center gap-3 mb-4">
33 <div className="text-3xl">{avatar}</div>
34 <h3 className="font-bold">{author}</h3>
35 </div>
36
37 <p className="mb-4">{content}</p>
38
39 <button
40 onClick={handleLike}
41 className={`flex items-center gap-2 ${
42 isLiked ? 'text-red-500' : 'text-gray-500'
43 }`}
44 >
45 {isLiked ? 'Tu aimes ça' : 'Aimer'} ({likes})
46 </button>
47 </div>
48 );
49}Chaque clic inverse isLiked et ajoute ou retire un J'aime. La prop initialLikes ne change jamais : seul l'état likes, initialisé avec cette valeur, change.
La prop Children - Les composants wrapper
Parfois, un composant doit être un coffre dans lequel tu ranges n'importe quel contenu. C'est le rôle de la prop spéciale children : tout ce que tu places entre la balise ouvrante et la balise fermante du composant :
1interface CardProps {
2 children: React.ReactNode;
3}
4
5function Card({ children }: CardProps) {
6 return (
7 <div className="bg-white p-6 rounded-lg shadow">
8 {children}
9 </div>
10 );
11}
12
13// Utilisation :
14<Card>
15 <h2>Titre</h2>
16 <p>Contenu de la carte</p>
17</Card>Card ne sait pas ce qu'il y a à l'intérieur : il ajoute seulement un cadre et une ombre. Le type React.ReactNode désigne tout ce que React sait afficher : du texte, des nombres, des éléments JSX et d'autres composants.
Le rendu conditionnel
Tous les pirates n'ont pas de badge de vérification, et tous les comptes ne sont pas premium. Tu peux afficher des parties de l'interface sous condition, directement dans le JSX :
1function Post({ isVerified, isPremium }: {
2 isVerified: boolean;
3 isPremium: boolean;
4}) {
5 return (
6 <div>
7 <h3>Capitaine Barbe-Rousse</h3>
8
9 {/* Opérateur && (affiche quand la condition est vraie) */}
10 {isVerified && <span>Vérifié</span>}
11
12 {/* Opérateur ternaire */}
13 {isPremium ? (
14 <span className="text-yellow-500">Premium</span>
15 ) : (
16 <span className="text-gray-500">Gratuit</span>
17 )}
18 </div>
19 );
20}L'opérateur && affiche un élément seulement quand la condition est vraie. L'opérateur ternaire condition ? a : b choisit l'un des deux éléments. Attention aux nombres : {count && <span>Nouveau</span>} avec count égal à 0 affiche un zéro à l'écran, car React affiche le nombre 0. Il est plus sûr d'écrire {count > 0 && <span>Nouveau</span>}.
Afficher des listes avec map()
Un fil est une liste, et dans React on affiche les listes avec la méthode map() : chaque élément d'un tableau de données devient un élément JSX :
1const pirates = [
2 { id: 1, name: 'Barbe-Rousse', role: 'Capitaine' },
3 { id: 2, name: 'Cœur-Noir', role: 'Navigatrice' },
4 { id: 3, name: 'Polly', role: 'Perroquet' },
5];
6
7function PirateList() {
8 return (
9 <div className="space-y-4">
10 {pirates.map((pirate) => (
11 <div key={pirate.id} className="bg-white p-4 rounded">
12 <h3 className="font-bold">{pirate.name}</h3>
13 <p className="text-gray-600">{pirate.role}</p>
14 </div>
15 ))}
16 </div>
17 );
18}Important : chaque élément de la liste a besoin de la prop key. Grâce à elle, React distingue les éléments et sait lequel a changé, est apparu ou a disparu. La meilleure clé est un id stable issu des données. L'index fourni par map() change quand tu ajoutes un post en haut de la liste, il ne convient donc pas comme clé.
Récapitulatif
- Composants - des fonctions qui retournent du JSX, avec un nom qui commence par une majuscule
- Props - des données transmises aux composants, en lecture seule
- Interface TypeScript - les types des props,
?indique un champ optionnel - Server Components - par défaut dans l'App Router, sans état ni API du navigateur
- Client Components -
'use client', interactifs - useState - l'état d'un composant (plus de détails dans la prochaine leçon)
- children - le contenu entre les balises du composant
- Rendu conditionnel -
&&et l'opérateur ternaire - map() - afficher des listes avec la prop
key
Dans le prochain exercice, on va construire un fil complet avec la possibilité d'ajouter des posts. Retiens : un composant, c'est le sceau du capitaine, gravé une seule fois et appliqué dans tout le fil.
Code de cette leçon : App.jsx
1// Composants React avec des Props
2function PirateCard({ name, role, avatar, description }) {
3 return (
4 <div className="bg-white p-6 rounded-lg shadow hover:shadow-xl transition-shadow">
5 <div className="text-5xl mb-3 text-center">{avatar}</div>
6 <h3 className="text-xl font-bold text-gray-900 mb-1">{name}</h3>
7 <p className="text-blue-600 text-sm font-medium mb-3">{role}</p>
8 {description && (
9 <p className="text-gray-600 text-sm">{description}</p>
10 )}
11 </div>
12 );
13}
14
15export default function App() {
16 return (
17 <div className="min-h-screen bg-gradient-to-br from-slate-900 to-blue-900 p-8">
18 <h1 className="text-4xl font-bold text-white text-center mb-8">
19 L'équipage du Black Pearl
20 </h1>
21
22 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto">
23 <PirateCard
24 name="Capitaine Barbe-Rousse"
25 role="Capitaine du navire"
26 avatar="CB"
27 description="Commande l'équipage d'une main de fer et avec un esprit stratégique"
28 />
29 <PirateCard
30 name="Morgan Cœur-Noir"
31 role="Navigatrice"
32 avatar="MC"
33 description="Connaît chaque étoile du ciel et chaque île de la carte"
34 />
35 <PirateCard
36 name="Polly Piaille"
37 role="Perroquet de communication"
38 avatar="PP"
39 description="Livre les messages plus vite qu'un télégraphe"
40 />
41 <PirateCard
42 name="Jack Main-d'Argent"
43 role="Mécanicien du pont"
44 avatar="JM"
45 description="Répare tout - des canons aux boussoles"
46 />
47 <PirateCard
48 name="Anne Bonny"
49 role="Gardienne du trésor"
50 avatar="AB"
51 description="Garde les trésors et compte chaque pièce"
52 />
53 <PirateCard
54 name="Calico Jack"
55 role="Cuisinier"
56 avatar="CJ"
57 description="Cuisine les meilleurs plats de toutes les Caraïbes"
58 />
59 </div>
60 </div>
61 );
62}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. Que sont les Props en React ?
2. Que retourne un composant React ?
Voici 2 des 8 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Écris un composant PirateCard (export default) avec les props name, role et avatar de type string. Décris les types des props dans une interface TypeScript, par exemple PirateCardProps. La carte affiche l'avatar, le nom dans un titre h3 et le rôle dans un paragraphe p.
- Tri horizontal
Ordonne les éléments qui transmettent une prop name au composant :
- Clique dans l'ordre
Clique sur les éléments pour utiliser le composant Post sans props :
- Tri vertical
Ordonne les lignes d'une définition simple de composant :
- Éditeur de code
Écris un composant Post façon Twitter (export default) : décris les props author et content de type string et likes de type number dans une interface TypeScript. Affiche l'auteur dans un titre h3, le contenu dans un paragraphe p et le nombre de likes dans un élément button. Stylise le post avec des classes Tailwind dans className, par exemple bg-white p-6 rounded-lg shadow.
- Tri horizontal
Ordonne les éléments qui commencent à mapper le tableau pirates :
- Tri vertical
Ordonne les lignes qui affichent une liste de pirates avec map() :
- Clique dans l'ordre
Clique sur les éléments pour construire l'import du hook useState :
- Tri vertical
Ordonne comment les données atteignent un composant via les props :