12 KiB
🎮 Énoncé React #2 — ton site Geometry Dash (version longue)
Bien joué Guillaume : tu as un Header avec ton intro, un Footer, un composant Carte avec des props, et un Compteur avec useState. Les briques sont là. 👌
Le souci : tes cartes affichent encore le contenu du tuto (Suivi en temps réel…), tu les écris une par une à la main, et ton compteur dit encore « Véhicules ». On va en faire un vrai site sur tes trilogies, et surtout apprendre beaucoup de React au passage.
🎯 Ce document est une série de modules (A → H), du plus simple au plus costaud. Fais-les dans l'ordre, teste à chaque fois. Je t'explique les mécaniques et te donne des mini-exemples ; à toi d'appliquer sur ton thème (je ne donne pas le site fini). Compte plusieurs séances — c'est normal.
| Module | Tu apprends | Niveau |
|---|---|---|
| A | Listes : .map() + key |
⭐ |
| B | Affichage conditionnel + extraire un <Badge> |
⭐ |
| C | État local par carte (useState) |
⭐ |
| D | Champ contrôlé + recherche/filtre + état vide | ⭐⭐ |
| E | Faire remonter l'état (lifting state up) + callbacks en props | ⭐⭐ |
| F | useEffect : titre de page + persistance localStorage |
⭐⭐ |
| G | Charger les données avec fetch (dans React) |
⭐⭐⭐ |
| H | Re-découpage propre (<Liste>, <BarreRecherche>) |
⭐⭐ |
🧱 Module A — Afficher une liste : .map() + key
Aujourd'hui tu écris chaque carte à la main. La bonne méthode : un tableau de données + .map().
A.1 — un tableau (au-dessus de function App())
const trilogies = [
{ id: 1, nom: "Bloodbath", createur: "Riot", difficulte: "Extreme Demon" },
{ id: 2, nom: "Cataclysm", createur: "Ggb0y", difficulte: "Extreme Demon" },
{ id: 3, nom: "Sonic Wave", createur: "Cyclic", difficulte: "Extreme Demon" },
// ... tes 5 trilogies
];
A.2 — générer les cartes
function App() {
return (
<div className="body">
<Header />
{trilogies.map((trilo) => (
<Carte key={trilo.id} nom={trilo.nom} createur={trilo.createur} difficulte={trilo.difficulte} />
))}
<Footer />
</div>
);
}
🧩 Deux nouveautés :
{ ... }dans le JSX = « insère ici du JavaScript ».{trilogies.map(...)}insère une carte par élément.key={...}= un identifiant unique par élément (utiliseid, pas l'index). React en a besoin pour suivre chaque carte ; sans lui → warning dans la console.
✅ Test : ajoute une trilogie au tableau → une carte apparaît toute seule.
🎖️ Module B — Affichage conditionnel + un composant <Badge>
D'abord enrichis Carte :
function Carte(props) {
return (
<div className="carte">
<h2>{props.nom}</h2>
<p>Par {props.createur}</p>
<p>Difficulté : {props.difficulte}</p>
</div>
);
}
Affichage conditionnel — afficher un truc seulement si une condition est vraie :
{props.difficulte === "Extreme Demon" && <span className="badge">🔥 Extreme</span>}
condition && <...>= « si vrai, affiche, sinon rien ».- choix entre deux :
{props.fini ? "✅ battu" : "❌ pas battu"}.
💡 Dans le JSX on n'écrit pas de
ifà l'intérieur dureturn: on utilise&&(afficher ou rien) ou? :(A ou B).
Exercice bonus — extraire un composant <Badge> (1 composant = 1 fichier) :
// components/Badge.jsx
function Badge({ difficulte }) {
if (difficulte !== "Extreme Demon") return null; // un composant peut renvoyer "rien"
return <span className="badge">🔥 Extreme</span>;
}
export default Badge;
🔎 Nouveau :
function Badge({ difficulte })— on « déballe » directement la prop entre accolades au lieu d'écrireprops.difficultepartout. Etreturn null= « n'affiche rien ». Deux outils très courants.
✅ Test : seules tes trilogies « Extreme Demon » montrent le badge.
❤️ Module C — État local : chaque Carte a son useState
Mets un bouton « j'aime » dans Carte :
import { useState } from "react";
function Carte(props) {
const [likes, setLikes] = useState(0);
return (
<div className="carte">
<h2>{props.nom}</h2>
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
</div>
);
}
🧩 Chaque
<Carte />est une instance indépendante avec son propre état : liker une carte ne touche pas les autres.
✅ Test : le ❤️ d'une carte monte sans bouger les autres.
🔎 Module D — Champ contrôlé + recherche
Nouveau concept important : un champ dont la valeur est pilotée par un useState (on dit « contrôlé »).
function App() {
const [recherche, setRecherche] = useState("");
const filtrees = trilogies.filter((t) =>
t.nom.toLowerCase().includes(recherche.toLowerCase())
);
return (
<div className="body">
<Header />
<input
type="search"
placeholder="Rechercher une trilogie…"
value={recherche} // l'input AFFICHE l'état
onChange={(e) => setRecherche(e.target.value)} // toute frappe MET À JOUR l'état
/>
{filtrees.length === 0 && <p>Aucune trilogie trouvée 😕</p>}
{filtrees.map((trilo) => (
<Carte key={trilo.id} nom={trilo.nom} createur={trilo.createur} difficulte={trilo.difficulte} />
))}
<Footer />
</div>
);
}
🧩 Le champ contrôlé, c'est la boucle : l'état → s'affiche dans le champ (
value) ; une frappe → met à jour l'état (onChange) ; l'état change → React réaffiche, donc la liste filtrée se recalcule. Tu reconnais ta recherche en JS pur, version React (et React refait l'affichage tout seul, plus deinnerHTML).
⚠️ Détail : on filtre avant le
.map(), et on prévoit le cas « aucun résultat » (l'état vide). Un bon réflexe.
✅ Test : tape « blood » → seules les trilogies correspondantes restent ; efface → tout revient ; tape n'importe quoi → message « aucune trilogie ».
⬆️ Module E — Faire remonter l'état (lifting state up)
Problème : et si tu veux un total de likes affiché en haut, qui additionne toutes les cartes ? Chaque carte a son like dans son coin, App ne les « voit » pas.
La solution React : remonter l'état dans le parent (App) et le redescendre aux enfants. L'enfant prévient le parent via une fonction passée en prop (un callback).
function App() {
const [total, setTotal] = useState(0);
return (
<div className="body">
<p>👍 Total de likes : {total}</p>
{trilogies.map((trilo) => (
<Carte
key={trilo.id}
nom={trilo.nom}
onLike={() => setTotal((t) => t + 1)} // 👈 on passe une FONCTION en prop
/>
))}
</div>
);
}
function Carte(props) {
return (
<div className="carte">
<h2>{props.nom}</h2>
<button onClick={props.onLike}>❤️ J'aime</button> {/* l'enfant appelle le callback du parent */}
</div>
);
}
🧩 L'idée clé de React : l'état « vit » au plus haut niveau qui en a besoin. Les données descendent en props ; les événements remontent via des fonctions en props. Ici, chaque carte ne sait pas compter le total — elle dit juste « on m'a likée » au parent, qui, lui, tient le total.
🔎
setTotal((t) => t + 1)(avec une fonction) est la façon sûre d'incrémenter quand plusieurs mises à jour peuvent s'enchaîner. Prends l'habitude.
✅ Test : liker n'importe quelle carte fait monter le total global en haut.
🔁 Module F — useEffect : agir « à côté » de l'affichage
useEffect sert à exécuter du code après l'affichage : changer le titre de l'onglet, lire/écrire dans localStorage, lancer une requête… Des choses qui ne sont pas « du JSX ».
F.1 — le titre de l'onglet
import { useEffect } from "react";
useEffect(() => {
document.title = `${trilogies.length} trilogies cultes`;
}, []); // 👈 tableau vide = "une seule fois, au chargement"
F.2 — des likes qui survivent au rechargement (localStorage + useEffect)
Dans Carte :
function Carte(props) {
// valeur de départ : ce qui est stocké, sinon 0
const [likes, setLikes] = useState(() => Number(localStorage.getItem("likes-" + props.id)) || 0);
// à chaque fois que "likes" change, on le sauvegarde
useEffect(() => {
localStorage.setItem("likes-" + props.id, likes);
}, [likes]); // 👈 "rejoue cet effet quand likes change"
return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
}
🧩 À comprendre : le tableau de dépendances (
[]ou[likes]) dit quand rejouer l'effet :[]= une seule fois ;[likes]= à chaque changement delikes. C'est le cœur deuseEffect. Et tu retrouves tonlocalStorage(texte →Number(...)pour reconvertir, commeparseIntavant).
✅ Test : like une carte, recharge la page (F5) → le nombre est toujours là. Et l'onglet affiche ton titre.
🌐 Module G — Charger les données avec fetch (le vrai truc des apps)
Au lieu d'écrire trilogies en dur, charge-les depuis un fichier JSON — comme une vraie app va chercher ses données sur un serveur.
- Crée
public/trilogies.json(le dossierpublic/est servi à la racine) avec ton tableau de trilogies (format JSON : guillemets doubles partout). - Dans
App:
import { useState, useEffect } from "react";
function App() {
const [trilogies, setTrilogies] = useState([]); // au départ : liste vide
const [chargement, setChargement] = useState(true);
useEffect(() => {
fetch("/trilogies.json")
.then((r) => r.json())
.then((data) => { setTrilogies(data); setChargement(false); });
}, []); // une seule fois, au chargement
if (chargement) return <p>Chargement…</p>; // tant que les données ne sont pas là
return (
<div className="body">
{trilogies.map((t) => <Carte key={t.id} nom={t.nom} /* ... */ />)}
</div>
);
}
🧩 Le schéma à retenir : un
useStatevide pour accueillir les données → unuseEffect([])quifetchau chargement →setTrilogies(data)remplit l'état → React réaffiche avec les données. Et on gère l'état « en cours de chargement » (très pro).⚠️ Comme en JS pur :
fetcha besoin du serveur de dev (npm run devte le donne déjà). Lepublic/est servi à la racine, donc/trilogies.json.
✅ Test : tes cartes s'affichent à partir du JSON. Ajoute une trilogie dans le fichier → elle apparaît.
🧹 Module H — Re-découper proprement
Ton App commence à beaucoup faire (recherche + total + liste…). Garde-le assembleur : extrais des composants, un par fichier.
components/BarreRecherche.jsx— l'input contrôlé (reçoitvaleuretonChangeen props).components/Liste.jsx— reçoit le tableau filtré et fait le.map().components/Badge.jsx— déjà fait au module B.
🧭 La règle d'or n'a pas changé : 1 composant = 1 fichier,
Appassemble. Dès qu'un fichier « fait trop de choses », découpe.
✅ Checklist
- A — tableau +
.map()+key - B — affichage conditionnel + composant
<Badge> - C — like local par carte
- D — recherche avec champ contrôlé + message « aucun résultat »
- E — total de likes remonté dans
App(callback en prop) - F —
useEffect: titre d'onglet + likes persistés enlocalStorage - G — données chargées via
fetchdepuispublic/trilogies.json - H — re-découpage (
<BarreRecherche>,<Liste>)
🚀 Encore plus loin
- Un tri (par difficulté, par likes) avec un
<select>contrôlé. - Une image par trilogie + affichage conditionnel si l'image existe.
- Un mode sombre global (état dans
App, classe sur le<div>racine).
🧠 Les nouveaux concepts de cet énoncé
Listes (map/key) · affichage conditionnel · état local · champ contrôlé · lifting state up + callbacks · useEffect + dépendances · fetch en React + état de chargement · re-découpage.
Bloqué ? Note ce que tu as tapé + le message exact (console F12). On débloque.