Files
stagiaire/ENONCE_react_services.md
T

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 (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 :

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 du return : 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'é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 :

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 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).

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 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 :
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.