Enonce React #2 services (Geometry Dash): 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:21:10 +02:00
parent af94eafc10
commit b30bae07c2
+307
View File
@@ -0,0 +1,307 @@
# 🎮 É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()`)
```jsx
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
```jsx
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 (utilise `id`, 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` :
```jsx
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 :
```jsx
{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 du `return` : on utilise `&&` (afficher ou rien) ou `? :` (A ou B).
**Exercice bonus — extraire un composant `<Badge>`** (1 composant = 1 fichier) :
```jsx
// 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'écrire `props.difficulte` partout. Et `return 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` :
```jsx
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é »).
```jsx
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 de `innerHTML`).
> ⚠️ 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*).
```jsx
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
```jsx
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` :
```jsx
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 de `likes`. C'est le cœur de `useEffect`.
> Et tu retrouves ton `localStorage` (texte → `Number(...)` pour reconvertir, comme `parseInt` avant).
**✅ 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.
1. Crée `public/trilogies.json` (le dossier `public/` est servi à la racine) avec ton tableau de trilogies (format JSON : guillemets doubles partout).
2. Dans `App` :
```jsx
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 `useState` vide pour accueillir les données → un `useEffect([])` qui `fetch` au 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 : `fetch` a besoin du serveur de dev (`npm run dev` te le donne déjà). Le `public/` 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çoit `valeur` et `onChange` en 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**, `App` assemble. 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 en `localStorage`
- [ ] G — données chargées via `fetch` depuis `public/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.