Cours Vibe coding avec l'IA · Module 3: Routing et composants
La gestion de l'état - useState et interactivité
Dans cette leçon8
Kraken's Call ressemble déjà à une vraie application, mais rien ne réagit aux clics. Essayons d'ajouter un compteur de J'aime de la façon la plus simple, avec une variable ordinaire :
1function LikeButton() {
2 let likes = 0;
3
4 return (
5 <button
6 onClick={() => {
7 likes++;
8 console.log(likes);
9 }}
10 >
11 J'aime : {likes}
12 </button>
13 );
14}La console affiche 1, 2, 3, mais l'écran affiche toujours 0. React ne sait pas que la variable a changé, donc il ne redessine pas le composant. Et même s'il le faisait, la variable repartirait de zéro, car la fonction du composant s'exécute depuis le début à chaque rendu.
Il nous faut donc une mémoire qui survit entre les rendus, et un signal qui dit « les données ont changé, rafraîchis l'écran ». Le hook useState apporte les deux. Vois-le comme le journal de bord : le capitaine y note l'état actuel du navire, et chaque nouvelle entrée indique à l'équipage qu'il faut vérifier ce qui a changé.
Ce que tu vas apprendre :
- créer l'état d'un composant avec
useStateet le modifier avec un setter, - mettre à jour sans risque les tableaux et les objets gardés dans l'état,
- construire un formulaire contrôlé avec
value,onChangeetonSubmit, - relier le formulaire à une liste de posts pour obtenir un fil qui fonctionne,
- remonter l'état vers un parent commun (lifting state up),
- lancer des effets de bord avec
useEffectet sauvegarder les posts dans localStorage.
useState - Le hook de gestion de l'état
useState est un hook qui permet aux composants de retenir des données entre les rendus. Un hook est une fonction spéciale de React dont le nom commence par use, appelée au niveau supérieur d'un composant (pas dans une condition ni dans une boucle). L'état vit dans le navigateur, c'est pourquoi le fichier commence par la directive 'use client'.
Utilisation de base
Voici un compteur qui fonctionne vraiment. On garde la valeur dans l'état count, et le bouton la modifie avec setCount :
1'use client';
2
3import { useState } from 'react';
4
5export default function Counter() {
6 // [valeur, fonction de mise à jour] = useState(valeur initiale)
7 const [count, setCount] = useState(0);
8
9 return (
10 <div>
11 <p>Compteur : {count}</p>
12 <button onClick={() => setCount(count + 1)}>
13 +1
14 </button>
15 </div>
16 );
17}Comment ça marche :
useState(0)crée un état avec la valeur initiale 0 et retourne un tableau de deux élémentscountest la valeur actuellesetCountest la fonction qui change la valeur, le setter- Quand tu cliques sur le bouton,
setCountenregistre la nouvelle valeur, React refait le rendu du composant, et l'écran affiche le nouveau nombre
L'écriture const [count, setCount] = useState(0) est une déstructuration de tableau. Tu choisis les noms toi-même, mais par convention le setter s'appelle set suivi du nom de l'état. Juste en dessous, tu peux tester le coffre au trésor des pirates : deux compteurs, chacun avec son propre état.
Code de cette leçon : App.jsx
1// Compteur avec useState
2import { useState } from 'react';
3
4export default function App() {
5 const [count, setCount] = useState(0);
6 const [coins, setCoins] = useState(100);
7
8 return (
9 <div className="min-h-screen bg-gradient-to-br from-yellow-900 to-amber-800 flex items-center justify-center p-8">
10 <div className="bg-white rounded-lg shadow-2xl p-8 max-w-md w-full">
11 <h1 className="text-3xl font-bold text-center mb-8">
12 Trésor des pirates
13 </h1>
14
15 {/* Compteur de trésors */}
16 <div className="bg-yellow-100 p-6 rounded-lg mb-6 text-center">
17 <p className="text-gray-600 mb-2">Trésors collectés</p>
18 <p className="text-5xl font-bold text-yellow-900">{count}</p>
19 </div>
20
21 {/* Boutons */}
22 <div className="grid grid-cols-2 gap-4 mb-6">
23 <button
24 onClick={() => setCount(count + 1)}
25 className="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition-colors"
26 >
27 + Trouvé un !
28 </button>
29 <button
30 onClick={() => setCount(count - 1)}
31 className="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition-colors"
32 disabled={count === 0}
33 >
34 - Perdu un
35 </button>
36 </div>
37
38 <button
39 onClick={() => setCount(0)}
40 className="w-full bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
41 >
42 Réinitialiser le compteur
43 </button>
44
45 {/* Compteur de pièces */}
46 <div className="mt-8 bg-amber-100 p-6 rounded-lg text-center">
47 <p className="text-gray-600 mb-2">Pièces d'or</p>
48 <p className="text-3xl font-bold text-amber-900 mb-4">{coins}</p>
49 <div className="grid grid-cols-2 gap-2">
50 <button
51 onClick={() => setCoins(coins + 10)}
52 className="bg-amber-600 hover:bg-amber-700 text-white font-semibold py-2 px-4 rounded transition-colors"
53 >
54 +10
55 </button>
56 <button
57 onClick={() => setCoins(coins - 10)}
58 className="bg-amber-800 hover:bg-amber-900 text-white font-semibold py-2 px-4 rounded transition-colors"
59 disabled={coins < 10}
60 >
61 -10
62 </button>
63 </div>
64 </div>
65 </div>
66 </div>
67 );
68}Ce que le setter ne fait pas
Le setter ne modifie pas la variable count tout de suite. Il demande un nouveau rendu à React, et dans l'appel en cours de la fonction, count garde encore l'ancienne valeur :
1const handleClick = () => {
2 setCount(count + 1);
3 console.log(count); // toujours l'ancienne valeur, par ex. 0
4};C'est pourquoi deux appels à setCount(count + 1) dans un même clic n'augmentent le compteur que de 1 : les deux voient le même count. Quand la nouvelle valeur dépend de la précédente, passe au setter une fonction de mise à jour :
1const addTwo = () => {
2 setCount((c) => c + 1);
3 setCount((c) => c + 1); // maintenant le compteur augmente de 2
4};React appelle cette fonction avec la valeur d'état la plus récente, donc aucune modification ne se perd. Pour un simple clic +1, les deux écritures fonctionnent de la même façon.
Différents types d'état
L'état peut contenir n'importe quelle valeur : du texte, un booléen, un tableau ou un objet. Chaque type a ses usages typiques.
Texte (string)
Le texte dans l'état sert le plus souvent pour les champs de formulaire. Le champ affiche la valeur de l'état, et chaque frappe au clavier enregistre la nouvelle valeur via e.target.value :
1const [name, setName] = useState('');
2
3<input
4 value={name}
5 onChange={(e) => setName(e.target.value)}
6/>Booléen (boolean)
Un booléen convient bien aux choses activées ou désactivées, par exemple un menu déplié. Le bouton l'inverse avec l'opérateur ! :
1const [isOpen, setIsOpen] = useState(false);
2
3<button onClick={() => setIsOpen(!isOpen)}>
4 Basculer
5</button>Tableau
Un tableau dans l'état se remplace toujours par un nouveau tableau. L'opérateur spread ...items copie les anciens éléments, et le nouvel élément va à la fin :
1const [items, setItems] = useState<string[]>([]);
2
3const addItem = (item: string) => {
4 setItems([...items, item]);
5};useState<string[]>([]) indique à TypeScript que le tableau contiendra des textes. N'écris pas items.push(item) : push modifie le tableau existant sur place sans appeler le setter, donc React ne sait rien du changement. Même setItems(items) après push ne sert à rien, car React reçoit le même tableau (la même référence), conclut que rien n'a changé et ne rafraîchit pas l'écran.
Objet
C'est la même chose avec les objets. Tu crées un nouvel objet, tu copies les anciens champs avec ...user et tu remplaces seulement ce qui change :
1const [user, setUser] = useState({
2 name: 'Barbe-Rousse',
3 age: 35
4});
5
6setUser({ ...user, age: 36 });Après ce changement, name vaut toujours 'Barbe-Rousse' et age vaut 36. L'ancien objet reste intact, et React, qui en reçoit un nouveau, sait qu'il faut refaire le rendu du composant.
Formulaire pour ajouter des posts
Voici maintenant le formulaire avec lequel l'équipage ajoute des posts. C'est un formulaire contrôlé : tu gardes la valeur du champ dans l'état et tu la transmets avec value, tandis que onChange enregistre chaque modification. Ainsi, tu sais à tout moment ce que contient le champ et tu peux, par exemple, compter les caractères :
1'use client';
2
3import { useState } from 'react';
4
5export default function CreatePostForm() {
6 const [content, setContent] = useState('');
7 const [charCount, setCharCount] = useState(0);
8 const MAX_LENGTH = 280;
9
10 const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
11 e.preventDefault(); // Empêche le rechargement de la page
12
13 if (content.trim()) {
14 console.log('Nouveau post :', content);
15 setContent(''); // Vide le formulaire
16 setCharCount(0);
17 }
18 };
19
20 const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
21 const text = e.target.value;
22 setContent(text);
23 setCharCount(text.length);
24 };
25
26 return (
27 <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow">
28 <textarea
29 value={content}
30 onChange={handleChange}
31 maxLength={MAX_LENGTH}
32 className="w-full p-4 border rounded-lg resize-none focus:ring-2 focus:ring-blue-500 outline-none"
33 rows={4}
34 />
35
36 <div className="flex items-center justify-between mt-4">
37 <span className={`text-sm ${
38 charCount > MAX_LENGTH * 0.9 ? 'text-red-500' : 'text-gray-500'
39 }`}>
40 {charCount}/{MAX_LENGTH}
41 </span>
42
43 <button
44 type="submit"
45 disabled={!content.trim() || charCount > MAX_LENGTH}
46 className="bg-blue-600 text-white px-6 py-2 rounded-lg font-semibold disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-700 transition"
47 >
48 Envoyer
49 </button>
50 </div>
51 </form>
52 );
53}Quelques points méritent une explication :
- Par défaut, quand on soumet un formulaire, le navigateur envoie les données à l'adresse du formulaire et recharge la page, et tout l'état est alors perdu.
e.preventDefault()bloque ce comportement, donc c'est ton code qui prend le relais. - Tu vides le champ en remettant l'état à une chaîne vide :
setContent(''). Commevaluevient de l'état, le champ se vide immédiatement. React.SyntheticEvent<HTMLFormElement>est le type de l'événement d'envoi du formulaire, etReact.ChangeEvent<HTMLTextAreaElement>le type d'une modification dans un champtextarea.
Voici comment fonctionne une version enrichie de ce formulaire, avec une barre de progression et un bouton qui se désactive quand le texte est vide ou trop long :
Code de cette leçon : App.jsx
1// Formulaire de création de post
2import { useState } from 'react';
3
4export default function App() {
5 const [content, setContent] = useState('');
6 const [charCount, setCharCount] = useState(0);
7 const MAX_LENGTH = 280;
8
9 const handleSubmit = (e) => {
10 e.preventDefault();
11
12 if (content.trim()) {
13 alert(`Post envoyé : "${content}"`);
14 setContent('');
15 setCharCount(0);
16 }
17 };
18
19 const handleChange = (e) => {
20 const text = e.target.value;
21 setContent(text);
22 setCharCount(text.length);
23 };
24
25 const progress = (charCount / MAX_LENGTH) * 100;
26 const isNearLimit = charCount > MAX_LENGTH * 0.9;
27 const isOverLimit = charCount > MAX_LENGTH;
28
29 return (
30 <div className="min-h-screen bg-gradient-to-br from-blue-900 to-slate-900 p-8 flex items-center justify-center">
31 <div className="max-w-2xl w-full">
32 <h1 className="text-4xl font-bold text-white text-center mb-8">
33 Kraken's Call
34 </h1>
35
36 <form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-xl p-6">
37 <div className="mb-4">
38 <div className="flex items-center gap-3 mb-4">
39 <div className="text-4xl">CB</div>
40 <div>
41 <h3 className="font-bold text-gray-900">Capitaine Barbe-Rousse</h3>
42 <p className="text-sm text-gray-500">@redbeard_captain</p>
43 </div>
44 </div>
45
46 <textarea
47 value={content}
48 onChange={handleChange}
49 placeholder="Quoi de neuf en mer, pirates ?"
50 maxLength={MAX_LENGTH}
51 className="w-full p-4 border-2 border-gray-200 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none text-lg"
52 rows={5}
53 />
54 </div>
55
56 {/* Barre de progression */}
57 <div className="mb-4">
58 <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
59 <div
60 className={`h-full transition-all duration-300 ${
61 isOverLimit ? 'bg-red-500' : isNearLimit ? 'bg-yellow-500' : 'bg-blue-500'
62 }`}
63 style={{ width: `${Math.min(progress, 100)}%` }}
64 />
65 </div>
66 </div>
67
68 <div className="flex items-center justify-between">
69 <span className={`text-sm font-medium ${
70 isOverLimit ? 'text-red-500' : isNearLimit ? 'text-yellow-600' : 'text-gray-500'
71 }`}>
72 {charCount}/{MAX_LENGTH}
73 {isOverLimit && ' - Trop de caractères !'}
74 </span>
75
76 <button
77 type="submit"
78 disabled={!content.trim() || isOverLimit}
79 className="bg-blue-600 hover:bg-blue-700 text-white font-bold px-8 py-3 rounded-lg transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed shadow-lg disabled:shadow-none"
80 >
81 Envoyer
82 </button>
83 </div>
84 </form>
85 </div>
86 </div>
87 );
88}Fil avec des posts dynamiques
Il est temps de relier le formulaire à la liste des posts. Le fil a besoin de deux états : le tableau des posts et le contenu du nouveau post. L'interface Post décrit à quoi ressemble un post :
1'use client';
2
3import { useState } from 'react';
4
5interface Post {
6 id: number;
7 author: string;
8 avatar: string;
9 content: string;
10 timestamp: Date;
11 likes: number;
12}
13
14export default function Feed() {
15 const [posts, setPosts] = useState<Post[]>([
16 {
17 id: 1,
18 author: 'Capitaine Barbe-Rousse',
19 avatar: 'CB',
20 content: 'On vient de découvrir une nouvelle île !',
21 timestamp: new Date(),
22 likes: 42,
23 },
24 ]);
25
26 const [newPostContent, setNewPostContent] = useState('');
27
28 const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
29 e.preventDefault();
30
31 if (!newPostContent.trim()) return;
32
33 const newPost: Post = {
34 id: Date.now(),
35 author: 'Morgan Cœur-Noir',
36 avatar: 'MC',
37 content: newPostContent,
38 timestamp: new Date(),
39 likes: 0,
40 };
41
42 setPosts([newPost, ...posts]); // Ajoute au début
43 setNewPostContent('');
44 };
45
46 return (
47 <div className="max-w-2xl mx-auto p-8">
48 {/* Formulaire */}
49 <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow mb-6">
50 <textarea
51 value={newPostContent}
52 onChange={(e) => setNewPostContent(e.target.value)}
53 className="w-full p-4 border rounded-lg"
54 rows={3}
55 />
56 <button
57 type="submit"
58 className="mt-4 bg-blue-600 text-white px-6 py-2 rounded-lg"
59 >
60 Envoyer
61 </button>
62 </form>
63
64 {/* Liste des posts */}
65 <div className="space-y-4">
66 {posts.map((post) => (
67 <div key={post.id} className="bg-white p-6 rounded-lg shadow">
68 <div className="flex items-center gap-3 mb-4">
69 <div className="text-3xl">{post.avatar}</div>
70 <div>
71 <h3 className="font-bold">{post.author}</h3>
72 <p className="text-sm text-gray-500">
73 {post.timestamp.toLocaleTimeString('fr-FR')}
74 </p>
75 </div>
76 </div>
77 <p className="text-gray-800">{post.content}</p>
78 <div className="mt-4 text-gray-500">
79 {post.likes}
80 </div>
81 </div>
82 ))}
83 </div>
84 </div>
85 );
86}La ligne clé est setPosts([newPost, ...posts]). Elle crée un nouveau tableau avec le nouveau post en premier et tous les anciens à sa suite, donc l'entrée la plus récente arrive en haut du fil. Chaque post a un id stable (ici Date.now()), qui sert de key dans map(). Après l'envoi, le formulaire vide le champ, et tu peux tout de suite écrire le post suivant.
Lifting State Up - Partager l'état entre des composants
Parfois, deux composants ont besoin des mêmes données : le bouton J'aime change le nombre, et le compteur sous le post l'affiche. Si chacun gardait son propre état, ils afficheraient des nombres différents. La solution s'appelle lifting state up : on remonte l'état vers leur parent commun, qui en devient le propriétaire :
1'use client';
2
3import { useState } from 'react';
4
5// Composant bouton J'aime
6function LikeButton({
7 likes,
8 onLike
9}: {
10 likes: number;
11 onLike: () => void;
12}) {
13 return (
14 <button onClick={onLike} className="flex items-center gap-2">
15 {likes}
16 </button>
17 );
18}
19
20// Composant compteur de J'aime
21function LikeCounter({ likes }: { likes: number }) {
22 return (
23 <div className="text-sm text-gray-500">
24 J'aime du post : {likes}
25 </div>
26 );
27}
28
29// Le parent détient l'état
30export default function Post() {
31 const [likes, setLikes] = useState(42);
32
33 const handleLike = () => {
34 setLikes(likes + 1);
35 };
36
37 return (
38 <div className="bg-white p-6 rounded-lg">
39 <p className="mb-4">Contenu du post...</p>
40
41 <LikeButton likes={likes} onLike={handleLike} />
42 <LikeCounter likes={likes} />
43 </div>
44 );
45}Les données descendent : le parent transmet likes aux deux enfants sous forme de prop. Les changements remontent : le parent transmet aussi la fonction handleLike dans la prop onLike, et LikeButton l'appelle au clic. Les deux props se trouvent dans la même balise JSX, donc le parent transmet la valeur et la fonction en même temps. L'enfant ne modifie pas l'état lui-même : il demande au parent de le faire. Après le changement, les deux enfants reçoivent le même nouveau nombre.
useEffect - Les effets de bord
Un composant ne se contente pas toujours de dessiner l'interface. Lancer un minuteur, récupérer des données ou enregistrer dans localStorage sont des effets de bord, et useEffect sert justement à cela. React exécute un effet après le rendu du composant, quand les changements sont déjà à l'écran :
1'use client';
2
3import { useState, useEffect } from 'react';
4
5export default function Timer() {
6 const [seconds, setSeconds] = useState(0);
7
8 useEffect(() => {
9 // S'exécute une fois, après le premier rendu
10 const interval = setInterval(() => {
11 setSeconds((s) => s + 1);
12 }, 1000);
13
14 // Nettoyage - supprime le minuteur quand le composant disparaît
15 return () => clearInterval(interval);
16 }, []); // [] = ne s'exécute qu'une seule fois
17
18 return <div>Temps écoulé : {seconds}s</div>;
19}La fonction retournée par l'effet sert au nettoyage (cleanup). React l'appelle quand le composant disparaît de l'écran, pour que le minuteur ne continue pas à tourner en arrière-plan. Remarque aussi setSeconds((s) => s + 1) : le minuteur utilise une fonction de mise à jour, car un effet avec un tableau vide ne voit que la valeur seconds du premier rendu.
Le tableau de dépendances
Le deuxième argument de useEffect décide du moment où l'effet s'exécute. Tu as le choix entre trois variantes :
1// S'exécute une seule fois (au montage)
2useEffect(() => {
3 console.log('Composant monté');
4}, []);
5
6// S'exécute après le premier rendu et quand count change
7useEffect(() => {
8 console.log('Count a changé :', count);
9}, [count]);
10
11// S'exécute après chaque rendu
12useEffect(() => {
13 console.log('Chaque rendu');
14}); // Pas de []D'après la documentation de React, un effet avec un tableau de dépendances s'exécute après le premier rendu, puis après chaque rendu où l'une des dépendances a changé. Un tableau vide [] signifie aucune dépendance, donc l'effet s'exécute une fois, après le premier rendu. Sans tableau, l'effet s'exécute après chaque rendu, ce qui est rarement utile.
En mode développement (npm run dev), tu verras « Composant monté » deux fois dans la console. Ce n'est pas un bug : le Strict Mode est activé par défaut dans l'App Router et, en développement, il exécute chaque effet une fois de plus (avec un nettoyage entre les deux) pour repérer les effets qui ne nettoient pas correctement. Dans la version de production, un effet avec [] s'exécute une fois.
LocalStorage - Sauvegarder l'état
Pour finir, faisons en sorte que les posts ne disparaissent pas quand on rafraîchit la page. localStorage est un petit espace de stockage dans le navigateur, qui garde du texte sous des clés. Tu transformes un tableau en texte avec JSON.stringify, et inversement avec JSON.parse.
Voici à quoi cela ressemble dans le code :
1'use client';
2
3import { useState, useEffect } from 'react';
4
5export default function Feed() {
6 const [posts, setPosts] = useState<Post[]>([]);
7 const [loaded, setLoaded] = useState(false);
8
9 // Charge depuis localStorage au démarrage
10 useEffect(() => {
11 const saved = localStorage.getItem('kraken-posts');
12 if (saved) {
13 setPosts(JSON.parse(saved));
14 }
15 setLoaded(true);
16 }, []);
17
18 // Sauvegarde seulement après le chargement
19 useEffect(() => {
20 if (loaded) {
21 localStorage.setItem('kraken-posts', JSON.stringify(posts));
22 }
23 }, [posts, loaded]);
24
25 // Reste du code...
26}Le drapeau loaded garantit le bon ordre. Sans lui, le deuxième effet enregistrerait un tableau vide au premier rendu et écraserait les posts sauvegardés, et dans npm run dev, React (Strict Mode) exécute les effets deux fois, donc les posts disparaîtraient à chaque rafraîchissement.
Les deux effets utilisent localStorage, une API du navigateur. Ce n'est pas un problème, car les effets ne s'exécutent que dans le navigateur, après le rendu du composant, et le fichier a de toute façon la directive 'use client'.
Récapitulatif
- useState - la mémoire du composant et le signal pour refaire le rendu
- Setter - modifie l'état seulement au rendu suivant ; quand la nouvelle valeur dépend de l'ancienne, utilise
setCount((c) => c + 1) - Tableaux et objets dans l'état - copie-les toujours :
[...items, item],{ ...user, age: 36 } - Formulaires -
value,onChange,onSubmit - e.preventDefault() - empêche le rechargement de la page
- Listes dynamiques - un nouveau post en haut :
[newPost, ...posts] - Lifting state up - les données descendent en props, les changements remontent par une fonction
- useEffect - effets de bord (minuteurs, récupération de données, localStorage)
- Tableau de dépendances - décide quand l'effet s'exécute
- localStorage - sauvegarder des données dans le navigateur
Juste en dessous t'attend un fil complet avec un formulaire et des J'aime, prêt pour tes expériences. Tu as maintenant une application Kraken's Call interactive ! Retiens : l'état, c'est le journal de bord du composant, et chaque entrée écrite avec le setter rafraîchit l'écran.
Code de cette leçon : App.jsx
1// Fil complet avec des posts dynamiques
2import { useState } from 'react';
3
4function Post({ author, avatar, content, timestamp, likes, onLike }) {
5 return (
6 <div className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
7 <div className="flex items-center gap-3 mb-4">
8 <div className="text-4xl">{avatar}</div>
9 <div className="flex-1">
10 <h3 className="font-bold text-gray-900">{author}</h3>
11 <p className="text-sm text-gray-500">{timestamp}</p>
12 </div>
13 </div>
14
15 <p className="text-gray-800 mb-4">{content}</p>
16
17 <div className="flex items-center gap-6 text-gray-500 border-t pt-4">
18 <button
19 onClick={onLike}
20 className="flex items-center gap-2 hover:text-red-500 transition-colors"
21 >
22 <span className="text-sm font-medium">{likes}</span>
23 </button>
24 </div>
25 </div>
26 );
27}
28
29export default function App() {
30 const [posts, setPosts] = useState([
31 {
32 id: 1,
33 author: 'Capitaine Barbe-Rousse',
34 avatar: 'CB',
35 content: 'On vient de découvrir une nouvelle île ! La carte pointe vers un trésor énorme',
36 timestamp: new Date().toLocaleString('fr-FR'),
37 likes: 42,
38 },
39 ]);
40
41 const [newPostContent, setNewPostContent] = useState('');
42
43 const handleSubmit = (e) => {
44 e.preventDefault();
45
46 if (!newPostContent.trim()) return;
47
48 const newPost = {
49 id: Date.now(),
50 author: 'Morgan Cœur-Noir',
51 avatar: 'MC',
52 content: newPostContent,
53 timestamp: new Date().toLocaleString('fr-FR'),
54 likes: 0,
55 };
56
57 setPosts([newPost, ...posts]);
58 setNewPostContent('');
59 };
60
61 const handleLike = (postId) => {
62 setPosts(posts.map(post =>
63 post.id === postId
64 ? { ...post, likes: post.likes + 1 }
65 : post
66 ));
67 };
68
69 return (
70 <div className="min-h-screen bg-gray-100 p-8">
71 <div className="max-w-2xl mx-auto">
72 <h1 className="text-4xl font-bold mb-8 text-center">
73 Kraken's Call
74 </h1>
75
76 {/* Formulaire de création de post */}
77 <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow mb-6">
78 <div className="flex items-center gap-3 mb-4">
79 <div className="text-4xl">MC</div>
80 <h3 className="font-bold text-gray-900">Morgan Cœur-Noir</h3>
81 </div>
82 <textarea
83 value={newPostContent}
84 onChange={(e) => setNewPostContent(e.target.value)}
85 placeholder="Quoi de neuf en mer ?"
86 className="w-full p-4 border rounded-lg resize-none focus:ring-2 focus:ring-blue-500 outline-none"
87 rows={3}
88 />
89 <button
90 type="submit"
91 className="mt-4 bg-blue-600 hover:bg-blue-700 text-white font-bold px-6 py-2 rounded-lg transition-colors"
92 >
93 Envoyer
94 </button>
95 </form>
96
97 {/* Fil des posts */}
98 <div className="space-y-4">
99 {posts.map((post) => (
100 <Post
101 key={post.id}
102 {...post}
103 onLike={() => handleLike(post.id)}
104 />
105 ))}
106 </div>
107 </div>
108 </div>
109 );
110}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 retourne useState(0) ?
2. Quelle est la deuxième valeur du tableau useState, par exemple setCount ?
Voici 2 des 9 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Écris un composant Counter (export default), un compteur de butin. Ajoute 'use client' en haut du fichier. Garde le nombre dans un état count avec useState(0), affiche-le et ajoute à côté un bouton +1 qui augmente le compteur de 1 à chaque clic.
- Tri horizontal
Ordonne les éléments d'une déclaration d'état pour le compteur :
- Clique dans l'ordre
Clique sur les éléments pour bloquer le rechargement de la page :
- Tri horizontal
Ordonne les éléments qui augmentent le compteur de un :
- Éditeur de code
Écris un composant CreatePostForm (export default), le formulaire du journal de bord du capitaine. Ajoute 'use client' en haut du fichier. Dans une balise form, place un textarea dont le contenu est gardé dans un état useState (value et onChange) et un bouton d'envoi (un button de type submit). Dans onSubmit, appelle e.preventDefault() et vide le champ.
- Clique dans l'ordre
Clique sur les éléments pour mettre à jour l'état avec la valeur de l'input :
- Tri vertical
Ordonne ce qui se passe après la soumission d'un formulaire :
- Tri horizontal
Ordonne les étapes du fonctionnement de useState :
- Clique dans l'ordre
Clique sur les éléments pour ajouter un nouveau post au début de la liste :
- Tri vertical
Ordonne les étapes pour faire remonter l'état vers le parent :
- Éditeur de code
Écris un composant Feed (export default) qui relie le formulaire à la liste des posts. Ajoute 'use client' en haut du fichier. Garde les posts dans un état tableau avec useState et le texte du nouveau post dans un état séparé relié à un textarea (value et onChange). À l'envoi du formulaire, appelle e.preventDefault(), ajoute le nouveau post en haut de la liste et vide le champ. Affiche les posts avec map() et une prop key.
- Clique dans l'ordre
Clique sur les éléments pour démarrer un useEffect qui s'exécute une seule fois :
- Tri vertical
Ordonne les étapes pour sauvegarder et charger les posts :