Stephen Build ← Roadmap
0/0
Roadmap des Builders · Étape 6 sur 7

React le framework

React, c'est une façon de construire des interfaces avec des composants : des petits blocs réutilisables qui se mettent à jour tout seuls quand les données changent. À la fin de ce tuto, t'auras refait ton appli météo en React, et elle sera en ligne.

Durée 3 à 4 semaines Niveau JavaScript et Git acquis Projet ta météo en React Mis à jour

À la fin, tu sauras

Les missions

Fais-les dans l'ordre, dans le même projet meteo-react. En parallèle, fais le tutoriel officiel de react.dev (un morpion, en anglais). Tape le code au lieu de le copier, et coche chaque mission quand ça marche.

Mission 0

À l'étape 4, tu changeais la page à la main avec querySelector. Avec React, tu décris à quoi la page doit ressembler selon tes données, et React la met à jour tout seul. Pour ça, il te faut Node.js : télécharge la version LTS sur nodejs.org et installe-la.

Ensuite, dans un terminal (VS Code : Terminal → Nouveau terminal) :

Terminal
$ node -v
$ npm create vite@latest meteo-react -- --template react
$ cd meteo-react
$ npm install
$ npm run dev
  • node -v : doit afficher un numéro de version. Le $ ne se tape pas.
  • npm create vite : crée le projet. Si on te pose une question, garde le choix par défaut.
  • npm install : télécharge React dans node_modules.
  • npm run dev : lance un serveur local. Ctrl + C pour l'arrêter.

Ouvre meteo-react dans VS Code. Tu vas travailler dans src/App.jsx. src/main.jsx l'affiche dans la <div id="root"> de index.html : tu n'y toucheras presque jamais.

Erreur fréquente

« node n'est pas reconnu » ? Ferme complètement le terminal et VS Code, puis rouvre-les. « Missing script: dev » ? T'as oublié le cd meteo-react.

Résultat attendu
Terminal
$ node -v
v22.20.0
$ npm run dev

  VITE v7.1.7  ready in 312 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Sur l'adresse Local, la page d'accueil de Vite avec les logos et un bouton compteur. Tes numéros de version peuvent être différents.

Mission 1

Un composant React, c'est une fonction qui renvoie ce qu'il faut afficher, en JSX. Vide src/App.jsx et écris :

src/App.jsx
export default function App() {
  const prenom = 'Alex';
  const ville = 'Lyon';
  const temperature = 18;

  return (
    <main className="app">
      <h1>Salut {prenom} !</h1>
      <p>Il fait {temperature} °C à {ville}.</p>
      <p>{temperature > 20 ? 'Sors en t-shirt' : 'Prends un pull'}</p>
    </main>
  );
}
  • Un seul parent dans le return. Sinon, un fragment <>...</>.
  • className au lieu de class, htmlFor au lieu de for.
  • { } : une expression JavaScript (variable, calcul, ? :). Pas de if ni de for.
  • Toutes les balises se ferment : <img />, <input />.
Erreur fréquente

« Adjacent JSX elements must be wrapped in an enclosing tag » : tu renvoies deux éléments côte à côte. Entoure-les d'un parent.

Résultat attendu
localhost:5173

18 n'est pas plus grand que 20, donc « Prends un pull ». Le fond sombre ou clair vient du index.css de Vite.

Mission 2

Un composant est un morceau d'interface réutilisable, et son nom commence toujours par une majuscule. Les props, ce sont les infos que tu lui passes, comme des attributs HTML.

src/App.jsx
function CityCard({ name, temperature }) {
  return (
    <article className="card">
      <h2>{name}</h2>
      <p>{temperature} °C</p>
    </article>
  );
}

export default function App() {
  return (
    <main className="app">
      <h1>Ma météo</h1>
      <CityCard name="Paris" temperature={16} />
      <CityCard name="Lyon" temperature={18} />
      <CityCard name="Marseille" temperature={22} />
    </main>
  );
}
  • { name, temperature } : on récupère directement les props.
  • Un texte entre guillemets, un nombre ou une variable entre accolades : temperature={16}.
  • Les props vont du parent vers l'enfant, et l'enfant ne les modifie jamais.
À toi

Ajoute une prop wind et affiche le vent sous la température. Puis stylise .card dans src/index.css.

Résultat attendu
localhost:5173

Trois cartes, une par <CityCard />. Pas encore de style : c'est pour « À toi ».

Mission 3

Si tu changes une variable normale, React ne le sait pas. Pour une donnée qui change à l'écran, tu utilises le state, avec useState.

src/App.jsx
import { useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <main className="app">
      <p>Tu as cliqué {count} fois.</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(0)}>Remettre à zéro</button>
    </main>
  );
}
  • [count, setCount] : la valeur, et la fonction pour la changer. Jamais count = 5.
  • setCount relance App avec la nouvelle valeur : c'est un re-render.
  • onClick, onChange : les événements, en camelCase, sur la balise.

Pour un formulaire, le state garde ce que tape l'utilisateur. C'est un champ contrôlé :

src/App.jsx
import { useState } from 'react';

export default function App() {
  const [city, setCity] = useState('');

  return (
    <main className="app">
      <label htmlFor="city">Ta ville</label>
      <input
        id="city"
        value={city}
        onChange={(e) => setCity(e.target.value)}
      />
      <p>Tu cherches : {city || 'rien pour l’instant'}</p>
    </main>
  );
}
Erreur fréquente

onClick={setCount(count + 1)} sans la flèche appelle la fonction pendant l'affichage : « Too many re-renders ». Écris onClick={() => setCount(count + 1)}.

Résultat attendu
localhost:5173
localhost:5173

En haut, le compteur après 3 clics. En bas, le champ après avoir tapé « Lyon » : le texte suit chaque lettre.

Mission 4

Pas de boucle for dans le JSX : tu transformes le tableau en éléments avec map.

src/App.jsx
import { useState } from 'react';

const CITIES = [
  { id: 'paris', name: 'Paris', lat: 48.85, lon: 2.35 },
  { id: 'lyon', name: 'Lyon', lat: 45.76, lon: 4.84 },
  { id: 'marseille', name: 'Marseille', lat: 43.3, lon: 5.37 },
  { id: 'lille', name: 'Lille', lat: 50.63, lon: 3.06 },
];

export default function App() {
  const [selected, setSelected] = useState(null);

  return (
    <main className="app">
      <h1>Choisis une ville</h1>
      <ul>
        {CITIES.map((city) => (
          <li key={city.id}>
            <button onClick={() => setSelected(city)}>{city.name}</button>
          </li>
        ))}
      </ul>

      {selected === null ? (
        <p>Aucune ville choisie.</p>
      ) : (
        <p>Tu as choisi {selected.name} ({selected.lat}, {selected.lon}).</p>
      )}

      {selected && <button onClick={() => setSelected(null)}>Effacer</button>}
    </main>
  );
}
  • map renvoie un <li> par ville.
  • key={city.id} : un identifiant unique et stable, pour que React sache quel élément a changé. Évite l'index du tableau.
  • condition ? A : B : le if/else du JSX.
  • condition && A : affiche A seulement si c'est vrai.
À toi

Ajoute un champ de recherche contrôlé au-dessus de la liste et n'affiche que les villes qui contiennent le texte tapé (filter avant map). Sinon, « Aucun résultat ».

Résultat attendu
localhost:5173

Après un clic sur « Lyon ». Au départ, « Aucune ville choisie. » et pas de bouton « Effacer ». Aucun avertissement « key » dans la console.

Mission 5

Appeler une API, c'est un effet : on le met dans useEffect, qui s'exécute après l'affichage. On reprend l'API Open-Meteo de l'étape 4.

src/App.jsx
import { useState, useEffect } from 'react';

const CITIES = [
  { id: 'paris', name: 'Paris', lat: 48.85, lon: 2.35 },
  { id: 'lyon', name: 'Lyon', lat: 45.76, lon: 4.84 },
  { id: 'marseille', name: 'Marseille', lat: 43.3, lon: 5.37 },
];

export default function App() {
  const [city, setCity] = useState(CITIES[0]);
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;

    async function load() {
      setLoading(true);
      setError(null);
      try {
        const url = `https://api.open-meteo.com/v1/forecast?latitude=${city.lat}&longitude=${city.lon}&current=temperature_2m,wind_speed_10m`;
        const res = await fetch(url);
        if (!res.ok) throw new Error('Erreur ' + res.status);
        const data = await res.json();
        if (!ignore) setWeather(data.current);
      } catch (err) {
        if (!ignore) setError('Impossible de charger la météo.');
      } finally {
        if (!ignore) setLoading(false);
      }
    }

    load();
    return () => {
      ignore = true;
    };
  }, [city]);

  return (
    <main className="app">
      <h1>Météo à {city.name}</h1>
      {CITIES.map((c) => (
        <button key={c.id} onClick={() => setCity(c)}>{c.name}</button>
      ))}

      {loading && <p>Chargement…</p>}
      {error && <p className="error">{error}</p>}
      {!loading && !error && weather && (
        <p>{weather.temperature_2m} °C, vent {weather.wind_speed_10m} km/h</p>
      )}
    </main>
  );
}
  • Trois states pour une requête : weather, loading, error.
  • [city] : le tableau de dépendances. L'effet tourne au départ, puis à chaque changement de ville.
  • L'effet ne peut pas être async lui-même : on crée load() dedans.
  • return () => { ignore = true; } : le nettoyage. Une réponse lente n'écrase pas la bonne.
Demande à l'IA
Explique-moi le tableau de dépendances de useEffect avec trois cas : pas de tableau, un tableau vide, et [city]. Pour chacun, dis-moi quand l'effet s'exécute. Ne m'écris pas de composant complet.
Résultat attendu
localhost:5173
localhost:5173

D'abord « Chargement… », puis la vraie météo (tes chiffres seront différents). Clique sur « Lyon » : le titre change et ça recharge.

Mission 6

Ton App.jsx devient long. On met chaque morceau d'interface dans son fichier, dans un dossier src/components.

src/components/SearchForm.jsx
export default function SearchForm({ cities, value, onChange }) {
  return (
    <form className="search" onSubmit={(e) => e.preventDefault()}>
      <label htmlFor="city">Ville</label>
      <select
        id="city"
        value={value}
        onChange={(e) => onChange(e.target.value)}
      >
        {cities.map((c) => (
          <option key={c.id} value={c.id}>{c.name}</option>
        ))}
      </select>
    </form>
  );
}
src/components/WeatherCard.jsx
export default function WeatherCard({ cityName, weather }) {
  return (
    <article className="card">
      <h2>{cityName}</h2>
      <p className="temp">{weather.temperature_2m} °C</p>
      <p>Vent : {weather.wind_speed_10m} km/h</p>
    </article>
  );
}
src/App.jsx
import { useState, useEffect } from 'react';
import SearchForm from './components/SearchForm.jsx';
import WeatherCard from './components/WeatherCard.jsx';

const CITIES = [
  { id: 'paris', name: 'Paris', lat: 48.85, lon: 2.35 },
  { id: 'lyon', name: 'Lyon', lat: 45.76, lon: 4.84 },
  { id: 'marseille', name: 'Marseille', lat: 43.3, lon: 5.37 },
];

export default function App() {
  const [cityId, setCityId] = useState('paris');
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const city = CITIES.find((c) => c.id === cityId);

  useEffect(() => {
    // Le même effet qu'à la mission 5, avec city.lat et city.lon
  }, [city]);

  return (
    <main className="app">
      <h1>Ma météo</h1>
      <SearchForm cities={CITIES} value={cityId} onChange={setCityId} />
      {loading && <p>Chargement…</p>}
      {error && <p className="error">{error}</p>}
      {!loading && !error && weather && (
        <WeatherCard cityName={city.name} weather={weather} />
      )}
    </main>
  );
}
  • Chaque fichier fait export default, et App l'importe.
  • Le state est remonté dans App : SearchForm affiche value et prévient le parent avec onChange.
  • Les données descendent par les props, les événements remontent par des fonctions.
À toi

Remplace le commentaire dans useEffect par ton code de la mission 5, sans le recopier. Puis installe l'extension React Developer Tools pour voir tes composants dans F12.

Résultat attendu
localhost:5173

Un menu déroulant à la place des boutons, la météo dans une carte. Choisis « Marseille » : la carte se met à jour.

Mission 7

Le navigateur ne lit pas le JSX. Vite construit ton projet en HTML, CSS et JS classiques, dans un dossier dist. Teste d'abord chez toi :

Terminal
$ npm run build
$ npm run preview

Ensuite, comme à l'étape 5 :

  1. Pousse ton projet sur GitHub. Le .gitignore de Vite exclut déjà node_modules et dist.
  2. Sur Vercel : Add New → Project, importe ton dépôt.
  3. Vérifie : Build Command npm run build, Output Directory dist. Puis Deploy.
Erreur fréquente

Le build échoue sur Vercel mais pas chez toi ? Souvent, un import avec la mauvaise casse : weatherCard.jsx au lieu de WeatherCard.jsx. Le serveur fait la différence.

Résultat attendu
Terminal
$ npm run build
vite v7.1.7 building for production...
✓ 32 modules transformed.
dist/index.html                   0.46 kB │ gzip:  0.30 kB
dist/assets/index-BxQ3k9tF.css    0.95 kB │ gzip:  0.52 kB
dist/assets/index-Dk7pL2mA.js   196.40 kB │ gzip: 61.87 kB
✓ built in 1.04s
$ npm run preview
  ➜  Local:   http://localhost:4173/

Un dossier dist apparaît. Sur localhost:4173, ton appli marche comme en dev. Après le déploiement, Vercel te donne une adresse en .vercel.app.

Le projet

Ta météo en React, puis un tableau de bord

Refais ton appli météo de l'étape 4 en React, dans un nouveau projet et sans regarder les exemples : sélecteur de ville, carte météo, messages de chargement et d'erreur. Puis transforme-la en tableau de bord : plusieurs villes en même temps, chacune dans sa carte. Indice : un composant CityWeather qui reçoit une ville en prop et charge sa météo avec useEffect, affiché avec map.

Ton projet est validé quand :

Bonus

Laisse l'utilisateur ajouter et retirer des villes du tableau de bord, et garde sa liste dans le localStorage (comme à l'étape 4) pour la retrouver au prochain chargement.

Pour aller plus loin

Étape 6 bouclée ?

Si ton tableau de bord coche tous les critères, tu sais faire du React. La suite logique, c'est Next.js, mais seulement une fois que les composants, le state et les effets sont devenus naturels. Avant ça, passe à l'étape finale : un vrai site, pour un vrai client.

Étape 7 : Le vrai client →