Enonce React #2 contact (Zelda): nettoyage template + fix Docs.jsx, map/key, conditionnel, etat local, champ controle, lifting state up, useEffect, fetch en React, re-decoupage

This commit is contained in:
2026-06-24 09:24:17 +02:00
parent 9b91627ed4
commit bdd8316838
+323
View File
@@ -0,0 +1,323 @@
# 🗡️ É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 les `import ... from "../assets/..."` en haut de `Docs.jsx`, soit (plus simple ici) tu **supprimes `Docs.jsx`** puisqu'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()`)
```jsx
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
```jsx
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 (utilise `id`). 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 de `innerHTML` ni `createElement` à la main).
---
## 🎖️ Module B — Affichage conditionnel + un composant `<Badge>`
Enrichis `Carte` :
```jsx
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 :
```jsx
{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 du `return` : on utilise `&&` (afficher ou rien) ou `? :` (A ou B).
**Exercice bonus — extraire `<Badge>`** (1 composant = 1 fichier) :
```jsx
// 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 de `props.note`). Et `return 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` :
```jsx
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é »).
```jsx
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.
```jsx
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
```jsx
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` :
```jsx
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 de `likes`. C'est le cœur de `useEffect`.
> Et tu retrouves ton `localStorage` (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.
1. Crée `public/zelda.json` (le dossier `public/` est servi à la racine), format JSON (guillemets doubles partout).
2. Dans `App` :
```jsx
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 `useState` vide → un `useEffect([])` qui `fetch` au chargement → `setJeux(data)` remplit l'état → React réaffiche. Et on gère l'état **« en cours de chargement »**.
> ⚠️ Comme en JS pur : `fetch` a besoin du serveur (`npm run dev` te le donne). `public/` est servi à la racine → `/zelda.json`. (Tu te souviens de l'erreur CORS en `file://` ? Ici `npm run dev` est 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çoit `valeur` + `onChange` en 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**, `App` assemble. 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.jsx` ré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 en `localStorage`
- [ ] G — données chargées via `fetch` depuis `public/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.