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:
@@ -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.
|
||||
Reference in New Issue
Block a user