13 KiB
🗡️ Énoncé React #2 — ton site Zelda (version longue)
Bien joué Mélissa : tu as découpé en composants (Header, Footer, Carte, Compteur, et même un début de Docs), tu utilises useState… les bases sont là. 👌
Deux choses avant de foncer : ton App.jsx contient encore tout le contenu d'exemple de Vite (le gros bloc « Documentation / Connect with us ») — c'est le fourre-tout qu'on veut éviter. Et ton Docs.jsx est cassé (on le règle au Module 0). On va nettoyer, puis bâtir un vrai site sur Zelda, en apprenant beaucoup de React.
🎯 Ce document est une série de modules (0 → H). Fais-les dans l'ordre, teste à chaque fois. Je t'explique les mécaniques avec des mini-exemples ; à toi d'appliquer sur ton thème Zelda. Compte plusieurs séances — c'est normal.
| Module | Tu apprends | Niveau |
|---|---|---|
| 0 | Nettoyer le template + réparer Docs.jsx |
⭐ |
| 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 | ⭐⭐ |
| F | useEffect : titre de page + persistance localStorage |
⭐⭐ |
| G | Charger les données avec fetch (dans React) |
⭐⭐⭐ |
| H | Re-découpage propre (<Liste>, <BarreRecherche>) |
⭐⭐ |
🧹 Module 0 — Nettoyer + réparer Docs.jsx
0.1 — Vider le contenu d'exemple
Dans App.jsx, tout le bloc <section id="next-steps">…</section> (Documentation, Connect with us, liens Vite/Discord…) vient du template de démarrage. Supprime-le : on va mettre ton contenu Zelda à la place. Garde juste <Header />, <Footer />, et ce que tu ajouteras.
0.2 — Le bug de Docs.jsx
Ton Docs.jsx utilise viteLogo et reactLogo… mais ne les importe pas dans le fichier. Si tu l'affichais, tu aurais viteLogo is not defined.
🧩 La règle : chaque fichier de composant doit importer ce qu'il utilise. Un composant est isolé : il ne « voit » pas les imports faits dans
App.jsx. Soit tu ajoutes lesimport ... from "../assets/..."en haut deDocs.jsx, soit (plus simple ici) tu supprimesDocs.jsxpuisqu'on enlève cette section de toute façon.
💡 À retenir : un composant qui plante (import manquant, export oublié) fait tomber tout le rendu — tu l'as vu avec l'histoire du Footer. D'où l'intérêt de garder chaque fichier propre et autonome.
✅ Test : la page n'affiche plus que ton Header/Footer, sans erreur dans la console (F12).
🧱 Module A — Afficher une liste : .map() + key
On va lister des jeux Zelda à partir de données, pas à la main.
A.1 — un tableau (au-dessus de function App())
const jeux = [
{ id: 1, titre: "Ocarina of Time", console: "N64", annee: 1998, note: 99 },
{ id: 2, titre: "Breath of the Wild", console: "Switch", annee: 2017, note: 97 },
{ id: 3, titre: "Majora's Mask", console: "N64", annee: 2000, note: 95 },
// ... tes jeux préférés
];
A.2 — générer les cartes
function App() {
return (
<>
<Header />
{jeux.map((jeu) => (
<Carte key={jeu.id} titre={jeu.titre} console={jeu.console} annee={jeu.annee} />
))}
<Footer />
</>
);
}
🧩 Deux nouveautés :
{ ... }dans le JSX = « insère ici du JavaScript ».{jeux.map(...)}insère une carte par jeu.key={...}= identifiant unique par élément (utiliseid). React en a besoin pour suivre chaque carte ; sans lui → warning console.
✅ Test : ajoute un jeu au tableau → une carte apparaît toute seule.
🔎 Tu reconnais ton tableau d'équipe +
.map()/filter()du JS pur ? C'est la même idée, mais React refait l'affichage tout seul (plus deinnerHTMLnicreateElementà la main).
🎖️ Module B — Affichage conditionnel + un composant <Badge>
Enrichis Carte :
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</h2>
<p>{props.console} — {props.annee}</p>
</div>
);
}
Affichage conditionnel — afficher un truc seulement si une condition est vraie :
{props.note >= 97 && <span className="badge">⭐ Chef-d'œuvre</span>}
condition && <...>= « si vrai, affiche, sinon rien ».- choix entre deux :
{props.console === "Switch" ? "🟢 récent" : "🕹️ rétro"}.
💡 Dans le JSX, pas de
ifà l'intérieur dureturn: on utilise&&(afficher ou rien) ou? :(A ou B).
Exercice bonus — extraire <Badge> (1 composant = 1 fichier) :
// components/Badge.jsx
function Badge({ note }) {
if (note < 97) return null; // un composant peut renvoyer "rien"
return <span className="badge">⭐ Chef-d'œuvre</span>;
}
export default Badge;
🔎 Nouveau :
function Badge({ note })déballe directement la prop (au lieu deprops.note). Etreturn null= « n'affiche rien ».
✅ Test : seuls les jeux notés ≥ 97 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.titre}</h2>
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
</div>
);
}
🧩 Chaque
<Carte />est une instance indépendante : liker un jeu ne touche pas les autres.
✅ Test : le ❤️ d'une carte monte sans bouger les autres.
🔎 Module D — Champ contrôlé + recherche
Nouveau concept : un champ dont la valeur est pilotée par un useState (« contrôlé »).
function App() {
const [recherche, setRecherche] = useState("");
const filtres = jeux.filter((j) =>
j.titre.toLowerCase().includes(recherche.toLowerCase())
);
return (
<>
<Header />
<input
type="search"
placeholder="Rechercher un jeu…"
value={recherche} // l'input AFFICHE l'état
onChange={(e) => setRecherche(e.target.value)} // toute frappe MET À JOUR l'état
/>
{filtres.length === 0 && <p>Aucun jeu trouvé 😕</p>}
{filtres.map((jeu) => (
<Carte key={jeu.id} titre={jeu.titre} console={jeu.console} annee={jeu.annee} />
))}
<Footer />
</>
);
}
🧩 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 → la liste filtrée se recalcule. Exactement ta recherche du JS, version React.
⚠️ On filtre avant le
.map(), et on gère le cas « aucun résultat » (l'état vide).
✅ Test : tape « ocarina » → seul ce jeu reste ; efface → tout revient ; tape n'importe quoi → message « aucun jeu ».
⬆️ Module E — Faire remonter l'état (lifting state up)
Problème : 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.
Solution React : remonter l'état dans le parent (App) et redescendre une fonction (callback) aux enfants.
function App() {
const [total, setTotal] = useState(0);
return (
<>
<p>👍 Total de likes : {total}</p>
{jeux.map((jeu) => (
<Carte
key={jeu.id}
titre={jeu.titre}
onLike={() => setTotal((t) => t + 1)} // 👈 une FONCTION passée en prop
/>
))}
</>
);
}
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</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. La carte ne sait pas compter le total — elle dit juste « on m'a likée » au parent.
🔎
setTotal((t) => t + 1)(forme avec une fonction) est la façon sûre d'incrémenter. 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 exécute du code après l'affichage : titre de l'onglet, localStorage, requêtes…
F.1 — le titre de l'onglet
import { useEffect } from "react";
useEffect(() => {
document.title = `${jeux.length} jeux Zelda`;
}, []); // 👈 tableau vide = "une seule fois, au chargement"
F.2 — des likes qui survivent au rechargement
Dans Carte :
function Carte(props) {
const [likes, setLikes] = useState(() => Number(localStorage.getItem("likes-" + props.id)) || 0);
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 dit quand rejouer l'effet :
[]= une fois ;[likes]= à chaque changement delikes. C'est le cœur deuseEffect. Et tu retrouves tonlocalStorage(texte →Number(...)pour reconvertir).
✅ Test : like un jeu, recharge (F5) → le nombre est toujours là. L'onglet affiche ton titre.
🌐 Module G — Charger les données avec fetch (dans React)
Au lieu d'écrire jeux en dur, charge-les depuis un JSON — comme une vraie app.
- Crée
public/zelda.json(le dossierpublic/est servi à la racine), format JSON (guillemets doubles partout). - Dans
App:
import { useState, useEffect } from "react";
function App() {
const [jeux, setJeux] = useState([]); // au départ : liste vide
const [chargement, setChargement] = useState(true);
useEffect(() => {
fetch("/zelda.json")
.then((r) => r.json())
.then((data) => { setJeux(data); setChargement(false); });
}, []); // une fois, au chargement
if (chargement) return <p>Chargement…</p>;
return (
<>
{jeux.map((j) => <Carte key={j.id} titre={j.titre} /* ... */ />)}
</>
);
}
🧩 Le schéma : un
useStatevide → unuseEffect([])quifetchau chargement →setJeux(data)remplit l'état → React réaffiche. Et on gère l'état « en cours de chargement ». ⚠️ Comme en JS pur :fetcha besoin du serveur (npm run devte le donne).public/est servi à la racine →/zelda.json. (Tu te souviens de l'erreur CORS enfile://? Icinpm run devest déjà un vrai serveur, donc pas de souci.)
✅ Test : tes cartes viennent du JSON ; ajoute un jeu dans le fichier → il apparaît.
🧹 Module H — Re-découper proprement
App commence à beaucoup faire (recherche + total + liste…). Garde-le assembleur, extrais des composants (un par fichier) :
components/BarreRecherche.jsx— l'input contrôlé (reçoitvaleur+onChangeen props).components/Liste.jsx— reçoit le tableau filtré et fait le.map().components/Badge.jsx— déjà fait au Module B.
🧭 Règle d'or : 1 composant = 1 fichier,
Appassemble. Dès qu'un fichier « fait trop », découpe. (C'est exactement le ménage du Module 0, mais en bien.)
✅ Checklist
- 0 — template nettoyé +
Docs.jsxréparé/supprimé - 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/zelda.json - H — re-découpage (
<BarreRecherche>,<Liste>)
🚀 Encore plus loin
- Un tri (par année, par note) avec un
<select>contrôlé. - Une image par jeu (
<img src={props.image} />) + affichage conditionnel si elle 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ée ? Note ce que tu as tapé + le message exact (console F12). On débloque.