Files
stagiaire/ENONCE_react_contact.md
T

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

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

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

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