skilling.maSkill. Scale. Soar.
Partie 0 — Introduction ES6+ JavaScript moderne
⏱ 10 min

JavaScript ES6+ — pourquoi cette révision est indispensable avant React

React est une bibliothèque JavaScript. Pas un langage à part, pas un framework magique — du JavaScript avec une couche par-dessus. Avant d'écrire le moindre composant, il faut comprendre le JavaScript moderne qu'utilise React en permanence. Cette partie est un rappel ciblé, pas un cours complet — chaque concept est présenté directement à travers le prisme de React et du projet DariGo.


1
Qu'est-ce qu'ES6 et pourquoi ça compte ?
Historique · ECMAScript · ES2015+ · Transpilation

JavaScript est standardisé sous le nom ECMAScript (ES). L'organisme ECMA International publie une nouvelle version du standard chaque année depuis 2015. ES6 — aussi appelé ES2015 — est la version qui a tout changé : elle a introduit en une seule fois des dizaines de nouvelles fonctionnalités qui ont modernisé radicalement la façon d'écrire du JavaScript.

Avant ES6, écrire une application front-end complexe en JavaScript pur était laborieux : pas de modules natifs, pas de classes, pas de syntaxe concise pour les fonctions, pas de gestion propre de l'asynchrone. Les développeurs jonglaient avec des workarounds, des patterns verbeux et des bibliothèques tierces pour combler les lacunes du langage. ES6 a apporté une solution native à presque tous ces problèmes.

Depuis ES2015, les versions annuelles ajoutent des améliorations incrémentales : ES2017 a introduit async/await, ES2018 a ajouté le spread sur les objets, ES2020 a apporté l'optional chaining et le nullish coalescing. Quand on parle de "JavaScript moderne" ou de "ES6+" dans le contexte React, on désigne l'ensemble de ces fonctionnalités disponibles à partir de 2015.

Les navigateurs n'implémentent pas toutes ces fonctionnalités au même rythme. C'est le rôle de Babel, le transpileur intégré à Vite et Create React App : il convertit le code ES6+ en ES5 compatible avec tous les navigateurs cibles. En tant que développeur React, vous écrivez en ES6+ et laissez les outils de build gérer la compatibilité.

VersionAnnéeApports principaux utilisés en React
ES6 / ES20152015let, const, arrow functions, classes, modules, destructuring, spread, template literals, Promise
ES20172017async/await
ES20182018Spread/rest sur les objets, Promise.finally()
ES20202020Optional chaining ?., nullish coalescing ??, Promise.allSettled()
ES2021+2021+Promise.any(), logical assignment operators

2
Ce que React utilise en permanence — vue d'ensemble
Mapping · Composants · Hooks · Services

Voici un composant React minimal complet pour l'application DariGo. Chaque ligne en surbrillance est une fonctionnalité ES6+ que vous allez apprendre dans cette partie. Lisez ce code maintenant — vous ne comprendrez peut-être pas tout, et c'est normal. Revenez-y après avoir terminé la Partie 0 : tout sera clair.

javascript
// ── Import ES6 (section 05) ───────────────────────────────────
import React, { useState, useEffect } from 'react';
import { platsService } from '../services/platsService';
import CartePlat from './CartePlat';

// ── Composant React — arrow function + destructuring des props (sections 01, 02)
const MenuDariGo = ({ ville = "Casablanca", onCommanderPlat }) => {

  // ── useState — destructuring de tableau (sections 02, hook React)
  const [plats, setPlats]               = useState([]);
  const [recherche, setRecherche]       = useState("");
  const [estEnChargement, setChargement]= useState(true);
  const [erreur, setErreur]             = useState(null);

  // ── useEffect + async/await + try/catch (sections 07, 09)
  useEffect(() => {
    const charger = async () => {
      try {
        const data = await platsService.getParVille(ville);  // Fetch API (section 09)
        setPlats(data);
      } catch (err) {
        setErreur(err.message);
      } finally {
        setChargement(false);
      }
    };
    charger();
  }, [ville]);  // tableau de dépendances

  // ── filter() + template literal + includes (sections 04, 06)
  const platsFiltres = plats
    .filter(p => p.disponible)
    .filter(p => p.nom.toLowerCase().includes(recherche.toLowerCase()));

  // ── reduce() — calculer des stats (section 06)
  const prixMoyen = platsFiltres.length
    ? platsFiltres.reduce((acc, p) => acc + p.prix, 0) / platsFiltres.length
    : 0;

  // ── Arrow function — handler d'événement (section 01)
  const ajouterAuPanier = (plat) => {
    // Spread — créer un nouvel objet sans muter (section 03)
    const platAvecDate = { ...plat, dateAjout: new Date().toISOString() };
    onCommanderPlat(platAvecDate);
  };

  if (estEnChargement) return 
Chargement du menu...
; if (erreur) return
{erreur}
; return (
{/* Template literal — classe CSS conditionnelle (section 04) */}

Menu DariGo — {ville}

Prix moyen : {prixMoyen.toFixed(0)} MAD

setRecherche(e.target.value)} /> {/* map() — rendre une liste de composants (section 06) */}
{platsFiltres.map(plat => ( // Spread des props — rest/spread (section 03) ajouterAuPanier(plat)} /> ))}
{platsFiltres.length === 0 && (

Aucun plat disponible pour "{recherche}" à {ville}

)}
); }; // ── Export par défaut (section 05) export default MenuDariGo;
ℹ Ce que couvre cette partie 0
  • Section 00 — let / const : portée de bloc, différences avec var, immutabilité de référence
  • Section 01 — Arrow functions : syntaxe, retour implicite, this lexical
  • Section 02 — Destructuring : objets, tableaux, imbriqué, paramètres de fonction
  • Section 03 — Spread & Rest : copier, fusionner, immutabilité React, ...props
  • Section 04 — Template literals : interpolation, multiligne, classes CSS dynamiques
  • Section 05 — Modules ES6 : import, export, nommé vs défaut, barrel
  • Section 06 — map(), filter(), reduce() : le trio de React
  • Section 07 — Promises & async/await : asynchrone, useEffect, AbortController
  • Section 08 — Classes ES6 : syntaxe, héritage, composants de classe legacy
  • Section 09 — Fetch API : CRUD, headers JWT, couche de service professionnelle
Partie 0 — Rappel JavaScript ES6 let const var
⏱ 15 min

let et const — portée de bloc

Avant d'écrire une seule ligne de React, il faut maîtriser la façon dont JavaScript ES6 gère les variables. var, let et const ne se comportent pas de la même façon — et cette différence provoque des bugs réels dans les applications React, souvent invisibles et difficiles à diagnostiquer.

ℹ Prérequis
  • Savoir ce qu'est une variable en JavaScript
  • Comprendre le concept de fonction
✓ Ce que vous allez apprendre
  • Pourquoi var est dangereux et abandonné en React
  • La portée de bloc (block scope) de let et comment elle évite les bugs
  • L'immutabilité de référence de const et la différence avec l'immutabilité de valeur
  • Le hoisting — pourquoi JavaScript "remonte" les déclarations et ce que ça implique
  • La règle d'or React : const par défaut, let uniquement quand nécessaire

1
Le problème de var — pourquoi il a été abandonné
Portée fonctionnelle · Hoisting · Bugs silencieux · TDZ

Pendant longtemps, var était le seul moyen de déclarer une variable en JavaScript. Le problème : var a une portée fonctionnelle (function scope), pas une portée de bloc. Concrètement, une variable var déclarée à l'intérieur d'un if, d'une boucle for, ou de n'importe quel bloc { } est visible dans toute la fonction parente — pas seulement dans ce bloc.

Ce comportement est contre-intuitif et source de bugs difficiles à repérer. Imaginez que vous travaillez sur le composant panier de DariGo : vous déclarez une variable total dans une condition, pensant qu'elle est locale à ce bloc. Avec var, elle écrase silencieusement la variable total du scope supérieur. Résultat : un montant faux affiché au client — sans aucune erreur JavaScript.

javascript
// ── PROBLÈME 1 : portée fonctionnelle de var ─────────────────
// Scénario : calcul du total d'une commande DariGo
function calculerTotal(panier) {
  var total = 0;

  if (panier.length > 5) {
    var total = panier.reduce((acc, p) => acc + p.prix, 0) * 0.9; // remise 10%
    // On pense créer une variable locale au bloc if...
  }

  return total;
  // MAIS var n'a pas de portée de bloc !
  // Si panier.length > 5, total est la valeur avec remise.
  // Si panier.length <= 5, total vaut toujours 0 car le if n'a pas été exécuté.
  // Dans les deux cas, le comportement est inattendu et trompeur.
}

// ── PROBLÈME 2 : var dans une boucle for ─────────────────────
// Scénario : afficher les plats avec un délai (animation DariGo)
var plats = ["Tajine", "Couscous", "Harira"];

for (var i = 0; i < plats.length; i++) {
  setTimeout(function() {
    console.log(plats[i]); // On s'attend à voir "Tajine", "Couscous", "Harira"
  }, i * 1000);
}
// RÉSULTAT RÉEL : undefined, undefined, undefined
// Explication : var i est partagée par toutes les itérations.
// Quand les callbacks setTimeout s'exécutent (après 0ms, 1s, 2s),
// la boucle est déjà terminée et i vaut 3 (plats[3] = undefined).
// Tous les callbacks partagent la MÊME variable i.

// ── PROBLÈME 3 : hoisting (hissage) ──────────────────────────
// JavaScript "remonte" les déclarations var en haut de la fonction
console.log(villeLivraison); // undefined — pas d'erreur !
var villeLivraison = "Casablanca";
// JavaScript interprète ce code comme :
// var villeLivraison;           ← déclaration remontée
// console.log(villeLivraison);  ← undefined (pas encore assignée)
// villeLivraison = "Casablanca"; ← assignation reste en place
// Ce comportement masque des bugs : utiliser une variable avant
// sa déclaration devrait être une erreur, pas retourner undefined.
⚠ var en React — une source de bugs invisibles

En React, les composants se re-rendent souvent. Si vous utilisez var dans des boucles ou des conditions à l'intérieur d'un composant, les variables peuvent se comporter de façon imprévisible entre les rendus. ESLint (l'outil de qualité de code standard en React) signale automatiquement l'utilisation de var comme une erreur. Ne l'utilisez jamais dans un projet React.


2
let — la portée de bloc, enfin
Block scope · TDZ · Boucles · Re-assignation

Introduit en ES6 (2015), let résout tous les problèmes de var grâce à la portée de bloc (block scope). Une variable let n'existe que dans le bloc { } où elle est déclarée — qu'il s'agisse d'un if, d'une boucle for, d'une fonction ou de simples accolades isolées.

let n'est pas hissé de la même façon que var. Si vous essayez d'accéder à une variable let avant sa déclaration, JavaScript lève une ReferenceError immédiate — un comportement sain qui expose les bugs au lieu de les masquer. La zone entre le début du bloc et la déclaration let s'appelle la Temporal Dead Zone (TDZ).

let autorise la re-assignation — vous pouvez changer la valeur de la variable autant de fois que nécessaire. C'est pour cela qu'on l'utilise pour les compteurs, les index de boucle, et les variables qui changent au fil du temps.

javascript
// ── let résout le problème de portée ─────────────────────────
function calculerTotal(panier) {
  let total = 0; // variable du scope de la fonction

  if (panier.length > 5) {
    let total = panier.reduce((acc, p) => acc + p.prix, 0) * 0.9;
    // Cette variable total est DIFFÉRENTE de celle du dessus.
    // Elle n'existe QUE dans ce bloc if { }.
    console.log("Total avec remise (dans le if) :", total);
  }

  return total; // Toujours 0 — la variable du scope fonction est intacte.
  // Pour corriger la logique, il faudrait réassigner le total extérieur :
}

// Version correcte avec let :
function calculerTotalCorrect(panier) {
  let total = panier.reduce((acc, p) => acc + p.prix, 0);
  if (panier.length > 5) {
    total = total * 0.9; // réassignation du let extérieur — intentionnel et lisible
  }
  return total;
}

// ── let dans les boucles — chaque itération a son propre i ───
let plats = ["Tajine", "Couscous", "Harira"];

for (let i = 0; i < plats.length; i++) {
  setTimeout(function() {
    console.log(plats[i]); // Correct : "Tajine", "Couscous", "Harira"
  }, i * 1000);
}
// Avec let, chaque itération de la boucle crée sa PROPRE copie de i.
// i vaut 0 dans le premier callback, 1 dans le second, 2 dans le troisième.

// ── TDZ — Temporal Dead Zone ─────────────────────────────────
console.log(villeLivraison); // ReferenceError: Cannot access 'villeLivraison' before initialization
let villeLivraison = "Casablanca";
// Contrairement à var, let expose le bug immédiatement.
// C'est le comportement attendu d'un langage sain.

// ── Quand utiliser let ────────────────────────────────────────
let compteurCommandes = 0;
compteurCommandes++;       // let car la valeur change

let statut = "en_attente";
statut = "en_preparation"; // let car réassignation nécessaire
statut = "livree";

for (let i = 0; i < plats.length; i++) {
  // let pour les index de boucle — portée limitée au for
}

3
const — immutabilité de référence
Référence vs valeur · Objets · Tableaux · React

const partage toutes les caractéristiques de let (portée de bloc, TDZ, pas de hoisting), avec une contrainte supplémentaire : la variable ne peut pas être réassignée. Une fois déclarée et initialisée, elle pointe toujours vers la même valeur ou le même objet en mémoire.

C'est ici que beaucoup de débutants font une erreur de compréhension fondamentale : const ne rend pas la valeur immuable — il rend la référence immuable. La différence est cruciale.

Pour les types primitifs (nombre, chaîne, booléen), référence et valeur sont la même chose — const empêche toute modification. Mais pour les objets et les tableaux, const empêche uniquement de pointer la variable vers un nouvel objet ou tableau. Le contenu de cet objet ou tableau peut tout à fait être modifié — les propriétés de l'objet, les éléments du tableau.

En React, const est utilisé dans 90% des cas. Chaque composant est une const. Chaque hook est une const. Chaque URL d'API est une const. Chaque fonction utilitaire est une const. La règle est simple : commencez toujours par const, passez à let seulement si vous avez besoin de réassigner la variable.

javascript
// ── const avec types primitifs ────────────────────────────────
const FRAIS_LIVRAISON = 15;      // en MAD
const VILLE_DEFAUT = "Casablanca";
const TVA = 0.2;

FRAIS_LIVRAISON = 20; // TypeError: Assignment to constant variable
VILLE_DEFAUT = "Rabat"; // TypeError

// ── const avec des objets — référence figée, contenu modifiable ──
const livreur = {
  nom: "Khalid El Amrani",
  ville: "Casablanca",
  vehicule: "moto",
  note: 4.7,
  disponible: true
};

// Modifier une propriété — AUTORISÉ
livreur.disponible = false;          // OK
livreur.ville = "Mohammedia";        // OK
livreur.nombreLivraisons = 312;      // OK — ajouter une propriété

// Réassigner la variable — INTERDIT
livreur = { nom: "Amine" };          // TypeError !
// La variable livreur doit TOUJOURS pointer vers le même objet en mémoire.

// ── const avec des tableaux — même logique ────────────────────
const villesLivraison = ["Casablanca", "Rabat", "Fès", "Marrakech"];

// Modifier le contenu — AUTORISÉ
villesLivraison.push("Agadir");      // OK
villesLivraison[0] = "Mohammedia";   // OK
villesLivraison.pop();               // OK

// Réassigner le tableau — INTERDIT
villesLivraison = ["Tanger"];        // TypeError !

// ── const en React — omniprésent ─────────────────────────────
// Composant React — toujours const
const CartePlat = ({ nom, prix, disponible }) => {
  return (
    <div className="carte-plat">
      <h3>{nom}</h3>
      <span>{prix} MAD</span>
    </div>
  );
};

// Hooks — toujours const (useState retourne un tableau fixe)
const [panier, setPanier] = useState([]);
const [villeLivraison, setVilleLivraison] = useState("Casablanca");

// URL d'API — toujours const (ne change jamais à l'exécution)
const API_URL = "https://api.darigo.ma";
const TIMEOUT_REQUETE = 5000; // 5 secondes

// Fonctions utilitaires — toujours const
const formaterPrix = (prix) => `${prix} MAD`;
const estDisponible = (plat) => plat.stock > 0 && plat.restaurant.ouvert;
Critèrevarletconst
PortéeFonctionnelleBlocBloc
HoistingOui (undefined)Oui (TDZ — erreur)Oui (TDZ — erreur)
RéassignationOuiOuiNon
RedéclarationOuiNonNon
Utilisé en ReactJamaisParfois (compteurs, boucles)Toujours (90% des cas)
⚠ const ne protège pas contre la mutation — problème React

En React, l'état ne doit jamais être muté directement — React compare les références pour détecter les changements. Si vous faites monTableau.push(nouveauItem), la référence ne change pas et React ne détecte pas le changement — le composant ne se re-rend pas. C'est pour cela qu'on utilise le spread operator ([...monTableau, nouveauItem]) qui crée un nouveau tableau avec une nouvelle référence. Nous verrons cela en détail dans la section Spread operator.


✓
Vérification des acquis
5 questions — corrigé intégré
Dans le composant panier de DariGo, on écrit : var total = 0; dans une fonction, puis var total = 250; dans un bloc if à l'intérieur. Que retourne la fonction après le bloc if ?
On utilise let i dans une boucle for qui génère des callbacks setTimeout. Pourquoi chaque callback affiche-t-il la bonne valeur de i, contrairement à var i ?
On déclare const livreur = { nom: "Khalid", ville: "Casablanca" }. Laquelle de ces opérations lève une erreur ?
Qu'est-ce que la Temporal Dead Zone (TDZ) avec let ?
Dans un composant React, on a const [plats, setPlats] = useState([]). On veut ajouter un plat. Pourquoi plats.push(nouveauPlat) ne fonctionne-t-il pas en React ?
Partie 0 — Rappel JavaScript ES6 Arrow Functions this lexical Retour implicite
⏱ 18 min

Arrow functions & this lexical

Les arrow functions sont la syntaxe de fonction utilisée dans 95% du code React. Chaque composant React est une arrow function. Chaque handler d'événement est une arrow function. Chaque callback de map(), filter() est une arrow function. Comprendre leur syntaxe et le comportement de this est non négociable avant d'aborder React.

✓ Ce que vous allez apprendre
  • Toutes les formes syntaxiques — du plus verbeux au plus concis
  • Le retour implicite — quand et comment l'utiliser
  • Pourquoi retourner un objet littéral nécessite des parenthèses
  • Le this lexical — la différence fondamentale avec les fonctions classiques
  • Pourquoi les arrow functions résolvent le problème de this en React

1
Syntaxe — toutes les formes
Paramètres · Corps · Retour implicite · Objet littéral

Une arrow function se construit avec la syntaxe (paramètres) => corps. Le symbole => (fat arrow) remplace le mot-clé function. En fonction du nombre de paramètres et de la complexité du corps, plusieurs niveaux de concision sont possibles.

javascript
// ── FORME 1 : corps avec accolades et return ──────────────────
const calculerLivraison = (distance, tarif) => {
  const frais = distance * tarif;
  return frais;
};

// ── FORME 2 : retour implicite ────────────────────────────────
// Une seule expression → pas d'accolades, pas de return
const calculerLivraison = (distance, tarif) => distance * tarif;
const formaterPrix = (prix) => `${prix} MAD`;
const estDisponible = (plat) => plat.stock > 0 && plat.restaurant.ouvert;

// ── FORME 3 : un seul paramètre ──────────────────────────────
const doubler = n => n * 2;           // parenthèses optionnelles
const formaterPrix = prix => `${prix} MAD`;

// Zéro paramètre → parenthèses OBLIGATOIRES
const saluer = () => "Marhba bik !";
const getTimestamp = () => Date.now();

// Plusieurs paramètres → parenthèses OBLIGATOIRES
const additionner = (a, b) => a + b;

// ── FORME 4 : retourner un objet littéral ─────────────────────
// PIÈGE : {} de l'objet confondu avec {} du corps de la fonction
const creerPlatResume = (plat) => { id: plat.id, nom: plat.nom }; // FAUX → undefined

// SOLUTION : entourer l'objet de parenthèses
const creerPlatResume = (plat) => ({ id: plat.id, nom: plat.nom }); // CORRECT
const creerLivreur = (nom, ville) => ({ nom, ville, disponible: true });

// ── EN REACT ─────────────────────────────────────────────────
// Composant — retour JSX multiligne avec parenthèses (retour implicite)
const CartePlat = ({ nom, prix }) => (
  <div className="carte">
    <h3>{nom}</h3>
    <p>{prix} MAD</p>
  </div>
);

// Handler inline
<button onClick={() => setPanier([...panier, plat])}>Ajouter</button>

// Callbacks de tableau
const noms = plats.map(p => p.nom);
const cartes = plats.map(p => <CartePlat key={p.id} {...p} />);

2
this lexical — la révolution des arrow functions
Contexte d'exécution · bind() · Composants classe · Closures

Le problème avec les fonctions classiques

Dans une fonction classique, this est déterminé dynamiquement au moment de l'appel — sa valeur dépend du contexte d'exécution. Dans un callback setInterval ou un handler d'événement, this ne pointe plus vers l'objet parent — il vaut undefined (strict mode) ou window. Les développeurs devaient utiliser bind(this) ou const self = this comme workarounds.

La solution : this lexical

Dans une arrow function, this est déterminé lexicalement au moment de la définition — il est capturé depuis le contexte parent et ne change jamais. Les arrow functions n'ont pas leur propre this.

javascript
// ── PROBLÈME : fonction classique dans setInterval ───────────
const suiviLivraison = {
  livreur: "Khalid El Amrani",
  demarrerSuivi: function() {
    setInterval(function() {
      // this = window ou undefined — PAS suiviLivraison !
      console.log("Position de " + this.livreur); // undefined ou erreur
    }, 1000);
  }
};

// ── SOLUTION 1 : const self = this (ancien) ───────────────────
const suiviLivraison = {
  livreur: "Khalid El Amrani",
  demarrerSuivi: function() {
    const self = this;
    setInterval(function() {
      console.log("Position de " + self.livreur); // fonctionne mais verbeux
    }, 1000);
  }
};

// ── SOLUTION 2 : arrow function (moderne) ────────────────────
const suiviLivraison = {
  livreur: "Khalid El Amrani",
  demarrerSuivi: function() {
    setInterval(() => {
      // this = suiviLivraison — capturé lexicalement
      console.log("Position de " + this.livreur); // "Position de Khalid El Amrani"
    }, 1000);
  }
};

// ── EN REACT — composant de classe (legacy) ───────────────────
// Avec méthode classique — bind OBLIGATOIRE dans constructor
class PanierDariGo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { nbArticles: 0 };
    this.ajouterArticle = this.ajouterArticle.bind(this); // sans ça : this = undefined
  }
  ajouterArticle() {
    this.setState({ nbArticles: this.state.nbArticles + 1 });
  }
}

// Avec arrow function — pas de bind, this lexical correct
class PanierDariGo extends React.Component {
  state = { nbArticles: 0 };
  ajouterArticle = () => {  // this = instance du composant
    this.setState({ nbArticles: this.state.nbArticles + 1 });
  };
}

// ── COMPOSANT FONCTIONNEL (React moderne) ────────────────────
// Avec hooks — plus aucun problème de this
const PanierDariGo = () => {
  const [nbArticles, setNbArticles] = useState(0);
  const ajouterArticle = () => setNbArticles(nbArticles + 1);
  return <button onClick={ajouterArticle}>{nbArticles} article(s)</button>;
};
CritèreFonction classiqueArrow function
thisDynamique — dépend de l'appelantLexical — capturé à la définition
ConstructeurOui — new MaFonction()Non — TypeError
argumentsOui — objet arguments impliciteNon — utiliser ...args
bind() nécessaireSouvent (callbacks, handlers)Jamais
Usage ReactRare — composants classe legacyOmniprésent

✓
Vérification des acquis
5 questions — corrigé intégré
On écrit const creerCommande = (client, ville) => ({ client, ville, statut: "en_attente" }). Pourquoi l'objet est-il entre parenthèses ?
Dans le composant de suivi DariGo, un setInterval avec une fonction classique ne peut pas accéder à this.livreur. Pourquoi une arrow function résout ce problème ?
Quelle est la forme la plus concise pour function formaterVille(v) { return v.toUpperCase(); } ?
Dans un composant React de classe, pourquoi les handlers d'événements doivent-ils être des arrow functions ou liés avec bind(this) ?
Peut-on écrire const Livreur = (nom) => { this.nom = nom; }; new Livreur("Khalid") ?
Partie 0 — Rappel JavaScript ES6 Destructuring Objets · Tableaux · Imbriqué
⏱ 18 min

Destructuring — objets & tableaux

Le destructuring est une syntaxe ES6 qui permet d'extraire des valeurs d'objets ou de tableaux en une seule instruction. En React, vous l'utiliserez constamment : pour lire les props d'un composant, pour déstructurer le retour de useState, pour extraire des données d'une réponse API, et pour simplifier chaque fonction qui reçoit un objet complexe.

✓ Ce que vous allez apprendre
  • Destructuring d'objets — extraction, renommage, valeur par défaut
  • Destructuring de tableaux — extraction positionnelle, skip, rest
  • Destructuring imbriqué — objets dans des objets
  • Destructuring dans les paramètres de fonctions — pattern React fondamental
  • Cas d'usage React : props, useState, réponses API

1
Destructuring d'objets
Extraction · Renommage · Valeur par défaut · Imbriqué · Props React

Sans destructuring, accéder aux propriétés d'un objet complexe nécessite de répéter le nom de l'objet à chaque accès. Dans un composant React qui reçoit un objet commande avec dix propriétés, écrire commande.client, commande.adresse, commande.statut... partout rend le code verbeux. Le destructuring extrait tout en une ligne.

javascript
const commande = {
  id: 1042,
  client: "Fatima-Zahra Alaoui",
  adresse: "Hay Riad, Avenue Mohammed VI",
  ville: "Rabat",
  total: 145,
  statut: "en_preparation",
  livreur: {
    nom: "Khalid El Amrani",
    telephone: "+212 6 12 34 56 78",
    position: { lat: 33.9716, lng: -6.8498 }
  }
};

// ── Sans destructuring — verbeux ──────────────────────────────
const id = commande.id;
const client = commande.client;
const ville = commande.ville;

// ── Avec destructuring — concis ───────────────────────────────
const { id, client, ville, total } = commande;
console.log(client); // "Fatima-Zahra Alaoui"

// ── Renommer une variable : { propriété: nouveauNom } ─────────
const { client: nomClient, total: montant, ville: villeDestination } = commande;
console.log(nomClient); // "Fatima-Zahra Alaoui"
// Les variables client, total, ville n'existent PAS — seulement les renommées

// ── Valeur par défaut si la propriété est absente ─────────────
const { statut, urgent = false, pourboire = 0 } = commande;
// commande.urgent n'existe pas → urgent = false
// commande.pourboire n'existe pas → pourboire = 0

// Combiner renommage + valeur par défaut
const { statut: etatCommande = "inconnu" } = commande;

// ── Destructuring imbriqué ────────────────────────────────────
const { livreur: { nom: nomLivreur, telephone } } = commande;
console.log(nomLivreur); // "Khalid El Amrani"

const { livreur: { position: { lat, lng } } } = commande;
console.log(lat); // 33.9716

// ── Destructuring dans les paramètres — PATTERN FONDAMENTAL REACT
// Sans destructuring
const CarteCommande = (props) => (
  <div>
    <h3>Commande #{props.id}</h3>
    <p>{props.client} — {props.ville}</p>
    <p>Total : {props.total} MAD</p>
  </div>
);

// Avec destructuring dans les paramètres — propre et lisible
const CarteCommande = ({ id, client, ville, total, statut = "en_attente" }) => (
  <div className={`carte carte-${statut}`}>
    <h3>Commande #{id}</h3>
    <p>{client} — {ville}</p>
    <p>Total : {total} MAD</p>
  </div>
);

2
Destructuring de tableaux
Position · Skip · Rest · useState · Swap

Le destructuring de tableaux fonctionne par position — le nom des variables est libre, c'est la position dans le tableau qui détermine quelle valeur est assignée. C'est exactement la syntaxe que React utilise pour useState : la fonction retourne un tableau de deux éléments, et le destructuring les assigne à deux variables dont vous choisissez librement les noms.

javascript
const villesLivraison = ["Casablanca", "Rabat", "Fès", "Marrakech", "Agadir", "Tanger"];

// ── Extraction positionnelle ──────────────────────────────────
const [v1, v2, v3] = villesLivraison;
// v1 = "Casablanca", v2 = "Rabat", v3 = "Fès"

// Les noms sont LIBRES — position uniquement
const [principale, capitale] = villesLivraison;

// ── Ignorer des éléments — virgule vide ──────────────────────
const [, , troisiemeVille] = villesLivraison;
console.log(troisiemeVille); // "Fès"

const [, , , quatrieme, cinquieme] = villesLivraison;
// "Marrakech", "Agadir"

// ── Capturer le reste avec ...rest ───────────────────────────
const [principale, ...autresVilles] = villesLivraison;
// principale = "Casablanca"
// autresVilles = ["Rabat", "Fès", "Marrakech", "Agadir", "Tanger"]

// ── Valeur par défaut ─────────────────────────────────────────
const [a, b, c, d, e, f, g = "Meknès"] = villesLivraison;
// g = "Meknès" — index 6 n'existe pas

// ── Swap de variables sans variable temporaire ────────────────
let prixMin = 30, prixMax = 150;
[prixMin, prixMax] = [prixMax, prixMin];
// prixMin = 150, prixMax = 30

// ── useState — le cas d'usage le plus important en React ──────
// useState() retourne un tableau [valeurActuelle, fonctionDeMAJ]

// Sans destructuring — illisible
const etatPanier = useState([]);
const panier = etatPanier[0];
const setPanier = etatPanier[1];

// Avec destructuring — idiomatique React
const [panier, setPanier] = useState([]);
const [villeLivraison, setVilleLivraison] = useState("Casablanca");
const [estCharge, setEstCharge] = useState(false);
const [erreur, setErreur] = useState(null);
const [plats, setPlats] = useState([]);

// Le nom est libre — choisissez ce qui est lisible
const [commandeEnCours, mettreAJourCommande] = useState(null);
const [menuVisible, basculerMenu] = useState(false);

✓
Vérification des acquis
5 questions — corrigé intégré
On destructure : const { total: montant = 0, urgent = false } = { total: 145, ville: "Rabat" }. Quelles sont les valeurs de montant et urgent ?
Dans un composant React, on écrit const CartePlat = ({ nom, prix, disponible = true }) => .... Que se passe-t-il si disponible n'est pas passée ?
On a const notes = [18, 14, 12, 16, 9]. Comment extraire la première et la cinquième note en une ligne par destructuring ?
Pourquoi écrit-on const [plats, setPlats] = useState([]) avec des crochets et non des accolades ?
Comment extraire directement le nom du livreur depuis un objet commande avec livreur imbriqué ?
Partie 0 — Rappel JavaScript ES6 Spread ... Rest ... · Immutabilité React
⏱ 18 min

Spread & Rest operator

L'opérateur ... joue deux rôles opposés selon le contexte : en position d'expression, il étale (spread) les éléments d'un tableau ou d'un objet. En position de paramètre ou de destructuring, il regroupe (rest) les éléments restants. La maîtrise du spread est indispensable pour respecter l'immutabilité en React — règle d'or de la gestion d'état.


1
Spread sur les tableaux
Copier · Fusionner · Insérer · Arguments · Immutabilité React

Le spread sur un tableau étale ses éléments. [...tableau] crée une copie superficielle — une nouvelle référence avec les mêmes éléments. C'est fondamental en React : setPlats([...plats, nouveauPlat]) crée un nouveau tableau, React détecte la nouvelle référence, le composant se re-rend. plats.push(nouveauPlat) mute le tableau original — React ne voit aucun changement, pas de re-rendu.

javascript
const platsPrincipaux = [
  { id: 1, nom: "Tajine Kefta",    prix: 75 },
  { id: 2, nom: "Couscous Royale", prix: 90 },
];
const desserts = [
  { id: 5, nom: "Chebakia", prix: 15 },
  { id: 6, nom: "Mhancha",  prix: 20 },
];

// ── Copier un tableau — nouvelle référence ────────────────────
const copiePlats = [...platsPrincipaux];
// copiePlats === platsPrincipaux → false (nouvelles références)
// copiePlats[0] === platsPrincipaux[0] → true (copie superficielle)

// ── Fusionner deux tableaux ───────────────────────────────────
const menuComplet = [...platsPrincipaux, ...desserts];

// ── Insérer avant ou après ────────────────────────────────────
const menuAvecSoupe  = [{ id: 0, nom: "Harira", prix: 25 }, ...platsPrincipaux];
const menuAvecCafe   = [...platsPrincipaux, { id: 9, nom: "Café marocain", prix: 10 }];

// ── Passer comme arguments individuels ────────────────────────
const prix = [75, 45, 120, 30, 90, 65];
const prixMax = Math.max(...prix); // Math.max(75, 45, 120, 30, 90, 65) → 120
const prixMin = Math.min(...prix); // 30

// ── Patterns React — gestion du tableau d'état ────────────────
const [panier, setPanier] = useState([]);

// AJOUTER un plat
const ajouterAuPanier = (plat) => setPanier([...panier, plat]);

// SUPPRIMER par id
const retirerDuPanier = (id) => setPanier(panier.filter(p => p.id !== id));

// METTRE À JOUR la quantité
const mettreAJourQuantite = (id, quantite) =>
  setPanier(panier.map(p => p.id === id ? { ...p, quantite } : p));

// VIDER le panier
const viderPanier = () => setPanier([]);

2
Spread sur les objets — pattern React central
Copier · Modifier · Fusionner · Ordre · setState

Le spread d'objet crée un nouvel objet avec toutes les propriétés de l'original. On peut ajouter ou écraser des propriétés. C'est le pattern central pour mettre à jour un objet d'état en React : setCommande({ ...commande, statut: "confirmee" }).

L'ordre est crucial : la dernière occurrence d'une propriété gagne. { ...obj, prop: 'nouvelle' } écrase prop de obj.

javascript
const livreur = {
  id: 7,
  nom: "Khalid El Amrani",
  ville: "Casablanca",
  vehicule: "moto",
  note: 4.7,
  disponible: true,
  nombreLivraisons: 312
};

// ── Modifier une propriété SANS muter l'original ─────────────
const livreurIndisponible = { ...livreur, disponible: false };
console.log(livreur.disponible);             // true  — original intact
console.log(livreurIndisponible.disponible); // false — copie modifiée

// ── Ajouter une propriété ─────────────────────────────────────
const livreurAvecZone = { ...livreur, zoneIntervention: ["Ain Sebaa", "Hay Mohammadi"] };

// ── L'ordre détermine quelle valeur gagne ────────────────────
const miseAJour = { ville: "Mohammedia", note: 4.9 };

// Spread d'abord, surcharge ensuite → miseAJour gagne
const livreurMisAJour = { ...livreur, ...miseAJour };
console.log(livreurMisAJour.ville); // "Mohammedia"

// Surcharge d'abord, spread ensuite → livreur gagne
const livreurNonMisAJour = { ...miseAJour, ...livreur };
console.log(livreurNonMisAJour.ville); // "Casablanca"

// ── Patterns React — mise à jour d'état objet ─────────────────
const [commande, setCommande] = useState({
  client: "Fatima-Zahra Alaoui",
  adresse: "Hay Riad, Rabat",
  total: 145,
  statut: "en_attente",
  livreur: null
});

setCommande({ ...commande, statut: "confirmee" });
setCommande({ ...commande, statut: "en_preparation", livreur: livreurMisAJour });

// ── NE JAMAIS faire — mutation directe ───────────────────────
commande.statut = "confirmee"; // React ne détecte PAS le changement

// ── Copie superficielle — mettre à jour un objet imbriqué ─────
// Il faut spreader à CHAQUE niveau
setCommande({
  ...commande,
  livreur: {
    ...commande.livreur,
    note: 4.9
  }
});

3
Rest operator — regrouper les éléments restants
Paramètres · Destructuring · ...autresProps React
javascript
// ── Rest dans les paramètres de fonction ──────────────────────
const calculerTotalPanier = (...articles) => {
  // articles est un VRAI tableau JavaScript
  return articles.reduce((total, a) => total + a.prix * a.quantite, 0);
};

calculerTotalPanier(
  { nom: "Tajine",   prix: 75, quantite: 2 },
  { nom: "Harira",   prix: 25, quantite: 3 },
  { nom: "Chebakia", prix: 15, quantite: 4 }
); // → 285 MAD

// Mélange paramètre fixe + rest
const log = (type, livreur, ...details) => {
  console.log(`[${type}] ${livreur}`);
  details.forEach(d => console.log("  →", d));
};

// ── Rest en destructuring d'objet ─────────────────────────────
const plat = {
  id: 3, nom: "Pastilla", prix: 65,
  categorie: "entree", disponible: true,
  allergenes: ["gluten", "oeufs"], restaurant: "La Sqala"
};

const { id, nom, prix, ...metadonnees } = plat;
// metadonnees = { categorie: "entree", disponible: true, allergenes: [...], restaurant: "..." }

// ── Pattern React — passer les props restantes vers un enfant ─
const BoutonDariGo = ({ label, variante = "primary", isLoading = false, ...autresProps }) => (
  <button
    className={`btn btn-${variante} ${isLoading ? "btn-loading" : ""}`}
    disabled={isLoading}
    {...autresProps}  // onClick, type, aria-label, data-testid, etc.
  >
    {isLoading ? "Chargement..." : label}
  </button>
);

// Utilisation — onClick et type passés via ...autresProps
<BoutonDariGo
  label="Commander"
  variante="success"
  onClick={passerCommande}
  type="submit"
  aria-label="Passer la commande DariGo"
/>

✓
Vérification des acquis
5 questions — corrigé intégré
Le panier DariGo contient 3 plats. On appelle setPanier([...panier, nouveauPlat]). Pourquoi cette syntaxe déclenche un re-rendu React, contrairement à panier.push(nouveauPlat) ?
Quel est le résultat de { ...{ ville: "Fès", note: 4.5 }, ville: "Meknès", actif: true } ?
On a const somme = (...nombres) => nombres.reduce((a, b) => a + b, 0). Quel est le type de nombres ?
Dans le composant BoutonDariGo, la syntaxe {...autresProps} dans le JSX sert à quoi ?
La copie avec spread est-elle profonde ou superficielle ? Quel impact en React ?
Partie 0 — Rappel JavaScript ES6 Template Literals Interpolation · Multiligne · Tagged
⏱ 12 min

Template literals — interpolation de chaînes

Avant ES6, construire des chaînes dynamiques en JavaScript était pénible : concaténation avec +, guillemets imbriqués, impossibilité d'écrire sur plusieurs lignes. Les template literals résolvent tout avec une syntaxe élégante. En React, vous les utiliserez pour construire des URLs d'API, des messages, des classes CSS conditionnelles et des textes dynamiques.


1
Syntaxe, interpolation et expressions
Backtick · ${ } · Multiligne · Expressions complexes

Un template literal est délimité par des backticks ` (accent grave). À l'intérieur, toute expression JavaScript peut être insérée entre ${ et }. L'expression est évaluée, convertie en chaîne, et insérée à sa place dans la chaîne finale. L'expression peut être une variable, un calcul, un appel de fonction, un opérateur ternaire ou un accès à une propriété.

javascript
const commande = {
  id: 1042, client: "Nadia Benali",
  ville: "Meknès", total: 145,
  statut: "en_preparation"
};
const livreur = "Khalid El Amrani";
const eta = 18;

// ── Avant ES6 — illisible avec beaucoup de variables ──────────
const msgAncien = "Marhba " + commande.client + " ! Commande #"
  + commande.id + " de " + commande.total + " MAD. ETA : " + eta + " min.";

// ── Template literal — lisible ────────────────────────────────
const msg = `Marhba ${commande.client} ! Commande #${commande.id} de ${commande.total} MAD. ETA : ${eta} min par ${livreur}.`;

// ── Expressions dans ${ } ─────────────────────────────────────
const totalTTC  = `Total TTC : ${(commande.total * 1.2).toFixed(2)} MAD`;
const nbArts    = `Panier : ${panier.length} article${panier.length > 1 ? "s" : ""}`;
const statutMsg = `Statut : ${commande.statut === "livree" ? "Livré" : "En cours"}`;
const heure     = `Heure : ${new Date().toLocaleTimeString("fr-MA")}`;

// ── Multiligne — sans \n ni concaténation ────────────────────
const resumeCommande = `
Récapitulatif DariGo
────────────────────
Commande  : #${commande.id}
Client    : ${commande.client} — ${commande.ville}
Total     : ${commande.total} MAD
Livraison : ${livreur}
ETA       : ${eta} minutes
`.trim();

// ── Construction d'URLs — usage central en React ──────────────
const API = "https://api.darigo.ma";
const page = 2;
const categorie = "plat";

const urlPlats       = `${API}/plats`;
const urlCommande    = `${API}/commandes/${commande.id}`;
const urlPlatsFilter = `${API}/plats?categorie=${categorie}&ville=Casablanca&page=${page}&limit=10`;

// ── Classes CSS conditionnelles en JSX ────────────────────────
const classeStatut = `badge ${commande.statut === "livree" ? "badge-success" : "badge-warning"}`;
const classeCarte  = `carte-plat ${plat.disponible ? "" : "carte-plat--indisponible"}`;
const classeBtn    = `btn btn-${variante} ${isLoading ? "btn--loading" : ""}`;

2
Tagged template literals
styled-components · GraphQL

Un template literal peut être "taggé" avec une fonction. La syntaxe est maFonction`texte ${valeur}`. La fonction reçoit les parties statiques et les valeurs interpolées séparément. Vous rencontrerez cette syntaxe dans des bibliothèques React populaires — vous n'avez pas besoin de créer vos propres tagged templates, mais il faut savoir les reconnaître.

javascript
// ── styled-components — CSS-in-JS avec React ─────────────────
import styled from 'styled-components';

const BoutonCommander = styled.button`
  background-color: ${props => props.urgent ? "#e74c3c" : "#2ecc71"};
  color: white;
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 16px;
  cursor: pointer;
  &:hover { opacity: 0.9; }
`;

<BoutonCommander urgent={commande.urgent}>
  Commander maintenant
</BoutonCommander>

// ── Apollo GraphQL ────────────────────────────────────────────
import { gql } from '@apollo/client';

const GET_PLATS = gql`
  query GetPlats($ville: String!) {
    plats(ville: $ville) {
      id nom prix disponible
    }
  }
`;
ℹ À retenir

Vous n'avez pas besoin de créer vos propres tagged template literals. L'essentiel est de reconnaître la syntaxe dans des bibliothèques comme styled-components ou Apollo. Un backtick après un nom de fonction indique un tagged template literal.


✓
Vérification des acquis
4 questions — corrigé intégré
Quel est le résultat de `Commande #${3 * 7} — ${["Casablanca", "Rabat"][1]}` ?
On veut construire dynamiquement la classe CSS "badge badge-success" ou "badge badge-warning" selon le statut. Quelle syntaxe utiliser en JSX ?
Quelle syntaxe permet d'écrire une chaîne sur plusieurs lignes en JavaScript ES6 ?
Vous voyez dans un projet React : const Titre = styled.h1`color: ${props => props.couleur};`. Que reconnaissez-vous ?
Partie 0 — Rappel JavaScript ES6 import · export Default · Named · Alias · Barrel
⏱ 15 min

Modules ES6 — import & export

Un projet React est composé de dizaines voire de centaines de fichiers. Le système de modules ES6 permet à ces fichiers de se référencer mutuellement — d'exporter ce qu'ils proposent et d'importer ce dont ils ont besoin. Comprendre la différence entre export nommé et export par défaut est indispensable : une confusion entre les deux produit un bug silencieux où le composant s'affiche vide, sans aucun message d'erreur.


1
Export nommé vs export par défaut
Named · Default · Accolades · Alias · Mixte · Barrel

Export nommé : autant qu'on veut par fichier. L'import se fait avec des accolades et le nom exact. Utilisé pour les fonctions utilitaires, les constantes, les hooks multiples d'un même fichier.

Export par défaut : un seul par fichier. L'import se fait sans accolades avec le nom de votre choix. Convention React : un fichier = un composant principal = un export default.

javascript
// ── FICHIER : utils/prix.js — exports nommés ──────────────────
export const formaterPrix = (prix) => `${prix} MAD`;
export const calculerTTC = (prix, tva = 0.2) => +(prix * (1 + tva)).toFixed(2);
export const appliquerRemise = (prix, pct) => +(prix * (1 - pct / 100)).toFixed(2);
export const FRAIS_LIVRAISON_DEFAUT = 15;
export const VILLES_LIVRAISON = ["Casablanca", "Rabat", "Fès", "Marrakech", "Agadir"];

// Ou grouper à la fin du fichier :
const formaterPrix = (prix) => `${prix} MAD`;
const calculerTTC = (prix) => +(prix * 1.2).toFixed(2);
export { formaterPrix, calculerTTC };

// ── FICHIER : components/CartePlat.jsx — export default ───────
const CartePlat = ({ id, nom, prix, disponible, restaurant }) => (
  <div className={`carte-plat ${disponible ? "" : "carte-plat--grise"}`}>
    <h3>{nom}</h3>
    <p className="prix">{prix} MAD</p>
    <p className="restaurant">{restaurant}</p>
    {!disponible && <span className="badge-indisponible">Indisponible</span>}
  </div>
);
export default CartePlat;

// ── FICHIER : components/Header.jsx — nommé ET défaut ────────
export const LOGO_URL = "/images/darigo-logo.svg"; // export nommé
const Header = ({ titre, panier }) => ( ... );
export default Header; // export par défaut

// ── IMPORTS ───────────────────────────────────────────────────
// Import nommé — accolades OBLIGATOIRES, nom EXACT
import { formaterPrix, calculerTTC } from './utils/prix';
import { FRAIS_LIVRAISON_DEFAUT, VILLES_LIVRAISON } from './utils/prix';

// Import nommé avec alias
import { formaterPrix as fp, VILLES_LIVRAISON as villes } from './utils/prix';

// Import namespace — tout le module dans un objet
import * as PrixUtils from './utils/prix';
PrixUtils.formaterPrix(85); // "85 MAD"

// Import par défaut — PAS d'accolades, NOM LIBRE
import CartePlat from './components/CartePlat';
import Carte from './components/CartePlat'; // renommage libre — identique

// Import mixte — défaut ET nommés dans la même instruction
import Header, { LOGO_URL } from './components/Header';
import React, { useState, useEffect, useCallback, useRef } from 'react';

// ── ERREUR FRÉQUENTE ET SILENCIEUSE ──────────────────────────
// CartePlat est un export default → import SANS accolades
import CartePlat from './components/CartePlat';         // CORRECT
import { CartePlat } from './components/CartePlat';    // FAUX — undefined !

// formaterPrix est un export nommé → import AVEC accolades
import { formaterPrix } from './utils/prix';            // CORRECT
import formaterPrix from './utils/prix';                // FAUX — importe le default (undefined)

// ── Index barrel — simplifier les imports ─────────────────────
// components/index.js
export { default as CartePlat } from './CartePlat';
export { default as PanierResume } from './PanierResume';
export { default as Header } from './Header';
export { default as SuiviLivraison } from './SuiviLivraison';

// Dans App.jsx — un seul import au lieu de plusieurs
import { CartePlat, PanierResume, Header, SuiviLivraison } from './components';

2
Structure d'un projet React — DariGo
Organisation · Conventions · src/
bash
src/
├── index.js                    # Point d'entrée — ReactDOM.createRoot()
├── App.jsx                     # Composant racine — export default App
│
├── components/                 # Composants réutilisables
│   ├── CartePlat.jsx           # export default CartePlat
│   ├── PanierResume.jsx        # export default PanierResume
│   ├── Header.jsx              # export default Header
│   ├── SuiviLivraison.jsx      # export default SuiviLivraison
│   └── index.js                # Barrel — re-export tous les composants
│
├── pages/                      # Pages (une par route)
│   ├── PageAccueil.jsx
│   ├── PageMenu.jsx
│   ├── PageCommande.jsx
│   └── PageSuivi.jsx
│
├── services/                   # Appels API
│   ├── api.js                  # utilitaire requete()
│   ├── platsService.js
│   ├── commandesService.js
│   └── authService.js
│
├── hooks/                      # Custom hooks
│   ├── usePlats.js
│   ├── useCommande.js
│   └── useAuth.js
│
├── store/                      # Redux
│   ├── index.js
│   ├── panierSlice.js
│   └── authSlice.js
│
└── utils/                      # Fonctions utilitaires
    ├── prix.js                 # formaterPrix, calculerTTC...
    ├── dates.js                # formaterDate, tempsEcoule...
    └── validation.js           # validerEmail, validerTel...

✓
Vérification des acquis
5 questions — corrigé intégré
Le fichier CartePlat.jsx contient export default CartePlat. On l'importe avec import { CartePlat } from './CartePlat'. Que se passe-t-il ?
Combien d'exports par défaut peut contenir un fichier JavaScript ?
On écrit import React, { useState, useEffect } from 'react'. Que sont React, useState et useEffect ?
Qu'est-ce qu'un index barrel dans un projet React et pourquoi l'utilise-t-on ?
On importe import { VILLES_LIVRAISON as villes } from './utils/prix'. Pourquoi utiliser un alias ?
Partie 0 — Rappel JavaScript ES6 map() filter() · reduce() · find()
⏱ 20 min

map(), filter(), reduce() — le trio React

Ces trois méthodes sont au cœur de React. map() transforme chaque élément d'un tableau de données en composant JSX — c'est comme ça qu'on affiche des listes en React. filter() affiche des sous-ensembles selon des critères. reduce() calcule des valeurs agrégées : total du panier, nombre d'articles, données groupées par catégorie. Ces méthodes ne mutent jamais le tableau original — elles retournent toujours un nouveau tableau ou une nouvelle valeur, respectant l'immutabilité de React.


1
map() — transformer et rendre des listes
Transformation · JSX · key · find() · some() · every()

map() appelle une fonction de transformation sur chaque élément et retourne un nouveau tableau de même longueur. En React, c'est la méthode pour transformer un tableau de données en tableau de composants JSX.

La prop key est obligatoire quand on utilise map() pour rendre une liste. React utilise cette clé pour identifier chaque élément de façon unique et optimiser les mises à jour. La clé doit être unique parmi les frères et stable dans le temps. Utilisez toujours l'identifiant de la donnée en base. N'utilisez l'index du tableau que si les données ne sont jamais réordonnées ni filtrées — sinon React fait des erreurs de rendu.

javascript
const plats = [
  { id: 1, nom: "Tajine Kefta",       prix: 75,  categorie: "plat",    disponible: true,  note: 4.8 },
  { id: 2, nom: "Harira Ramadaniya",  prix: 25,  categorie: "soupe",   disponible: false, note: 4.9 },
  { id: 3, nom: "Pastilla au Poulet", prix: 65,  categorie: "entree",  disponible: true,  note: 4.6 },
  { id: 4, nom: "Couscous Royale",    prix: 90,  categorie: "plat",    disponible: true,  note: 4.7 },
  { id: 5, nom: "Chebakia",           prix: 15,  categorie: "dessert", disponible: true,  note: 4.5 },
];

// ── map() — transformations ───────────────────────────────────
const noms = plats.map(p => p.nom);
// ["Tajine Kefta", "Harira Ramadaniya", ...]

const prixAvecRemise = plats.map(p => ({
  ...p,
  prix: +(p.prix * 0.9).toFixed(2),
  enPromo: true
}));

const nomsNumerotes = plats.map((p, index) =>
  `${index + 1}. ${p.nom} — ${p.prix} MAD`
);

// ── map() en JSX — lister des composants ──────────────────────
const MenuDariGo = ({ plats }) => (
  <section className="menu">
    <div className="grille-plats">
      {plats.map(plat => (
        <CartePlat
          key={plat.id}        // OBLIGATOIRE — id stable de la BDD
          id={plat.id}
          nom={plat.nom}
          prix={plat.prix}
          disponible={plat.disponible}
          note={plat.note}
        />
      ))}
    </div>
  </section>
);

// ── Méthodes complémentaires ──────────────────────────────────
// find() — premier élément correspondant (ou undefined)
const platPrincipal = plats.find(p => p.categorie === "plat");
// { id: 1, nom: "Tajine Kefta", ... }
const platInexistant = plats.find(p => p.prix > 200); // undefined

// findIndex() — index du premier élément correspondant
const indexTajine = plats.findIndex(p => p.id === 1); // 0

// some() — au moins un élément correspond → booléen
const aDesPlatsDisponibles = plats.some(p => p.disponible); // true
const aDesPlatsExpensifs = plats.some(p => p.prix > 100);   // false

// every() — tous les éléments correspondent → booléen
const tousDisponibles = plats.every(p => p.disponible);     // false (Harira non dispo)
const tousMoinsDeDeuxCents = plats.every(p => p.prix < 200); // true

2
filter() — afficher des sous-ensembles
Condition · Recherche · Chaînage · Suppression d'état

filter() appelle une fonction prédicat sur chaque élément et retourne un nouveau tableau contenant uniquement les éléments pour lesquels la fonction a retourné true. En React, c'est le cœur de toute fonctionnalité de recherche, de filtrage par catégorie, et de suppression d'éléments dans un tableau d'état.

javascript
// ── filter() — cas d'usage simples ───────────────────────────
const platsDisponibles = plats.filter(p => p.disponible);
const platsPrincipaux  = plats.filter(p => p.categorie === "plat");
const platsBonMarche   = plats.filter(p => p.prix <= 30);
const platsNotesHaut   = plats.filter(p => p.note >= 4.7);

// ── filter() en React — barre de recherche temps réel ─────────
const MenuAvecRecherche = ({ tousLesPlats }) => {
  const [recherche, setRecherche] = useState("");
  const [categorieActive, setCategorieActive] = useState("tous");
  const [seulementDisponibles, setSeulementDisponibles] = useState(true);

  // Chaîner plusieurs filter() — chaque filtre s'applique au résultat du précédent
  const platsFiltres = tousLesPlats
    .filter(p => !seulementDisponibles || p.disponible)
    .filter(p => categorieActive === "tous" || p.categorie === categorieActive)
    .filter(p => p.nom.toLowerCase().includes(recherche.toLowerCase()));

  return (
    <div>
      <input
        type="text"
        placeholder="Chercher un plat..."
        value={recherche}
        onChange={e => setRecherche(e.target.value)}
      />
      <div className="filtres">
        {["tous", "plat", "entree", "soupe", "dessert"].map(cat => (
          <button
            key={cat}
            className={categorieActive === cat ? "actif" : ""}
            onClick={() => setCategorieActive(cat)}
          >{cat}</button>
        ))}
      </div>
      <p>{platsFiltres.length} plat(s) trouvé(s)</p>
      <div className="grille">
        {platsFiltres.map(p => <CartePlat key={p.id} {...p} />)}
      </div>
    </div>
  );
};

// ── filter() pour supprimer de l'état ────────────────────────
const retirerDuPanier = (id) =>
  setPanier(prev => prev.filter(item => item.id !== id));

// ── Dédupliquer ───────────────────────────────────────────────
const categoriesUniques = [...new Set(plats.map(p => p.categorie))];
// ["plat", "soupe", "entree", "dessert"]

3
reduce() — agréger des données complexes
Accumulateur · Total · Grouper · Indexer

reduce() est la méthode la plus puissante. Elle parcourt le tableau en maintenant un accumulateur qui évolue à chaque itération. La signature est tableau.reduce(fonction, valeurInitiale). La fonction reçoit (accumulateur, elementCourant). L'accumulateur peut être un nombre, une chaîne, un tableau ou un objet.

javascript
const panier = [
  { id: 1, nom: "Tajine Kefta",    prix: 75, quantite: 2 },
  { id: 3, nom: "Pastilla",        prix: 65, quantite: 1 },
  { id: 5, nom: "Chebakia",        prix: 15, quantite: 4 },
];

// ── Total et nombre d'articles ────────────────────────────────
const sousTotal = panier.reduce((acc, item) => acc + item.prix * item.quantite, 0);
// 75*2 + 65*1 + 15*4 = 275 MAD

const nbArticles = panier.reduce((acc, item) => acc + item.quantite, 0);
// 2 + 1 + 4 = 7 articles

// ── Grouper les plats par catégorie ──────────────────────────
const platsParCategorie = plats.reduce((groupes, plat) => {
  const cat = plat.categorie;
  if (!groupes[cat]) groupes[cat] = [];
  groupes[cat].push(plat);
  return groupes;
}, {});
// { "plat": [Tajine, Couscous], "soupe": [Harira], "entree": [Pastilla], "dessert": [Chebakia] }

// ── Indexer par id — accès O(1) ───────────────────────────────
const platsParId = plats.reduce((index, plat) => {
  index[plat.id] = plat;
  return index;
}, {});
// platsParId[3] → Pastilla directement

// ── Statistiques du menu ──────────────────────────────────────
const stats = plats.reduce((acc, plat) => {
  acc.total++;
  acc.prixTotal += plat.prix;
  if (plat.disponible) acc.disponibles++;
  if (plat.note > acc.meilleureNote) {
    acc.meilleureNote = plat.note;
    acc.meilleureNom = plat.nom;
  }
  return acc;
}, { total: 0, prixTotal: 0, disponibles: 0, meilleureNote: 0, meilleureNom: "" });

// ── Composant résumé panier ───────────────────────────────────
const ResumePanier = ({ panier, fraisLivraison = 15 }) => {
  const sousTotal  = panier.reduce((acc, item) => acc + item.prix * item.quantite, 0);
  const nbArticles = panier.reduce((acc, item) => acc + item.quantite, 0);
  return (
    <div className="resume-panier">
      <p>{nbArticles} article(s)</p>
      <p>Sous-total : {sousTotal} MAD</p>
      <p>Livraison : {fraisLivraison} MAD</p>
      <strong>Total : {sousTotal + fraisLivraison} MAD</strong>
    </div>
  );
};

✓
Vérification des acquis
5 questions — corrigé intégré
Dans MenuDariGo, on utilise key={plat.id}. Que se passe-t-il si on utilise key={index} alors que la liste peut être filtrée ?
On chaîne : plats.filter(p => p.disponible).filter(p => p.prix < 80).map(p => p.nom). Combien de nouveaux tableaux sont créés ?
Que retourne [{ prix: 75, qte: 2 }, { prix: 65, qte: 1 }].reduce((acc, item) => acc + item.prix * item.qte, 0) ?
Quelle méthode utilise-t-on pour savoir si AU MOINS UN plat est noté 5 étoiles ?
On veut mettre à jour le prix d'un plat dans le tableau d'état plats. Quelle méthode ?
Partie 0 — Rappel JavaScript ES6 Promise async / await · try/catch · AbortController
⏱ 20 min

Promises & async/await

Toute communication avec une API dans une application React est asynchrone. Charger les plats DariGo, passer une commande, s'authentifier, suivre une livraison — tout cela implique des opérations dont le résultat n'est pas immédiatement disponible. Comprendre les Promises et async/await est une nécessité absolue avant d'aborder React.

ℹ Pourquoi JavaScript est-il asynchrone ?

JavaScript est mono-thread et non-bloquant. Quand on fait une requête réseau qui peut prendre 100ms à 2 secondes, bloquer le thread principal gèlerait toute l'interface. L'asynchrone délègue l'opération longue au navigateur, continue d'exécuter le reste du code, et reprend quand le résultat est disponible.


1
Les Promises — représenter une valeur future
États · then · catch · finally · Promise.all · Promise.allSettled

Une Promise représente la complétion éventuelle d'une opération asynchrone. Elle peut être dans l'un de ces trois états :

  • Pending : l'opération est en cours — la requête est partie, on attend.
  • Fulfilled : l'opération a réussi — la réponse est arrivée avec les données.
  • Rejected : l'opération a échoué — erreur réseau, timeout, erreur serveur 500.

Une fois fulfilled ou rejected, une Promise est figée. On accède à sa valeur avec .then() (succès) et .catch() (erreur).

javascript
// ── Chaîner avec .then(), .catch(), .finally() ────────────────
fetch("https://api.darigo.ma/plats")
  .then(response => {
    // IMPORTANT : fetch ne rejette pas pour les codes HTTP 4xx/5xx
    if (!response.ok) {
      throw new Error(`Erreur HTTP ${response.status}`);
    }
    return response.json(); // retourne une autre Promise
  })
  .then(plats => setPlats(plats))
  .catch(erreur => setErreur(erreur.message))
  .finally(() => setEstEnChargement(false)); // toujours exécuté

// ── Promise.all() — paralléliser sans tout annuler ────────────
// Lance toutes les Promises en PARALLÈLE.
// Si UNE échoue → tout échoue (reject immédiat).
Promise.all([
  fetch("https://api.darigo.ma/plats").then(r => r.json()),
  fetch("https://api.darigo.ma/categories").then(r => r.json()),
  fetch("https://api.darigo.ma/restaurants?ville=Casablanca").then(r => r.json()),
])
.then(([plats, categories, restaurants]) => {
  setPlats(plats);
  setCategories(categories);
  setRestaurants(restaurants);
})
.catch(erreur => console.error("Chargement partiel échoué :", erreur));

// ── Promise.allSettled() — attendre sans tout annuler ─────────
// Attend que TOUTES les Promises se terminent — succès OU échec.
Promise.allSettled([
  fetch("https://api.darigo.ma/plats").then(r => r.json()),
  fetch("https://api.darigo.ma/promotions").then(r => r.json()), // peut échouer
])
.then(resultats => {
  resultats.forEach((resultat, i) => {
    if (resultat.status === "fulfilled") {
      console.log(`Requête ${i} réussie :`, resultat.value);
    } else {
      console.warn(`Requête ${i} échouée :`, resultat.reason.message);
    }
  });
});

2
async/await — code asynchrone lisible
Syntaxe · try/catch · useEffect · AbortController

async/await est du sucre syntaxique sur les Promises — il rend le code asynchrone lisible comme du code synchrone. Une fonction async retourne toujours une Promise. Le mot-clé await suspend l'exécution de la fonction jusqu'à ce que la Promise soit résolue, puis retourne sa valeur.

javascript
// ── async/await avec gestion d'erreurs ────────────────────────
const passerCommande = async (panier, adresse, token) => {
  try {
    const response = await fetch("https://api.darigo.ma/commandes", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`
      },
      body: JSON.stringify({ articles: panier, adresseLivraison: adresse })
    });

    if (!response.ok) {
      const erreurBody = await response.json();
      throw new Error(erreurBody.message || `Erreur ${response.status}`);
    }

    return await response.json();
    // { id: 1042, statut: "confirmee", estimationLivraison: 18 }

  } catch (erreur) {
    if (erreur.name === "TypeError") {
      throw new Error("Pas de connexion internet — vérifiez votre réseau");
    }
    throw erreur; // re-lancer pour que l'appelant gère
  }
};

// ── useEffect avec async/await — pattern fondamental React ────
// RÈGLE : useEffect ne peut PAS être async directement.
// Une fonction async retourne une Promise — useEffect ne sait quoi en faire.
// SOLUTION : déclarer une fonction async INTERNE et l'appeler immédiatement.

const MenuDariGo = ({ ville }) => {
  const [plats, setPlats] = useState([]);
  const [estEnChargement, setEstEnChargement] = useState(true);
  const [erreur, setErreur] = useState(null);

  useEffect(() => {
    const charger = async () => {
      try {
        setEstEnChargement(true);
        setErreur(null);
        const data = await chargerMenu(ville);
        setPlats(data);
      } catch (err) {
        setErreur(err.message);
      } finally {
        setEstEnChargement(false);
      }
    };
    charger(); // appel immédiat
  }, [ville]); // re-exécuter si ville change

  if (estEnChargement) return <div className="spinner">Chargement...</div>;
  if (erreur) return <div className="erreur">{erreur}</div>;
  return <div>{plats.map(p => <CartePlat key={p.id} {...p} />)}</div>;
};

// ── AbortController — annuler une requête en cours ────────────
useEffect(() => {
  const controller = new AbortController();

  const charger = async () => {
    try {
      const response = await fetch("https://api.darigo.ma/plats", {
        signal: controller.signal // lier la requête au controller
      });
      const data = await response.json();
      setPlats(data);
    } catch (err) {
      if (err.name !== "AbortError") { // ignorer l'erreur d'annulation
        setErreur(err.message);
      }
    }
  };

  charger();
  return () => controller.abort(); // Cleanup : annuler au démontage
}, []);
⚠ Mise à jour d'état sur composant démonté

Si une requête async se termine après que le composant soit démonté (l'utilisateur a navigué ailleurs), appeler setState sur un composant démonté produit un avertissement React et peut causer des fuites mémoire. La solution est l'AbortController pour annuler la requête lors du démontage, comme montré ci-dessus.


✓
Vérification des acquis
5 questions — corrigé intégré
L'API DariGo répond avec un code HTTP 503. fetch() rejette-t-il automatiquement la Promise ?
Pourquoi ne peut-on pas écrire directement useEffect(async () => { const data = await fetchPlats(); setPlats(data); }, []) ?
On veut charger les plats ET les promotions en parallèle. Les promotions peuvent échouer sans bloquer les plats. Quelle méthode ?
Pourquoi utilise-t-on un AbortController dans useEffect lors d'un fetch ?
Une fonction async sans return explicite retourne quoi ?
Partie 0 — Rappel JavaScript ES6 Classes Héritage · super · Legacy React
⏱ 16 min

Classes ES6

Les classes ES6 sont du sucre syntaxique sur le système de prototypes de JavaScript. En React, elles importent pour deux raisons : les composants de classe (legacy) sont basés sur des classes qui étendent React.Component, présents dans beaucoup de codebases existants. Et comprendre les classes vous aide à saisir pourquoi les composants fonctionnels avec hooks ont été introduits — pour éviter exactement les complexités que les classes introduisaient.

ℹ Classes en React aujourd'hui

Depuis React 16.8 (2019), les hooks permettent de gérer l'état entièrement dans des composants fonctionnels. Les composants de classe ne sont plus recommandés pour les nouveaux développements. Cependant, de nombreuses entreprises marocaines et internationales ont des codebases React écrits avant 2019 avec des composants de classe. Savoir les lire et les maintenir est une compétence attendue sur le marché.


1
Syntaxe des classes ES6
constructor · méthodes · static · getters · extends · super
javascript
class Plat {
  constructor(nom, prix, categorie, restaurant) {
    this.nom = nom;
    this.prix = prix;
    this.categorie = categorie;
    this.restaurant = restaurant;
    this.disponible = true;
    this.dateAjout = new Date();
  }

  // Méthode d'instance
  afficher() {
    return `${this.nom} — ${this.prix} MAD (${this.restaurant})`;
  }

  appliquerRemise(pourcentage) {
    // Créer un nouvel objet — ne pas muter this (immutabilité)
    return new Plat(
      this.nom,
      +(this.prix * (1 - pourcentage / 100)).toFixed(2),
      this.categorie,
      this.restaurant
    );
  }

  // Méthode statique — appelée sur la CLASSE, pas une instance
  static creerSoupe(restaurant = "Restaurant Al Fassia, Rabat") {
    return new Plat("Harira Ramadaniya", 25, "soupe", restaurant);
  }

  static comparer(platA, platB) {
    return platA.prix - platB.prix; // Pour Array.sort()
  }

  // Getter — propriété calculée, accédée sans parenthèses
  get prixTTC() {
    return +(this.prix * 1.2).toFixed(2);
  }

  get estAbordable() {
    return this.prix <= 50;
  }

  // Setter
  set prixHT(valeur) {
    if (valeur < 0) throw new Error("Le prix ne peut pas être négatif");
    this.prix = +valeur.toFixed(2);
  }
}

// ── Instancier et utiliser ────────────────────────────────────
const tajine = new Plat("Tajine Kefta", 75, "plat", "La Sqala, Casablanca");
console.log(tajine.afficher()); // "Tajine Kefta — 75 MAD (La Sqala, Casablanca)"
console.log(tajine.prixTTC);    // 90 (getter — sans parenthèses)
console.log(tajine.estAbordable); // false

const tajinePromo = tajine.appliquerRemise(10); // nouvel objet, tajine intact
const soupe = Plat.creerSoupe(); // méthode statique

// Tri par prix
const platsTriesParPrix = [tajine, couscous, soupe].sort(Plat.comparer);

// ── Héritage avec extends et super ───────────────────────────
class PlatSpecial extends Plat {
  constructor(nom, prix, categorie, restaurant, chef) {
    super(nom, prix, categorie, restaurant); // OBLIGATOIRE en premier
    // super() appelle le constructor de Plat et initialise this
    // Sans super(), accéder à this lève une ReferenceError
    this.chef = chef;
    this.labelSpecial = true;
  }

  afficher() {
    return `${super.afficher()} | Chef : ${this.chef}`;
  }
}

const specialite = new PlatSpecial(
  "Mechoui d'Agneau",  220, "plat",
  "La Mamounia, Marrakech", "Chef Hassan Alaoui"
);
console.log(specialite instanceof Plat);        // true
console.log(specialite instanceof PlatSpecial); // true

2
Composant de classe vs fonctionnel — comparaison réelle
React.Component · state · lifecycle · Hooks
javascript
// ── COMPOSANT DE CLASSE (LEGACY) ─────────────────────────────
class PanierDariGo extends React.Component {
  constructor(props) {
    super(props); // OBLIGATOIRE — initialise React.Component
    this.state = {
      articles: [],
      villeLivraison: props.villeDefaut || "Casablanca",
    };
    // bind nécessaire pour les méthodes classiques
    this.ajouterArticle = this.ajouterArticle.bind(this);
  }

  // Méthode classique — nécessite bind dans constructor
  ajouterArticle(plat) {
    this.setState(prevState => ({
      articles: [...prevState.articles, plat]
    }));
  }

  // Arrow function comme propriété — this lexical, pas de bind
  retirerArticle = (id) => {
    this.setState(prevState => ({
      articles: prevState.articles.filter(a => a.id !== id)
    }));
  };

  // Lifecycle — équivalent de useEffect(fn, [])
  componentDidMount() {
    fetch("https://api.darigo.ma/plats")
      .then(r => r.json())
      .then(plats => this.setState({ platsDisponibles: plats }));
  }

  componentWillUnmount() {
    clearInterval(this.intervalId); // nettoyage
  }

  render() { // OBLIGATOIRE
    const { articles, villeLivraison } = this.state;
    const total = articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
    return (
      <div className="panier">
        <h2>Votre panier — {villeLivraison}</h2>
        {articles.map(a => (
          <div key={a.id}>
            <span>{a.nom}</span>
            <button onClick={() => this.retirerArticle(a.id)}>Retirer</button>
          </div>
        ))}
        <strong>Total : {total} MAD</strong>
      </div>
    );
  }
}

// ── ÉQUIVALENT AVEC HOOKS — ce que vous écrirez ───────────────
const PanierDariGo = ({ villeDefaut = "Casablanca" }) => {
  const [articles, setArticles] = useState([]);
  const [villeLivraison] = useState(villeDefaut);

  useEffect(() => {
    fetch("https://api.darigo.ma/plats")
      .then(r => r.json())
      .then(setPlatsDisponibles);
  }, []);

  const ajouterArticle = (plat) => setArticles(prev => [...prev, plat]);
  const retirerArticle = (id) => setArticles(prev => prev.filter(a => a.id !== id));
  const total = articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);

  return (
    <div className="panier">
      <h2>Votre panier — {villeLivraison}</h2>
      {articles.map(a => (
        <div key={a.id}>
          <span>{a.nom}</span>
          <button onClick={() => retirerArticle(a.id)}>Retirer</button>
        </div>
      ))}
      <strong>Total : {total} MAD</strong>
    </div>
  );
};
CritèreComposant de classe (legacy)Composant fonctionnel + Hooks
SyntaxeVerbeux — extends, constructor, super, render()Concis — simple fonction arrow
Étatthis.state + this.setState()useState() par variable
Cycle de viecomponentDidMount, componentWillUnmount...useEffect() unifié
Problème de thisFréquent — bind() nécessaireInexistant — pas de this
Logique réutilisableHOC ou Render Props — complexeCustom hooks — élégant
Recommandé en 2025Non — maintenance uniquementOui — standard de l'industrie

✓
Vérification des acquis
4 questions — corrigé intégré
Dans un composant React de classe, pourquoi doit-on appeler super(props) et pas juste super() ?
Dans un composant de classe, quelle est la différence entre une méthode classique et une arrow function comme propriété de classe ?
Quel est l'équivalent de componentDidMount en composant fonctionnel ?
Les classes ES6 introduisent-elles un vrai système d'héritage orienté objet, différent des prototypes ?
Partie 0 — Rappel JavaScript ES6 Fetch API GET · POST · PUT · DELETE · JWT
⏱ 18 min

Fetch API — communication HTTP depuis le navigateur

La Fetch API est l'interface native du navigateur pour effectuer des requêtes HTTP. C'est elle qui permet à DariGo React de communiquer avec le back-end : récupérer la liste des plats, s'authentifier, passer une commande, suivre une livraison. Sans Fetch API, une SPA React est une page statique — c'est Fetch qui lui donne vie.


1
Les 4 méthodes HTTP — CRUD
GET · POST · PUT/PATCH · DELETE · Headers · JWT
Méthode HTTPOpération CRUDExemple DariGoBody ?
GETReadGET /plats — lister les platsNon
POSTCreatePOST /commandes — nouvelle commandeOui
PUTUpdate completPUT /commandes/42 — remplacer toutOui
PATCHUpdate partielPATCH /commandes/42/statut — modifier le statutOui
DELETEDeleteDELETE /commandes/42 — annulerNon
javascript
const API = "https://api.darigo.ma";

// ── GET — lire avec paramètres et auth ────────────────────────
const chargerPlats = async (ville = "Casablanca", categorie = null, page = 1) => {
  const url = new URL(`${API}/plats`);
  url.searchParams.set("ville", ville);
  url.searchParams.set("page", page);
  url.searchParams.set("limit", 10);
  if (categorie) url.searchParams.set("categorie", categorie);

  const response = await fetch(url);
  if (!response.ok) throw new Error(`Erreur ${response.status}`);
  return response.json();
};

const chargerMesCommandes = async (token) => {
  const response = await fetch(`${API}/commandes/mes-commandes`, {
    headers: { "Authorization": `Bearer ${token}` }
  });
  if (!response.ok) throw new Error("Non autorisé ou session expirée");
  return response.json();
};

// ── POST — créer une commande ─────────────────────────────────
const passerCommande = async (panier, adresse, ville, token) => {
  const response = await fetch(`${API}/commandes`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json", // OBLIGATOIRE pour que le serveur parse le body
      "Authorization": `Bearer ${token}`
    },
    body: JSON.stringify({
      articles: panier.map(a => ({ idPlat: a.id, quantite: a.quantite })),
      adresseLivraison: adresse,
      villeLivraison: ville,
      modePaiement: "CMI"
    })
  });

  if (!response.ok) {
    const errBody = await response.json().catch(() => ({}));
    throw new Error(errBody.message || `Commande refusée (${response.status})`);
  }
  return response.json(); // { id: 1042, statut: "confirmee", estimationLivraison: 18 }
};

// ── PATCH — mettre à jour partiellement ──────────────────────
const updateStatut = async (id, statut, token) => {
  const response = await fetch(`${API}/commandes/${id}/statut`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${token}`
    },
    body: JSON.stringify({ statut }) // shorthand ES6
  });
  return response.json();
};

// ── DELETE — annuler ─────────────────────────────────────────
const annulerCommande = async (id, token) => {
  const response = await fetch(`${API}/commandes/${id}`, {
    method: "DELETE",
    headers: { "Authorization": `Bearer ${token}` }
  });
  if (response.status === 204) return true; // No Content — succès sans body
  if (!response.ok) throw new Error(`Annulation impossible (${response.status})`);
  return response.json();
};

2
Couche de service API — architecture professionnelle
DRY · Centralisation · Token JWT · Timeout · Intercepteurs

Dans un projet React professionnel, on ne disperse jamais les appels fetch() dans les composants. On les centralise dans une couche de service. Si l'URL de base change, on modifie un seul endroit. La gestion des headers d'auth, des erreurs et des timeouts est standardisée. Les composants restent purs — ils reçoivent des données et les affichent, sans logique réseau.

javascript
// ── services/api.js — fonction utilitaire centrale ────────────
const BASE_URL = process.env.REACT_APP_API_URL || "https://api.darigo.ma";
const TIMEOUT_MS = 10000;

const requete = async (endpoint, options = {}) => {
  const token = localStorage.getItem("darigo_token");
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);

  try {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      signal: controller.signal,
      headers: {
        "Content-Type": "application/json",
        ...(token && { "Authorization": `Bearer ${token}` }),
        ...options.headers
      },
      ...options
    });

    if (response.status === 401) {
      localStorage.removeItem("darigo_token");
      window.location.href = "/login";
      throw new Error("Session expirée");
    }

    if (!response.ok) {
      const errBody = await response.json().catch(() => ({}));
      throw new Error(errBody.message || `Erreur ${response.status}`);
    }

    if (response.status === 204) return null;
    return response.json();

  } catch (erreur) {
    if (erreur.name === "AbortError") {
      throw new Error("La requête a expiré — vérifiez votre connexion");
    }
    throw erreur;
  } finally {
    clearTimeout(timeoutId);
  }
};

// ── services/platsService.js ──────────────────────────────────
export const platsService = {
  getAll:        (params = {}) => requete(`/plats?${new URLSearchParams(params)}`),
  getById:       (id)         => requete(`/plats/${id}`),
  getParVille:   (ville)      => requete(`/plats?ville=${encodeURIComponent(ville)}`),
  rechercher:    (terme, ville) =>
    requete(`/plats?q=${encodeURIComponent(terme)}&ville=${encodeURIComponent(ville)}`),
};

// ── services/commandesService.js ──────────────────────────────
export const commandesService = {
  creer:          (data) => requete("/commandes", { method: "POST",  body: JSON.stringify(data) }),
  getById:        (id)   => requete(`/commandes/${id}`),
  getMesCommandes: ()    => requete("/commandes/mes-commandes"),
  updateStatut:   (id, statut) => requete(`/commandes/${id}/statut`, {
    method: "PATCH",
    body: JSON.stringify({ statut })
  }),
  annuler: (id) => requete(`/commandes/${id}`, { method: "DELETE" }),
};

// ── Utilisation dans un composant React ──────────────────────
import { platsService } from '../services/platsService';

const PageMenu = ({ ville }) => {
  const [plats, setPlats] = useState([]);
  const [erreur, setErreur] = useState(null);
  const [estEnChargement, setEstEnChargement] = useState(true);

  useEffect(() => {
    const charger = async () => {
      try {
        const data = await platsService.getParVille(ville);
        setPlats(data);
      } catch (err) {
        setErreur(err.message);
      } finally {
        setEstEnChargement(false);
      }
    };
    charger();
  }, [ville]);
  // ...
};

✓
Vérification des acquis
5 questions — corrigé intégré
On envoie une commande avec POST. Le header Content-Type: application/json est omis. Que se passe-t-il côté serveur Express.js ?
Quelle est la différence entre PUT /commandes/42 et PATCH /commandes/42 ?
Pourquoi utilise-t-on encodeURIComponent(terme) pour construire une URL de recherche ?
Dans la couche de service, pourquoi gère-t-on globalement l'erreur 401 avec une redirection vers /login ?
Quand controller.abort() est appelé après timeout, que se passe-t-il pour la Promise fetch ?
Partie 1 — Fondamentaux Web & SPA HTML · CSS · JS DOM · Navigateur · Écosystème
⏱ 18 min

Web Front-end — rappels fondamentaux & écosystème moderne

Avant de comprendre pourquoi React existe, il faut comprendre le problème qu'il résout. Le web front-end est le territoire du navigateur : HTML structure le contenu, CSS le met en forme, JavaScript le rend interactif. Pendant des années, les développeurs ont géré directement le DOM — l'arbre d'objets que le navigateur construit depuis le HTML. Ce travail manuel est verbeux, error-prone et difficile à maintenir dès que l'application devient complexe. C'est exactement ce que React vient résoudre.


1
Le navigateur — de l'URL à l'affichage
Parsing HTML · DOM · CSSOM · Render Tree · Reflow · Repaint

Quand un utilisateur tape https://darigo.ma dans son navigateur, une chaîne d'opérations précise se déclenche. Comprendre ce pipeline est fondamental pour comprendre les décisions architecturales de React — notamment pourquoi le Virtual DOM existe et pourquoi les mises à jour du DOM réel sont coûteuses.

1. Récupération des ressources

Le navigateur envoie une requête DNS pour résoudre darigo.ma en adresse IP, établit une connexion TCP/TLS, et envoie une requête HTTP GET. Le serveur répond avec le fichier HTML. Pour chaque ressource référencée dans ce HTML (CSS, JavaScript, images), le navigateur envoie de nouvelles requêtes.

2. Construction du DOM

Le moteur HTML du navigateur parse le fichier HTML caractère par caractère et construit le DOM (Document Object Model) — un arbre d'objets JavaScript qui représente la structure du document. Chaque balise HTML devient un nœud dans cet arbre. <div>, <p>, <h1> — chacun est un objet avec des propriétés, des méthodes et des événements. C'est cet arbre que JavaScript peut manipuler.

3. Construction du CSSOM

En parallèle, le navigateur parse les fichiers CSS et construit le CSSOM (CSS Object Model) — un arbre similaire au DOM mais pour les styles. Le CSSOM contient toutes les règles CSS résolues pour chaque élément, en tenant compte de la cascade, de l'héritage et de la spécificité.

4. Render Tree, Layout et Paint

Le navigateur combine le DOM et le CSSOM pour créer le Render Tree — uniquement les éléments visibles avec leurs styles calculés. Ensuite vient le Layout (ou Reflow) : calculer la position et la taille exacte de chaque élément à l'écran. Enfin le Paint : dessiner les pixels à l'écran. Ces deux dernières étapes sont les plus coûteuses en termes de performance.

Pourquoi c'est important pour React

Chaque modification du DOM peut déclencher un reflow et un repaint — des opérations qui mobilisent le CPU et peuvent bloquer l'interface. React optimise cela avec son Virtual DOM : avant de toucher le DOM réel, React calcule dans un arbre JavaScript virtuel ce qui a changé, puis n'applique que les modifications minimales nécessaires au DOM réel. C'est le sujet de la Partie 2.

javascript
// ── Manipulation DOM classique — sans React ───────────────────
// Scénario : afficher la liste des plats DariGo reçus de l'API

// 1. Récupérer le conteneur dans le DOM
const conteneurPlats = document.getElementById('liste-plats');

// 2. Pour chaque plat, créer des éléments DOM manuellement
function afficherPlats(plats) {
  // Vider le conteneur avant de re-remplir
  conteneurPlats.innerHTML = '';

  plats.forEach(plat => {
    // Créer chaque élément un par un
    const carte = document.createElement('div');
    carte.classList.add('carte-plat');
    if (!plat.disponible) carte.classList.add('carte-plat--indisponible');

    const titre = document.createElement('h3');
    titre.textContent = plat.nom;

    const prix = document.createElement('p');
    prix.classList.add('prix');
    prix.textContent = `${plat.prix} MAD`;

    const bouton = document.createElement('button');
    bouton.textContent = plat.disponible ? 'Ajouter' : 'Indisponible';
    bouton.disabled = !plat.disponible;

    // Attacher un event listener sur chaque bouton
    bouton.addEventListener('click', () => ajouterAuPanier(plat));

    // Assembler l'arbre manuellement
    carte.appendChild(titre);
    carte.appendChild(prix);
    carte.appendChild(bouton);
    conteneurPlats.appendChild(carte);
  });
}

// 3. Mettre à jour une propriété — trouver et modifier dans le DOM
function mettreAJourPrix(idPlat, nouveauPrix) {
  // Traverser le DOM pour trouver l'élément...
  const cartes = document.querySelectorAll('.carte-plat');
  cartes.forEach(carte => {
    if (carte.dataset.idPlat === String(idPlat)) {
      carte.querySelector('.prix').textContent = `${nouveauPrix} MAD`;
    }
  });
  // Fragile, verbeux, difficile à maintenir
}

// ── Le même résultat avec React ───────────────────────────────
// React se charge de TOUT le DOM — vous décrivez l'état souhaité
const MenuDariGo = ({ plats }) => (
  <div id="liste-plats">
    {plats.map(plat => (
      <div
        key={plat.id}
        className={`carte-plat ${plat.disponible ? '' : 'carte-plat--indisponible'}`}
      >
        <h3>{plat.nom}</h3>
        <p className="prix">{plat.prix} MAD</p>
        <button
          disabled={!plat.disponible}
          onClick={() => ajouterAuPanier(plat)}
        >
          {plat.disponible ? 'Ajouter' : 'Indisponible'}
        </button>
      </div>
    ))}
  </div>
);
// React synchronise le DOM automatiquement quand plats change.
// Plus besoin de getElementById, appendChild, ou querySelector.

2
L'écosystème front-end moderne
npm · Bundler · Transpileur · Linter · TypeScript · DevTools

Un projet React moderne n'est pas juste du JavaScript dans un fichier HTML. C'est un ensemble d'outils qui travaillent ensemble pour transformer votre code source en une application optimisée pour le navigateur. Comprendre le rôle de chaque outil évite la confusion quand quelque chose se passe mal.

OutilRôleExemple dans DariGo
npm / yarnGestionnaire de paquets — installer et gérer les dépendances du projetnpm install react react-dom react-router-dom axios
ViteBundler moderne — assemble tous les fichiers JS/CSS en bundles optimisés pour la productionnpm create vite@latest darigo-frontend -- --template react
BabelTranspileur — convertit ES6+ et JSX en ES5 lisible par tous les navigateurs (intégré à Vite)Transforme const CartePlat = () => ... en code ES5
ESLintLinter — analyse le code statiquement et signale les erreurs et mauvaises pratiquesSignale var, clés manquantes dans les listes, hooks mal utilisés
PrettierFormateur de code — applique un style cohérent automatiquementIndentation, guillemets, virgules finales — uniformes dans toute l'équipe
TypeScriptSur-ensemble typé de JavaScript — détecte les erreurs de type à la compilationTyper les props des composants, les réponses API, le store Redux
React DevToolsExtension navigateur — inspecter les composants, l'état, les props, les re-rendusVoir pourquoi un composant se re-rend trop souvent
bash
# ── Structure d'un projet React après initialisation avec Vite ──
darigo-frontend/
├── node_modules/          # Toutes les dépendances — jamais commitées dans Git
├── public/
│   └── vite.svg           # Assets statiques servis tels quels
├── src/
│   ├── main.jsx           # Point d'entrée — ReactDOM.createRoot()
│   ├── App.jsx            # Composant racine
│   ├── App.css
│   └── index.css
├── index.html             # Template HTML — Vite y injecte le JS compilé
├── package.json           # Dépendances et scripts npm
├── package-lock.json      # Versions exactes des dépendances installées
├── vite.config.js         # Configuration Vite
└── .eslintrc.cjs          # Règles ESLint

# ── Scripts disponibles après création ───────────────────────
npm run dev    # Serveur de développement avec hot reload — http://localhost:5173
npm run build  # Build de production — génère le dossier dist/
npm run preview # Prévisualiser le build de production en local
npm run lint   # Vérifier le code avec ESLint

3
Évolution du développement front-end
Pages statiques → jQuery → Angular/Vue/React → Méta-frameworks

Génération 1 — Pages statiques et CGI (1990-2000)

Le web originel : des fichiers HTML statiques servis directement. Chaque interaction nécessitait un rechargement complet de la page. Aucune interactivité côté client — tout était généré côté serveur. Pour un site comme DariGo, passer une commande rechargait toute la page.

Génération 2 — jQuery et DOM manipulation (2006-2013)

jQuery a simplifié la manipulation du DOM et résolu les problèmes de compatibilité entre navigateurs. Pour la première fois, il était simple d'ajouter de l'interactivité : afficher/cacher des éléments, faire des requêtes AJAX, animer des transitions. Mais jQuery ne structurait pas le code — sur des applications complexes, le JavaScript devenait rapidement un entrelacement de callbacks et de manipulations DOM difficile à maintenir. Les développeurs appelaient ça le "spaghetti code".

Génération 3 — Frameworks MVC (2010-2015)

Backbone.js, puis AngularJS (Angular 1) ont apporté une structure au code front-end. Le pattern MVC (Modèle-Vue-Contrôleur) séparait les données, l'affichage et la logique. C'était un progrès majeur, mais ces frameworks restaient complexes à apprendre et difficiles à optimiser.

Génération 4 — React, Vue, Angular (2013-présent)

React (2013, Facebook), Vue.js (2014), Angular 2+ (2016) introduisent le paradigme des composants : des unités d'interface encapsulées, réutilisables et composables. Plutôt que de manipuler le DOM directement, le développeur décrit l'état de l'interface et le framework synchronise le DOM automatiquement. C'est le saut conceptuel le plus important du développement front-end moderne.

Génération 5 — Méta-frameworks (2017-présent)

Next.js, Remix, Astro construisent par-dessus React pour ajouter le rendu côté serveur (SSR), la génération statique (SSG) et des architectures hybrides. Pour DariGo, une version production utiliserait probablement Next.js pour le SEO des pages restaurant et le SSR des menus.

ℹ React sur le marché marocain en 2025

React domine le marché du développement front-end au Maroc comme à l'international. Les offres d'emploi front-end à Casablanca, Rabat et dans les centres nearshore (Capgemini, Sopra, IBM, Atos, CGI) demandent majoritairement React ou Next.js. Vue.js est présent mais minoritaire. Angular est utilisé dans les grandes structures (banques, télécoms) pour des projets legacy. La maîtrise de React ouvre la grande majorité des opportunités front-end disponibles.


✓
Vérification des acquis
5 questions — corrigé intégré
Pourquoi modifier le DOM directement en JavaScript est-il coûteux en performance ?
Quel est le rôle de Babel dans un projet React avec Vite ?
Quelle est la différence entre un bundler (Vite) et un transpileur (Babel) ?
Le dossier node_modules/ est absent du dépôt Git DariGo. Un nouveau développeur clone le projet. Comment récupère-t-il les dépendances ?
Quelle génération du développement front-end React représente-t-il, et quelle est son innovation principale par rapport à jQuery ?
Partie 1 — Fondamentaux Web & SPA SPA MPA · Client-side routing · History API
⏱ 20 min

SPA vs MPA — Applications monopages et leur architecture

DariGo est une Single Page Application (SPA). L'utilisateur charge la page une seule fois — le navigateur télécharge le HTML, le CSS et tout le JavaScript de l'application. Ensuite, chaque navigation (vers la page menu, la page commande, la page suivi) ne déclenche pas de rechargement : React intercepte le clic, met à jour l'interface en JavaScript, et met à jour l'URL dans la barre d'adresse. Comprendre cette architecture, ses avantages et ses limites, est indispensable pour concevoir correctement une application React.


1
MPA — Application multipage traditionnelle
Rendu serveur · Rechargement complet · Session · SEO natif

Dans une MPA (Multi-Page Application), chaque URL correspond à une page distincte sur le serveur. Quand l'utilisateur clique sur un lien ou soumet un formulaire, le navigateur envoie une requête HTTP au serveur, qui génère une nouvelle page HTML complète et la renvoie. Le navigateur détruit la page courante et affiche la nouvelle depuis zéro.

C'est le modèle de la quasi-totalité des sites web des années 2000 et encore utilisé aujourd'hui par des frameworks serveur comme Laravel (PHP), Django (Python), Ruby on Rails, Spring (Java). WordPress est une MPA. Le site de Bank Al-Maghrib est une MPA. La plupart des sites institutionnels marocains sont des MPAs.

Chaque navigation implique un rechargement complet avec une requête réseau, une reconstruction du DOM, et souvent un "flash" blanc pendant le chargement. L'état JavaScript (panier, données en mémoire, position de scroll) est perdu à chaque navigation.

text
── Cycle de vie d'une navigation MPA ──────────────────────────

Utilisateur sur darigo.ma/menu
       │
       ▼
Clic sur "Voir ma commande"
       │
       ▼
Navigateur : GET https://darigo.ma/commande/1042
       │
       ▼
Serveur : génère la page HTML complète de la commande
  - Requête BDD pour récupérer la commande
  - Injection des données dans le template HTML
  - Retour d'un document HTML complet (~50KB)
       │
       ▼
Navigateur : reçoit le HTML
  - Détruit la page actuelle (perd l'état JS)
  - Parse le HTML, charge les CSS/JS liés
  - Reconstruit le DOM complet
  - Affiche la nouvelle page
       │
       ▼
Temps total : 300ms à 2s selon réseau et serveur

Problèmes :
- Flash blanc pendant le rechargement
- Perte de l'état (panier, scroll, filtres actifs)
- Header/footer re-rendu inutilement à chaque page
- Chaque page = nouvelle requête réseau + génération HTML serveur

2
SPA — Application monopage
Chargement initial · History API · Client-side routing · État persistant

Dans une SPA, le serveur envoie une seule fois un fichier HTML minimal (souvent un <div id="root"> vide) et les bundles JavaScript de toute l'application. Ensuite, React prend le contrôle du navigateur : il affiche les composants correspondant à l'URL courante, intercepte les clics sur les liens, met à jour l'affichage en JavaScript pur, et utilise l'History API du navigateur pour modifier l'URL sans rechargement.

Du point de vue de l'utilisateur, la navigation semble instantanée — il n'y a pas de rechargement de page visible. L'application ressemble à une application mobile native. L'état (panier, données utilisateur, position de scroll) persiste entre les pages. Le header et le footer, communs à toutes les pages, ne sont jamais reconstruits — ils restent en place pendant que seul le contenu central change.

L'History API est l'interface du navigateur qui permet de manipuler l'historique de navigation et l'URL sans déclencher de rechargement. React Router, la bibliothèque standard de routage pour React, s'appuie entièrement sur cette API. Quand vous écrivez <Link to="/menu">, React Router appelle window.history.pushState() pour mettre à jour l'URL et affiche le composant correspondant sans aucune requête au serveur.

text
── Cycle de vie d'une navigation SPA ──────────────────────────

Chargement initial : https://darigo.ma
       │
       ▼
Navigateur : GET https://darigo.ma
Serveur : retourne index.html (minimal) + bundle JS (~300KB gzippé)
       │
       ▼
React démarre, affiche la page d'accueil
Tout le code de l'application est déjà en mémoire.
       │
       ▼
Utilisateur clique sur "Voir ma commande"
       │
       ▼
React Router intercepte le clic (preventDefault)
window.history.pushState({}, '', '/commande/1042')
  → URL mise à jour SANS rechargement
       │
       ▼
React Router affiche le composant PageCommande
  → fetch('https://api.darigo.ma/commandes/1042')
  → Données reçues, état mis à jour, composant re-rendu
       │
       ▼
Temps de navigation : 0-50ms (rendu JS immédiat)
+ temps du fetch API : 100-300ms
Aucun flash, aucune reconstruction du header/footer
État conservé (panier, utilisateur connecté)
javascript
// ── History API — ce que React Router utilise en interne ─────
// Naviguer sans rechargement
window.history.pushState({ page: 'commande' }, '', '/commande/1042');
// L'URL change dans la barre d'adresse — pas de rechargement

// Écouter les navigations arrière/avant (bouton ← du navigateur)
window.addEventListener('popstate', (event) => {
  console.log('Navigation vers :', window.location.pathname);
  afficherPagePourURL(window.location.pathname);
});

// ── React Router — abstraction de haut niveau sur History API ─
import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom';

const AppDariGo = () => (
  <BrowserRouter>
    <Header />  {/* Rendu UNE FOIS — jamais reconstruit */}

    <Routes>
      {/* Chaque Route affiche un composant différent selon l'URL */}
      <Route path="/"               element={<PageAccueil />} />
      <Route path="/menu"           element={<PageMenu />} />
      <Route path="/menu/:ville"    element={<PageMenu />} />
      <Route path="/commande/:id"   element={<PageCommande />} />
      <Route path="/suivi/:id"      element={<PageSuivi />} />
      <Route path="/profil"         element={<PageProfil />} />
      <Route path="*"               element={<Page404 />} />
    </Routes>

    <Footer />  {/* Rendu UNE FOIS — jamais reconstruit */}
  </BrowserRouter>
);

// Link — génère un <a> qui intercepte le clic et utilise History API
const NavDariGo = () => (
  <nav>
    <Link to="/">Accueil</Link>
    <Link to="/menu">Menu</Link>
    <Link to="/profil">Mon profil</Link>
  </nav>
);

// useNavigate — navigation programmatique depuis le code
const FormulaireCommande = () => {
  const navigate = useNavigate();

  const soumettre = async () => {
    const commande = await commandesService.creer(panier);
    // Après succès, naviguer vers la page de suivi
    navigate(`/suivi/${commande.id}`); // History API — pas de rechargement
  };

  return <button onClick={soumettre}>Confirmer ma commande</button>;
};

3
Comparaison SPA vs MPA — avantages, limites, choix
Performance · SEO · Premier chargement · Accessibilité · SSR
CritèreSPA (React)MPA (Laravel, Django...)
Expérience utilisateurNavigation instantanée, état persistant, expérience "app native"Rechargement complet à chaque page, flash blanc
Premier chargementLent — bundle JS complet à télécharger (~200KB-1MB)Rapide — HTML généré serveur, pas de JS requis pour voir le contenu
SEOProblématique — HTML initial vide, contenu généré en JS (invisible pour les moteurs de recherche basiques)Excellent — HTML complet dès la première requête
Performances réseauAprès le chargement initial, seules les données API transitent — économiqueChaque page = nouveau document HTML complet
ComplexitéPlus complexe — gestion du routing, de l'état, du cycle de vie côté clientPlus simple pour les CRUD basiques
Cas d'usage idéalApplications interactives : dashboards, réseaux sociaux, apps de livraison, SaaSSites de contenu, blogs, sites institutionnels, e-commerce simple

Le problème du SEO des SPAs et la solution SSR

Le principal inconvénient des SPAs est le référencement naturel (SEO). Quand Googlebot indexe la page darigo.ma/menu, il reçoit d'abord un HTML presque vide — juste un <div id="root"></div>. Même si Googlebot peut exécuter le JavaScript pour voir le contenu rendu, ce processus est moins fiable et plus lent que du HTML statique.

La solution est le Server Side Rendering (SSR) : générer le HTML complet sur le serveur à chaque requête, puis "hydrater" (reprendre le contrôle de) ce HTML côté client avec React. Next.js est le framework qui implémente cette approche par-dessus React. Pour DariGo, les pages de menu et de restaurant bénéficieraient du SSR pour le SEO, tandis que les pages commande et suivi (privées) resteraient des SPAs classiques.

ℹ DariGo — choix architectural

Pour ce cours, DariGo est une SPA React pure — c'est l'architecture la plus courante et celle qui couvre tous les concepts React fondamentaux. Dans un projet production réel, les pages publiques (menu, restaurants, promotions) seraient rendues en SSR avec Next.js pour le SEO et la performance mobile sur les réseaux marocains.


✓
Vérification des acquis
5 questions — corrigé intégré
Un utilisateur de DariGo est sur la page menu et clique sur "Ma commande". Dans une SPA React, combien de requêtes HTTP sont envoyées au serveur web principal (hors API) ?
Pourquoi le premier chargement d'une SPA est-il généralement plus lent que celui d'une MPA ?
Quel est le problème SEO principal des SPAs et comment le SSR le résout-il ?
Sur l'application DariGo SPA, l'utilisateur appuie sur le bouton ← (précédent) de son navigateur. Que se passe-t-il ?
Pour un nouveau site institutionnel de la Wilaya de Casablanca (contenu statique, aucune interactivité), quelle architecture choisiriez-vous et pourquoi ?
Partie 2 — Démarrer avec React React Virtual DOM · Composants · Écosystème
⏱ 18 min

Présentation de React & son écosystème

React est une bibliothèque JavaScript créée par Facebook en 2013 pour construire des interfaces utilisateur. Une bibliothèque — pas un framework complet. React ne gère que la couche vue. Tout le reste (routage, gestion d'état global, requêtes HTTP) est délégué à des bibliothèques tierces que vous choisissez et assemblez. C'est à la fois sa force — liberté totale — et sa principale difficulté pour les débutants : il faut comprendre l'écosystème pour faire les bons choix.


1
Pourquoi React — le problème qu'il résout
DOM impératif → UI déclarative · Synchronisation état/interface · Composabilité

Le problème fondamental du développement front-end avant React : synchroniser l'interface avec les données. Dans une application de livraison comme DariGo, les données changent constamment — le statut d'une commande passe de "en attente" à "en préparation" à "livrée", le panier se remplit et se vide, la position du livreur se met à jour toutes les secondes. Chaque changement de données doit se refléter immédiatement dans l'interface.

Avec JavaScript pur ou jQuery, le développeur doit gérer cette synchronisation manuellement : trouver les éléments DOM à modifier, les mettre à jour, s'assurer de ne rien oublier. Sur une application simple c'est faisable. Sur une application avec des dizaines d'états interdépendants et des centaines d'éléments d'interface, c'est un cauchemar de maintenance.

React inverse le paradigme. Plutôt que de manipuler le DOM manuellement (impératif — "comment faire"), le développeur décrit à quoi l'interface doit ressembler pour un état donné (déclaratif — "ce que je veux"). React se charge de calculer les différences et de mettre à jour le DOM automatiquement.

Les trois idées fondatrices de React

1. Composants — L'interface est découpée en pièces réutilisables et indépendantes, chacune responsable de son propre rendu. La page menu de DariGo est composée d'un composant Header, d'une barre de filtres, d'une grille de CartePlat, d'un composant PanierResume. Chaque CartePlat est lui-même composé d'une image, d'un titre, d'un prix et d'un bouton. Cette composition peut aller aussi profondément que nécessaire.

2. Flux de données unidirectionnel — Les données circulent dans un seul sens : du parent vers l'enfant, via les props. Un composant enfant ne peut pas modifier directement les données d'un parent — il notifie le parent via une fonction de callback, et c'est le parent qui met à jour son état. Ce flux prévisible rend les applications React faciles à déboguer.

3. Virtual DOM — Pour éviter les modifications DOM coûteuses, React maintient une copie légère du DOM en mémoire JavaScript (le Virtual DOM). Quand l'état change, React construit un nouveau Virtual DOM, compare avec l'ancien (algorithme de diffing), et n'applique au DOM réel que les changements minimaux nécessaires. C'est ce qui rend React performant même avec des interfaces complexes.

javascript
// ── APPROCHE IMPÉRATIVE (JavaScript pur) ─────────────────────
// Le statut d'une commande DariGo passe à "livree"
// Le développeur doit savoir COMMENT mettre à jour chaque élément DOM

function mettreAJourStatutLivree(idCommande) {
  const badge = document.querySelector(`#commande-${idCommande} .badge-statut`);
  const btnAnnuler = document.querySelector(`#commande-${idCommande} .btn-annuler`);
  const message = document.querySelector(`#commande-${idCommande} .message-statut`);
  const timeline = document.querySelector(`#commande-${idCommande} .timeline`);

  badge.textContent = 'Livrée';
  badge.className = 'badge-statut badge-success';
  btnAnnuler.style.display = 'none';
  message.textContent = 'Votre commande a bien été livrée. Bon appétit !';
  timeline.querySelector('[data-etape="livree"]').classList.add('etape-active');

  // Et si on oublie de mettre à jour un élément ? Bug silencieux.
  // Et si le DOM a changé entre-temps ? Erreur null pointer.
}

// ── APPROCHE DÉCLARATIVE (React) ─────────────────────────────
// Le développeur décrit CE QUE l'interface doit afficher
// React se charge du COMMENT mettre à jour le DOM

const CarteCommande = ({ commande }) => {
  // Je décris l'interface pour chaque état possible de commande.statut
  // React synchronise le DOM automatiquement quand commande change
  return (
    <div id={`commande-${commande.id}`} className="carte-commande">
      <span className={`badge-statut ${commande.statut === 'livree' ? 'badge-success' : 'badge-warning'}`}>
        {commande.statut === 'livree' ? 'Livrée' : 'En cours'}
      </span>

      {commande.statut !== 'livree' && (
        <button className="btn-annuler">Annuler</button>
      )}

      <p className="message-statut">
        {commande.statut === 'livree'
          ? 'Votre commande a bien été livrée. Bon appétit !'
          : `En cours — ETA : ${commande.eta} minutes`}
      </p>

      <Timeline commande={commande} />
    </div>
  );
};
// Quand commande.statut change → React recalcule et met à jour automatiquement.

2
L'écosystème React — bibliothèques standard
React Router · Redux · React Query · Axios · styled-components · Testing

React seul ne gère que le rendu de composants. Pour construire une application complète comme DariGo, il faut assembler des bibliothèques complémentaires. Voici les choix les plus courants en 2025, ceux que vous rencontrerez dans les entreprises marocaines et internationales.

BesoinBibliothèque standardAlternativeUsage DariGo
RoutageReact Router v6TanStack RouterNavigation entre pages Menu, Commande, Suivi, Profil
État globalRedux ToolkitZustand, Jotai, Context APIPanier partagé, utilisateur connecté, notifications
Requêtes serveurTanStack Query (React Query)SWR, RTK QueryChargement plats, commandes avec cache et synchronisation
FormulairesReact Hook FormFormikFormulaire de commande, inscription, adresse de livraison
HTTP clientAxiosFetch natifAppels API DariGo avec intercepteurs et gestion d'erreurs
CSS-in-JSstyled-componentsEmotion, CSS ModulesComposants stylés réutilisables
UI Componentsshadcn/uiMUI, Ant Design, ChakraBoutons, modals, toasts, formulaires
TestsJest + Testing LibraryVitestTests unitaires des composants et des services
BuildViteCreate React App (déprécié)Serveur dev + build production
ℹ React 18 et React 19 — ce qui change

React 18 (mars 2022) — version stable actuelle dans la majorité des projets en production : Concurrent rendering, automatic batching, useTransition, useDeferredValue, Suspense amélioré.

React 19 (déc. 2024) — nouvelles APIs : Server Components (rendu côté serveur sans Next.js), Actions (gestion des mutations asynchrones), use() hook, useFormStatus, useOptimistic. Ce cours est basé sur React 18 avec des callouts dédiés React 19 quand les APIs divergent.


3
Le Virtual DOM — fonctionnement interne
Diffing · Reconciliation · Fiber · Clé de liste

Le Virtual DOM est l'une des innovations les plus importantes de React. Comprendre son fonctionnement permet de comprendre les bonnes pratiques React — notamment pourquoi la prop key est obligatoire dans les listes et pourquoi on ne doit jamais muter l'état directement.

Le cycle de rendu React

Quand l'état d'un composant change (via setState ou useState), React déclenche un cycle de rendu en trois étapes :

1. Render — React appelle la fonction du composant pour obtenir le nouveau JSX. Cette étape est pure — pas d'effets de bord, pas de modifications DOM. React construit un nouvel arbre Virtual DOM en mémoire.

2. Reconciliation (Diffing) — React compare le nouvel arbre Virtual DOM avec l'ancien (algorithme de diffing). Il cherche les différences minimales : quels nœuds ont été ajoutés, supprimés ou modifiés.

3. Commit — React applique uniquement les changements identifiés au DOM réel. C'est la seule étape qui touche le DOM — et elle est aussi minimale que possible.

Pourquoi la prop key est critique dans les listes

Quand React compare deux listes, il utilise la prop key pour identifier chaque élément de manière unique. Sans key, React compare les éléments par position — si vous ajoutez un élément au début de la liste, React pense que tous les éléments ont changé et recrée tout. Avec une key stable (l'id de l'objet), React identifie exactement ce qui a changé et ne met à jour que ce qui est nécessaire.

javascript
// ── Visualiser le cycle de rendu React ───────────────────────
const SuiviCommande = ({ idCommande }) => {
  const [commande, setCommande] = useState(null);
  const [statut, setStatut] = useState('en_attente');

  // ÉTAPE 1 — Render : React appelle cette fonction
  // Elle retourne du JSX (Virtual DOM) — aucun DOM réel touché ici
  console.log('Render — statut:', statut);

  // ÉTAPE 3 — Commit : useEffect s'exécute APRÈS que le DOM soit mis à jour
  useEffect(() => {
    console.log('Commit — DOM mis à jour avec statut:', statut);

    // Simuler une mise à jour temps réel du statut de livraison
    const interval = setInterval(async () => {
      const data = await commandesService.getById(idCommande);
      setStatut(data.statut); // Déclenche un nouveau cycle Render→Diff→Commit
    }, 5000);

    return () => clearInterval(interval);
  }, [idCommande]);

  return (
    <div className="suivi-commande">
      <h2>Commande #{idCommande}</h2>
      <BadgeStatut statut={statut} />
      {/* React ne re-rend que BadgeStatut si statut change — pas toute la page */}
    </div>
  );
};

// ── Impact de key sur la performance des listes ───────────────
// Scénario : le menu DariGo — ajout d'un plat spécial en tête de liste

// SANS key stable — React compare par position :
// Ancien : [Tajine(pos0), Couscous(pos1), Harira(pos2)]
// Nouveau : [Méchoui(pos0), Tajine(pos1), Couscous(pos2), Harira(pos3)]
// React pense que TOUS les éléments ont changé → recrée 4 éléments DOM
{plats.map((p, index) => <CartePlat key={index} {...p} />)} // MAUVAIS

// AVEC key={p.id} — React compare par identifiant :
// Tajine(id:1) existe toujours → DOM inchangé
// Couscous(id:2) existe toujours → DOM inchangé
// Harira(id:3) existe toujours → DOM inchangé
// Méchoui(id:99) est nouveau → React crée UN seul nouveau nœud DOM
{plats.map(p => <CartePlat key={p.id} {...p} />)} // CORRECT

✓
Vérification des acquis
5 questions — corrigé intégré
React est souvent décrit comme une "bibliothèque" et non un "framework". Quelle est la conséquence pratique de cette distinction pour un projet comme DariGo ?
Dans quel ordre React effectue-t-il les étapes lors d'un changement d'état ?
On ajoute un nouveau plat en tête du tableau d'état plats de DariGo. Combien d'éléments DOM React doit-il créer/modifier si on utilise key={plat.id} ?
Quelle est la différence entre l'approche impérative (jQuery/JS pur) et l'approche déclarative (React) pour gérer l'interface ?
Pour l'application DariGo, quelle bibliothèque choisiriez-vous pour gérer le panier partagé entre la page menu, la page commande et le header ?
Partie 2 — Démarrer avec React Vite npm · create-react-app · Structure projet
⏱ 20 min

Installation & premier projet — Vite & CRA

Il existe deux façons standards de démarrer un projet React : Vite — l'outil recommandé en 2025, ultra-rapide, avec un serveur de développement quasi-instantané — et Create React App (CRA) — l'outil historique de Facebook, aujourd'hui déprécié mais encore présent dans de nombreux projets existants. Vous devez connaître les deux. Cette section vous guide pas à pas de l'installation de Node.js jusqu'au premier composant affiché dans le navigateur.


1
Prérequis — Node.js et npm
Node.js LTS · npm · Vérification · nvm

React s'exécute dans le navigateur, mais les outils de développement (Vite, Babel, ESLint, les scripts npm) s'exécutent dans Node.js — l'environnement d'exécution JavaScript côté serveur. Node.js n'est pas nécessaire en production pour une SPA React (le code compilé est du HTML/CSS/JS statique), mais il est indispensable pendant le développement.

Installez toujours la version LTS (Long Term Support) de Node.js — elle est stable et supportée pendant plusieurs années. En mai 2025, la version LTS est Node.js 22.x. Évitez les versions impaires (21, 23) qui sont des versions de développement avec un support court.

bash
# ── Vérifier que Node.js et npm sont installés ────────────────
node --version   # doit afficher v22.x.x (ou v20.x.x minimum)
npm --version    # doit afficher 10.x.x ou supérieur

# ── Installer Node.js ─────────────────────────────────────────
# Windows / Mac : télécharger l'installeur LTS sur https://nodejs.org
# Linux (Ubuntu/Debian) :
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

# ── nvm — gérer plusieurs versions de Node.js (recommandé) ───
# Utile pour jongler entre projets qui nécessitent des versions différentes
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

nvm install 22        # Installer Node.js 22 LTS
nvm use 22            # Utiliser Node.js 22 dans le terminal courant
nvm alias default 22  # Définir comme version par défaut

2
Créer un projet avec Vite — méthode recommandée
npm create vite · Structure · main.jsx · App.jsx · Serveur dev

Vite (prononcé "vite" comme en français — vitesse) est un outil de build créé par Evan You (créateur de Vue.js) qui utilise les modules ES natifs du navigateur pendant le développement. Résultat : démarrage du serveur en moins d'une seconde et rechargement à chaud (HMR) quasi-instantané. C'est l'outil standard recommandé par l'équipe React depuis 2023 après la dépréciation de Create React App.

bash
# ── Créer le projet DariGo avec Vite ─────────────────────────
npm create vite@latest darigo-frontend -- --template react
# darigo-frontend = nom du dossier
# --template react = template JavaScript (sans TypeScript)
# Pour TypeScript : --template react-ts

# Autres templates disponibles :
# react          JavaScript pur
# react-ts       TypeScript
# react-swc      JavaScript + SWC (transpileur Rust, encore plus rapide)
# react-swc-ts   TypeScript + SWC

# ── Installer les dépendances et démarrer ─────────────────────
cd darigo-frontend
npm install          # Installe toutes les dépendances (react, react-dom, vite...)
npm run dev          # Démarre le serveur de développement

# Le serveur démarre sur http://localhost:5173 (port par défaut de Vite)
# Modification d'un fichier → rechargement instantané dans le navigateur

# ── Autres commandes utiles ───────────────────────────────────
npm run build        # Compiler pour la production → dossier dist/
npm run preview      # Prévisualiser le build de production en local
npm run lint         # Analyser le code avec ESLint
bash
# ── Structure du projet après npm create vite ─────────────────
darigo-frontend/
│
├── public/
│   └── vite.svg               # Assets servis sans traitement (favicon, robots.txt...)
│
├── src/
│   ├── assets/
│   │   └── react.svg          # Assets importés dans le JS (optimisés par Vite)
│   ├── App.css                # Styles du composant App
│   ├── App.jsx                # Composant racine de l'application
│   ├── index.css              # Styles globaux
│   └── main.jsx               # Point d'entrée — monte React dans le DOM
│
├── index.html                 # Template HTML — Vite injecte le JS compilé ici
├── package.json               # Dépendances et scripts
├── package-lock.json          # Versions exactes installées (commiter dans Git)
├── vite.config.js             # Configuration Vite
└── .eslintrc.cjs              # Règles ESLint
javascript
// ── index.html — le seul fichier HTML du projet SPA ──────────
// Vite injecte automatiquement la balise <script> pour main.jsx
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DariGo — Livraison de plats marocains</title>
  </head>
  <body>
    <div id="root"></div>  <!-- React monte l'application ici -->
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

// ── src/main.jsx — point d'entrée de l'application ───────────
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

// React 18 — createRoot() remplace l'ancien ReactDOM.render()
// createRoot active le Concurrent Mode de React 18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    {/* StrictMode détecte les problèmes potentiels en développement */}
    {/* En production, StrictMode est automatiquement désactivé */}
    <App />
  </React.StrictMode>
);

// ── src/App.jsx — composant racine à remplacer pour DariGo ───
import { useState } from 'react';
import './App.css';

const App = () => {
  return (
    <div className="app">
      <h1>DariGo — Livraison de plats marocains</h1>
      <p>Tajine, Couscous, Harira livrés chez vous à Casablanca</p>
    </div>
  );
};

export default App;

3
Create React App — connaître le legacy
CRA · Webpack · Déprécié · Différences avec Vite

Create React App (CRA) a été l'outil officiel de Facebook de 2016 à 2023. Il est aujourd'hui déprécié — plus maintenu activement. Mais il est encore présent dans des millions de projets en production que vous serez amené à maintenir. Il faut donc le connaître, comprendre ses différences avec Vite, et savoir migrer.

CritèreViteCreate React App (CRA)
BundlerRollup (prod) + ESBuild (dev)Webpack
Démarrage serveur dev< 1 seconde15-60 secondes selon projet
HMR (rechargement à chaud)Quasi-instantané1-10 secondes
Port par défaut51733000
Config exposéevite.config.js simpleCachée (ou eject pour l'exposer)
StatutActif — recommandé 2025Déprécié depuis 2023
Extension fichiers.jsx ou .js.js ou .jsx
bash
# ── CRA — commandes de référence (projets legacy) ─────────────
npx create-react-app darigo-frontend          # JavaScript
npx create-react-app darigo-frontend --template typescript  # TypeScript

cd darigo-frontend
npm start          # Serveur dev sur http://localhost:3000
npm run build      # Build production → dossier build/ (pas dist/ comme Vite)
npm test           # Lance Jest en mode watch

# ── Différence de structure CRA vs Vite ──────────────────────
# CRA génère :
darigo-frontend/
├── public/
│   ├── index.html    # Template HTML (dans public/ — différent de Vite)
│   └── ...
├── src/
│   ├── App.js        # Extension .js (pas .jsx par défaut dans CRA)
│   ├── App.test.js   # Test Jest généré automatiquement
│   ├── index.js      # Point d'entrée (pas main.jsx)
│   └── ...
└── package.json

# Vite génère :
darigo-frontend/
├── index.html        # À la RACINE — pas dans public/
├── src/
│   ├── App.jsx       # Extension .jsx par convention
│   ├── main.jsx      # Point d'entrée
│   └── ...
└── vite.config.js    # Config visible et modifiable facilement

4
Initialiser DariGo — projet complet
Structure professionnelle · Dépendances · vite.config · alias
bash
# ── Créer et configurer DariGo ───────────────────────────────
npm create vite@latest darigo-frontend -- --template react
cd darigo-frontend
npm install

# Installer les dépendances du projet complet
npm install react-router-dom          # Routage
npm install @reduxjs/toolkit react-redux  # État global
npm install axios                     # Requêtes HTTP
npm install react-hook-form           # Formulaires
npm install @tanstack/react-query     # Cache et synchro serveur

# Dépendances de développement
npm install -D eslint-plugin-react-hooks  # Règles ESLint pour les hooks
javascript
// ── vite.config.js — configuration recommandée pour DariGo ───
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      // Alias pour éviter les imports relatifs profonds
      // Avant : import CartePlat from '../../../components/CartePlat'
      // Après : import CartePlat from '@components/CartePlat'
      '@'          : path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@pages'     : path.resolve(__dirname, './src/pages'),
      '@services'  : path.resolve(__dirname, './src/services'),
      '@hooks'     : path.resolve(__dirname, './src/hooks'),
      '@store'     : path.resolve(__dirname, './src/store'),
      '@utils'     : path.resolve(__dirname, './src/utils'),
    }
  },
  server: {
    port: 3000,   // Changer le port par défaut si nécessaire
    proxy: {
      // Proxy les requêtes /api vers le backend pour éviter les erreurs CORS en dev
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});

✓
Vérification des acquis
5 questions — corrigé intégré
Pourquoi Vite démarre-t-il beaucoup plus rapidement que CRA (Create React App) ?
Dans un projet Vite React, où se trouve le fichier index.html par rapport à CRA ?
Pourquoi React 18 utilise ReactDOM.createRoot() à la place de l'ancien ReactDOM.render() ?
Qu'est-ce que React.StrictMode et quel est son impact en production ?
Dans vite.config.js, on configure l'alias '@components': path.resolve(__dirname, './src/components'). Quel en est l'avantage concret ?
Partie 2 — Démarrer avec React JSX Babel · React.createElement · Expressions · Règles
⏱ 22 min

Comprendre le JSX

JSX (JavaScript XML) est la syntaxe qui permet d'écrire du HTML à l'intérieur de JavaScript. C'est une extension syntaxique de JavaScript — pas du HTML, pas du template engine, du JavaScript pur avec une notation ressemblant à du HTML. Babel le compile en appels React.createElement() avant que le navigateur l'exécute. Comprendre ce que JSX devient après compilation est fondamental pour comprendre pourquoi certaines règles existent et éviter des erreurs qui n'ont pas de message d'erreur explicite.


1
JSX — ce que Babel génère réellement
React.createElement · Arbre de nœuds · Propriétés · Enfants

Quand Babel voit du JSX, il le transforme en appels imbriqués à React.createElement(type, props, ...enfants). Cette fonction crée un objet JavaScript simple — un nœud du Virtual DOM. Ce n'est pas du HTML, ce ne sont pas des vrais éléments DOM — c'est une description JavaScript de ce que React doit créer dans le DOM réel.

Comprendre cette transformation explique immédiatement plusieurs règles JSX qui semblent arbitraires : pourquoi il faut un seul élément racine (un appel de fonction ne peut retourner qu'une valeur), pourquoi class devient className (class est un mot réservé JavaScript), pourquoi for devient htmlFor, et pourquoi on utilise camelCase pour les attributs.

javascript
// ── JSX → React.createElement() : la transformation Babel ────

// CODE JSX que vous écrivez :
const CartePlat = ({ nom, prix, disponible }) => (
  <div className={`carte-plat ${disponible ? '' : 'grise'}`}>
    <h3>{nom}</h3>
    <p className="prix">{prix} MAD</p>
    <button disabled={!disponible}>
      {disponible ? 'Ajouter' : 'Indisponible'}
    </button>
  </div>
);

// CE QUE BABEL GÉNÈRE (React 17+ avec le nouveau transform JSX) :
// Note: depuis React 17, l'import React n'est plus obligatoire pour le JSX
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';

const CartePlat = ({ nom, prix, disponible }) =>
  _jsxs('div', {
    className: `carte-plat ${disponible ? '' : 'grise'}`,
    children: [
      _jsx('h3', { children: nom }),
      _jsxs('p', { className: 'prix', children: [prix, ' MAD'] }),
      _jsx('button', {
        disabled: !disponible,
        children: disponible ? 'Ajouter' : 'Indisponible'
      })
    ]
  });

// ── L'objet Virtual DOM résultant ────────────────────────────
// React.createElement retourne un objet JavaScript simple :
{
  type: 'div',
  props: {
    className: 'carte-plat',
    children: [
      { type: 'h3', props: { children: 'Tajine Kefta' }, ... },
      { type: 'p', props: { className: 'prix', children: '75 MAD' }, ... },
      { type: 'button', props: { disabled: false, children: 'Ajouter' }, ... }
    ]
  },
  key: null,
  ref: null,
  // ...autres métadonnées React
}
// Ce sont ces objets que React compare (Diffing) pour déterminer
// ce qui a changé dans le DOM réel.

2
Règles JSX — toutes les différences avec HTML
className · htmlFor · camelCase · Auto-fermeture · Racine unique · Fragment

JSX ressemble à du HTML mais ce n'en est pas. Plusieurs différences importantes à connaître pour ne pas passer des heures à chercher pourquoi un style ne s'applique pas ou pourquoi un label ne fonctionne pas.

javascript
// ── RÈGLE 1 : className au lieu de class ─────────────────────
// class est un mot réservé JavaScript (pour les classes ES6)
<div class="carte">...</div>         // FAUX — warning React
<div className="carte">...</div>     // CORRECT

// ── RÈGLE 2 : htmlFor au lieu de for ─────────────────────────
// for est un mot réservé JavaScript (boucles)
<label for="email">Email</label>     // FAUX
<label htmlFor="email">Email</label> // CORRECT

// ── RÈGLE 3 : attributs en camelCase ─────────────────────────
// Les attributs HTML avec tirets deviennent camelCase en JSX
<div tabindex="1"></div>             // FAUX
<div tabIndex={1}></div>             // CORRECT

<input maxlength="50" />             // FAUX
<input maxLength={50} />             // CORRECT

// Exception : les attributs data-* et aria-* gardent leur tiret
<div data-testid="carte-plat" aria-label="Tajine Kefta">...</div> // CORRECT

// ── RÈGLE 4 : fermeture obligatoire de toutes les balises ─────
// En HTML, <br>, <img>, <input> n'ont pas de balise fermante
<br>                                 // FAUX en JSX
<br />                               // CORRECT

<img src={platImg} alt="Tajine">     // FAUX
<img src={platImg} alt="Tajine" />   // CORRECT

<input type="text" placeholder="Rechercher...">  // FAUX
<input type="text" placeholder="Rechercher..." /> // CORRECT

// ── RÈGLE 5 : un seul élément racine ─────────────────────────
// Un composant React ne peut retourner qu'un seul élément racine
// (car Babel génère un seul appel React.createElement au niveau supérieur)

// FAUX — deux éléments racines :
const CartePlat = () => (
  <h3>Tajine Kefta</h3>
  <p>75 MAD</p>
);
// SyntaxError: Adjacent JSX elements must be wrapped in an enclosing tag

// SOLUTION 1 : wrapper div
const CartePlat = () => (
  <div>
    <h3>Tajine Kefta</h3>
    <p>75 MAD</p>
  </div>
);

// SOLUTION 2 : Fragment — sans div supplémentaire dans le DOM
const CartePlat = () => (
  <>
    <h3>Tajine Kefta</h3>
    <p>75 MAD</p>
  </>
);
// <>...</> est le shorthand de <React.Fragment>...</React.Fragment>
// Un Fragment ne crée aucun élément DOM — il regroupe logiquement les enfants

// Fragment avec key (nécessaire dans les listes) :
plats.map(p => (
  <React.Fragment key={p.id}>
    <dt>{p.nom}</dt>
    <dd>{p.prix} MAD</dd>
  </React.Fragment>
));

3
Expressions JavaScript dans le JSX
{ } · Variables · Ternaire · && · map() · Styles inline · Commentaires

Dans le JSX, les accolades { } permettent d'insérer n'importe quelle expression JavaScript — une valeur qui peut être évaluée à un résultat. Une expression retourne une valeur : variable, calcul, appel de fonction, opérateur ternaire. Ce qui n'est pas une expression (les instructions if, les boucles for, les déclarations const) ne peut pas être utilisé directement dans le JSX.

javascript
const plat = {
  id: 1,
  nom: "Tajine Kefta",
  prix: 75,
  note: 4.8,
  disponible: true,
  restaurant: "La Sqala, Casablanca",
  image: "/images/tajine-kefta.jpg"
};
const remise = 10; // %

const CartePlatComplete = ({ plat }) => (
  <div className="carte-plat">

    {/* ── Variables ── */}
    <h3>{plat.nom}</h3>
    <p>{plat.restaurant}</p>

    {/* ── Expressions arithmétiques ── */}
    <p>Prix original : {plat.prix} MAD</p>
    <p>Prix remisé : {plat.prix * (1 - remise / 100)} MAD</p>
    <p>TTC : {(plat.prix * 1.2).toFixed(2)} MAD</p>

    {/* ── Appels de méthodes ── */}
    <p>Note : {plat.note.toFixed(1)} / 5</p>
    <p>Nom en majuscules : {plat.nom.toUpperCase()}</p>

    {/* ── Opérateur ternaire — affichage conditionnel ── */}
    <span className={plat.disponible ? "badge-vert" : "badge-rouge"}>
      {plat.disponible ? "Disponible" : "Indisponible"}
    </span>

    {/* ── && — afficher uniquement si condition vraie ── */}
    {plat.disponible && (
      <button className="btn-ajouter">Ajouter au panier</button>
    )}
    {!plat.disponible && (
      <p className="msg-indispo">Disponible demain</p>
    )}

    {/* ── Attention au && avec 0 ── */}
    {/* PIÈGE : 0 && ... affiche "0" dans le DOM — pas rien ! */}
    {panier.length && <span>{panier.length} articles</span>}
    {/* Si panier est vide (length = 0), React affiche "0" dans le DOM */}
    {/* CORRECTION : forcer un booléen */}
    {panier.length > 0 && <span>{panier.length} articles</span>}
    {/* ou : */}
    {!!panier.length && <span>{panier.length} articles</span>}

    {/* ── Styles inline — objet JavaScript, pas une string CSS ── */}
    <div style={{ backgroundColor: '#2ecc71', padding: '8px 16px', borderRadius: 8 }}>
      {/* Double accolades : la première = expression JSX, la seconde = objet JS */}
      {/* Les propriétés CSS sont en camelCase : background-color → backgroundColor */}
    </div>

    {/* ── Commentaires JSX — dans des accolades ── */}
    {/* Ceci est un commentaire JSX — visible dans le code source, pas dans le DOM */}

    {/* ── Ce qui N'EST PAS possible directement dans JSX ── */}
    {/* if/else — utiliser le ternaire ou && */}
    {/* for — utiliser map() */}
    {/* const/let/var — déclarer en dehors du return */}

    {/* ── map() pour rendre des listes ── */}
    <ul className="tags">
      {['halal', 'marocain', 'traditionnel'].map(tag => (
        <li key={tag} className="tag">{tag}</li>
      ))}
    </ul>

  </div>
);

// ── NE PAS mettre d'instructions dans JSX ────────────────────
// FAUX — if n'est pas une expression
const CartePlat = ({ plat }) => (
  <div>
    {if (plat.disponible) { return <button>Ajouter</button> }}  // SyntaxError !
  </div>
);

// CORRECT — ternaire ou &&
const CartePlat = ({ plat }) => (
  <div>
    {plat.disponible ? <button>Ajouter</button> : null}
  </div>
);

4
JSX et les composants — majuscule, props, composition
Majuscule · Composants comme balises · Composition · Children

En JSX, la casse de la première lettre détermine si React traite l'élément comme un élément HTML natif ou comme un composant React. Les éléments en minuscules (<div>, <p>, <button>) sont des éléments HTML. Les éléments commençant par une majuscule (<CartePlat>, <Header>) sont des composants React.

Cette convention est obligatoire : si vous nommez un composant cartePlat et l'utilisez comme <cartePlat />, React le traite comme un élément HTML inconnu et ne l'affiche pas correctement.

javascript
// ── Majuscule obligatoire pour les composants ─────────────────
const cartePlat = ({ nom }) => <h3>{nom}</h3>;  // MAUVAIS nom
<cartePlat nom="Tajine" />  // React pense que c'est une balise HTML inconnue

const CartePlat = ({ nom }) => <h3>{nom}</h3>;  // CORRECT
<CartePlat nom="Tajine" />  // React sait que c'est un composant

// ── Passer des props — attributs avec valeurs ─────────────────
// String — guillemets directs
<CartePlat nom="Tajine Kefta" restaurant="La Sqala" />

// Expression JavaScript — accolades
<CartePlat nom={plat.nom} prix={plat.prix} note={4.8} disponible={true} />

// Booléen true — shorthand (sans ={true})
<CartePlat disponible />         // équivalent à disponible={true}
<CartePlat disponible={false} /> // false doit être explicite

// Spread des props — passer tout l'objet
<CartePlat {...plat} />
// équivalent à : <CartePlat id={plat.id} nom={plat.nom} prix={plat.prix} ... />

// ── La prop children — contenu entre les balises ─────────────
const Card = ({ titre, children }) => (
  <div className="card">
    <h2>{titre}</h2>
    <div className="card-body">{children}</div>
  </div>
);

// Utilisation — tout ce qui est entre les balises devient children
<Card titre="Menu DariGo">
  <CartePlat {...tajine} />
  <CartePlat {...couscous} />
  <CartePlat {...harira} />
</Card>

// ── Composition — assembler des composants ────────────────────
const PageMenu = ({ ville }) => {
  const [plats, setPlats] = useState([]);
  const [panier, setPanier] = useState([]);

  return (
    <div className="page-menu">
      <Header titre={`Menu DariGo — ${ville}`} nbArticles={panier.length} />

      <main>
        <FiltresMenu onFiltrer={setFiltres} />

        <div className="grille-plats">
          {plats.map(p => (
            <CartePlat
              key={p.id}
              {...p}
              onAjouter={() => setPanier(prev => [...prev, p])}
            />
          ))}
        </div>
      </main>

      <PanierFlottant panier={panier} onValider={passerCommande} />
    </div>
  );
};

✓
Vérification des acquis
5 questions — corrigé intégré
Pourquoi utilise-t-on className au lieu de class en JSX ?
On écrit dans un composant DariGo : {commandes.length && <ListeCommandes data={commandes} />}. Que s'affiche-t-il quand commandes est un tableau vide ?
Un composant Modal doit afficher du contenu variable selon l'endroit où il est utilisé. Comment passer ce contenu dynamique ?
Pourquoi un composant React doit-il retourner un seul élément racine ?
Comment appliquer un style inline conditionnel en JSX — par exemple, colorer en rouge le prix si supérieur à 100 MAD ?
Partie 3 — Composants, Props & State Composants Fonctionnels · Classe · Pure · HOC
⏱ 20 min

Composants fonctionnels & de classe

Un composant React est la brique de base de toute interface. C'est une fonction (ou une classe) qui reçoit des données en entrée et retourne du JSX décrivant ce qui doit s'afficher. Toute l'architecture de DariGo — le header, les cartes de plats, le panier, le formulaire de commande, le suivi de livraison — est une hiérarchie de composants imbriqués. Maîtriser ce que fait un composant, quand le découper, et comment le nommer est la compétence centrale du développeur React.


1
Composant fonctionnel — la forme standard
Fonction pure · JSX · Props · Export · Convention de nommage

Un composant fonctionnel est une fonction JavaScript qui reçoit un objet props et retourne du JSX. C'est la forme recommandée depuis React 16.8 et l'introduction des hooks. La règle fondamentale : un composant fonctionnel doit se comporter comme une fonction pure pour ses props — même props en entrée = même JSX en sortie, sans effets de bord dans le corps de la fonction.

La convention de nommage est stricte : PascalCase obligatoire. React distingue les composants (CartePlat) des éléments HTML natifs (div, button) par la casse. Un composant en minuscules est traité comme une balise HTML inconnue.

javascript
// ── Composant minimal — forme la plus simple ──────────────────
const Bienvenue = () => <h1>Marhba bik sur DariGo !</h1>;

// ── Composant avec props ──────────────────────────────────────
const CartePlat = ({ nom, prix, disponible, note, restaurant }) => (
  <article className={`carte-plat ${disponible ? '' : 'carte-plat--grise'}`}>
    <header className="carte-plat__header">
      <h3 className="carte-plat__nom">{nom}</h3>
      <span className="carte-plat__note">{note.toFixed(1)} ★</span>
    </header>
    <p className="carte-plat__restaurant">{restaurant}</p>
    <footer className="carte-plat__footer">
      <strong className="carte-plat__prix">{prix} MAD</strong>
      {disponible
        ? <button className="btn btn-primary">Ajouter</button>
        : <span className="badge badge-gris">Indisponible</span>
      }
    </footer>
  </article>
);

// ── Composants imbriqués — composition ────────────────────────
const BadgeStatut = ({ statut }) => {
  const config = {
    en_attente:    { label: 'En attente',    classe: 'warning' },
    en_preparation:{ label: 'En préparation',classe: 'info'    },
    en_livraison:  { label: 'En livraison',  classe: 'primary' },
    livree:        { label: 'Livrée',        classe: 'success' },
    annulee:       { label: 'Annulée',       classe: 'danger'  },
  };
  const { label, classe } = config[statut] || { label: statut, classe: 'default' };
  return <span className={`badge badge-${classe}`}>{label}</span>;
};

const CarteCommande = ({ id, client, total, statut, dateCommande }) => (
  <div className="carte-commande">
    <div className="carte-commande__header">
      <span className="carte-commande__id">Commande #{id}</span>
      <BadgeStatut statut={statut} />   {/* composant imbriqué */}
    </div>
    <p className="carte-commande__client">{client}</p>
    <p className="carte-commande__date">
      {new Date(dateCommande).toLocaleDateString('fr-MA')}
    </p>
    <strong className="carte-commande__total">{total} MAD</strong>
  </div>
);

// ── Composant avec logique interne ────────────────────────────
const FormateurPrix = ({ prix, remise = 0, afficherTTC = false }) => {
  const prixRemise = remise > 0 ? prix * (1 - remise / 100) : prix;
  const prixFinal  = afficherTTC ? prixRemise * 1.2 : prixRemise;
  const label      = afficherTTC ? 'TTC' : 'HT';

  return (
    <div className="prix-wrapper">
      {remise > 0 && (
        <span className="prix-barre">{prix} MAD</span>
      )}
      <span className="prix-principal">
        {prixFinal.toFixed(2)} MAD {label}
      </span>
      {remise > 0 && (
        <span className="badge-remise">-{remise}%</span>
      )}
    </div>
  );
};

// Utilisation
<FormateurPrix prix={90} remise={10} afficherTTC />
// Affiche : ~~90 MAD~~   81.00 MAD TTC   -10%

2
Quand et comment découper en composants
Principe de responsabilité unique · Réutilisabilité · Granularité · Nommage

Savoir quand créer un nouveau composant est une compétence qui s'acquiert avec l'expérience. Deux principes guident cette décision :

Principe de responsabilité unique : un composant fait une chose et la fait bien. Si vous vous trouvez à décrire un composant avec "et", il fait probablement trop de choses. CartePlat affiche un plat — c'est tout. Elle ne gère pas le panier, elle ne fait pas de requête réseau, elle n'affiche pas les avis clients.

Réutilisabilité : si le même JSX apparaît deux fois dans votre code avec des données différentes, c'est un composant. Les cartes de plats du menu, les lignes du panier, les badges de statut — tous répétitifs, tous candidats à être des composants.

javascript
// ── Décomposition de la page Menu DariGo ─────────────────────
//
// PageMenu
// ├── Header           (titre, logo, lien panier)
// ├── BarreRecherche   (input texte, bouton clear)
// ├── FiltresCategorie (boutons Tous / Plat / Entrée / Soupe / Dessert)
// ├── GrillePlats
// │   ├── CartePlat    × N
// │   │   ├── ImagePlat
// │   │   ├── InfosPlat   (nom, restaurant, note)
// │   │   └── ActionPlat  (prix + bouton ajouter)
// │   └── MessageVide  (si aucun résultat)
// └── PanierFlottant
//     ├── LignePanier  × N
//     └── TotalPanier

// ── Chaque composant = un fichier dans src/components/ ────────

// src/components/BarreRecherche.jsx
const BarreRecherche = ({ valeur, onChange, placeholder = "Chercher un plat..." }) => (
  <div className="barre-recherche">
    <svg className="barre-recherche__icone" /* icône loupe */ />
    <input
      type="search"
      className="barre-recherche__input"
      value={valeur}
      onChange={e => onChange(e.target.value)}
      placeholder={placeholder}
    />
    {valeur && (
      <button className="barre-recherche__clear" onClick={() => onChange('')}>
        ✕
      </button>
    )}
  </div>
);

// src/components/FiltresCategorie.jsx
const CATEGORIES = ['tous', 'plat', 'entree', 'soupe', 'dessert'];

const FiltresCategorie = ({ active, onChange }) => (
  <div className="filtres-categorie" role="group" aria-label="Filtrer par catégorie">
    {CATEGORIES.map(cat => (
      <button
        key={cat}
        className={`filtre-btn ${active === cat ? 'filtre-btn--active' : ''}`}
        onClick={() => onChange(cat)}
        aria-pressed={active === cat}
      >
        {cat === 'tous' ? 'Tous' : cat.charAt(0).toUpperCase() + cat.slice(1)}
      </button>
    ))}
  </div>
);

// src/components/MessageVide.jsx
const MessageVide = ({ recherche, categorie }) => (
  <div className="message-vide">
    <p className="message-vide__titre">Aucun plat trouvé</p>
    <p className="message-vide__detail">
      {recherche
        ? `Aucun résultat pour "${recherche}"${categorie !== 'tous' ? ` dans la catégorie ${categorie}` : ''}`
        : `Aucun plat disponible dans la catégorie ${categorie}`
      }
    </p>
  </div>
);

3
Composants de classe — lire et maintenir le legacy
React.Component · render() · this.state · lifecycle · Conversion

Les composants de classe sont la forme historique de React (avant 2019). Vous les rencontrerez dans des projets existants. Savoir les lire et les convertir en composants fonctionnels est une compétence attendue. La section 08 de la Partie 0 a couvert leur syntaxe — voici leur usage React concret et comment migrer.

javascript
// ── COMPOSANT DE CLASSE (legacy — à lire dans des projets existants) ──
class CartePlatLegacy extends React.Component {
  constructor(props) {
    super(props);
    this.state = { quantite: 1, ajoute: false };
    this.handleAjouter = this.handleAjouter.bind(this);
  }

  handleAjouter() {
    this.props.onAjouter({ ...this.props.plat, quantite: this.state.quantite });
    this.setState({ ajoute: true });
    setTimeout(() => this.setState({ ajoute: false }), 2000);
  }

  render() {
    const { plat } = this.props;
    const { quantite, ajoute } = this.state;
    return (
      <div className="carte-plat">
        <h3>{plat.nom}</h3>
        <p>{plat.prix} MAD</p>
        <input
          type="number" min="1" max="10"
          value={quantite}
          onChange={e => this.setState({ quantite: +e.target.value })}
        />
        <button onClick={this.handleAjouter} disabled={ajoute}>
          {ajoute ? 'Ajouté !' : 'Ajouter au panier'}
        </button>
      </div>
    );
  }
}

// ── MÊME COMPOSANT en fonctionnel + hooks (migration) ─────────
const CartePlatModerne = ({ plat, onAjouter }) => {
  const [quantite, setQuantite] = useState(1);
  const [ajoute, setAjoute]     = useState(false);

  const handleAjouter = () => {
    onAjouter({ ...plat, quantite });
    setAjoute(true);
    setTimeout(() => setAjoute(false), 2000);
  };

  return (
    <div className="carte-plat">
      <h3>{plat.nom}</h3>
      <p>{plat.prix} MAD</p>
      <input
        type="number" min="1" max="10"
        value={quantite}
        onChange={e => setQuantite(+e.target.value)}
      />
      <button onClick={handleAjouter} disabled={ajoute}>
        {ajoute ? 'Ajouté !' : 'Ajouter au panier'}
      </button>
    </div>
  );
};

✓
Vérification des acquis
5 questions — corrigé intégré
On crée un composant const listePlats = ({ plats }) => ... et on l'utilise avec <listePlats plats={data} />. Que se passe-t-il ?
Un composant DariGo affiche le nom du plat, son prix, la note ET gère l'ajout au panier ET charge les avis depuis l'API. Quel problème cela pose-t-il ?
Quelle est la règle fondamentale d'un composant fonctionnel par rapport à ses props ?
Dans la migration du composant de classe vers fonctionnel, this.setState({ ajoute: true }) devient quoi ?
Le composant BadgeStatut reçoit statut="livree" et affiche "Livrée" en vert. Si le même statut est passé une deuxième fois, React re-rend-il le composant ?
Partie 3 — Composants, Props & State Props PropTypes · Flux unidirectionnel · Lifting State Up
⏱ 18 min

Props — passer des données entre composants

Les props sont le mécanisme par lequel les composants React communiquent. Un composant parent passe des données à ses enfants via les props — exactement comme les arguments d'une fonction. Les props sont en lecture seule : un composant enfant ne peut jamais modifier les props qu'il reçoit. C'est le flux de données unidirectionnel de React — la règle la plus importante de l'architecture React, celle qui rend les applications prévisibles et déboguables.


1
Passer et recevoir des props
Types de valeurs · Valeurs par défaut · Spread · children · Fonctions
javascript
// ── Passer tous types de valeurs en props ─────────────────────
const plat = {
  id: 1, nom: "Tajine Kefta", prix: 75,
  note: 4.8, disponible: true, categorie: "plat",
  restaurant: "La Sqala, Casablanca",
  tags: ["halal", "traditionnel", "sans-gluten"],
  nutritions: { calories: 520, proteines: 38, glucides: 22 }
};

<CartePlat
  // String
  nom="Tajine Kefta"
  restaurant="La Sqala, Casablanca"

  // Nombre
  prix={75}
  note={4.8}

  // Booléen — true peut s'écrire sans valeur
  disponible
  estNouveau={false}

  // Tableau
  tags={["halal", "traditionnel"]}

  // Objet
  nutritions={{ calories: 520, proteines: 38 }}

  // Fonction (callback — communication enfant → parent)
  onAjouter={(quantite) => ajouterAuPanier(plat, quantite)}
  onVoirDetails={() => navigate(`/plat/${plat.id}`)}

  // Composant React comme prop (render prop pattern)
  icone={<svg>...</svg>}
/>

// ── Spread de props — passer tout un objet ────────────────────
// Au lieu d'énumérer chaque propriété :
<CartePlat id={plat.id} nom={plat.nom} prix={plat.prix} note={plat.note} ... />

// Utiliser le spread :
<CartePlat {...plat} onAjouter={handleAjouter} />
// Attention : le spread passe TOUTES les propriétés de l'objet.
// Si l'objet a des propriétés que le composant n'attend pas, elles sont ignorées
// (mais peuvent générer des warnings si passées à des éléments HTML natifs).

// ── Valeurs par défaut dans les paramètres ────────────────────
const CartePlat = ({
  nom,
  prix,
  note = 0,             // par défaut 0 si non fourni
  disponible = true,    // par défaut disponible
  remise = 0,
  onAjouter = () => {}, // callback vide par défaut — évite les erreurs
  className = '',
}) => {
  const prixFinal = remise > 0 ? prix * (1 - remise / 100) : prix;
  return ( /* ... */ );
};

// ── children — contenu entre les balises ─────────────────────
const SectionMenu = ({ titre, children }) => (
  <section className="section-menu">
    <h2 className="section-menu__titre">{titre}</h2>
    <div className="section-menu__contenu">
      {children}
    </div>
  </section>
);

// Utilisation
<SectionMenu titre="Plats du moment">
  <CartePlat {...tajine} />
  <CartePlat {...couscous} />
  <CartePlat {...harira} />
</SectionMenu>

2
Flux unidirectionnel & Lifting State Up
Parent → enfant · Callbacks · Remonter l'état · Communication fraternelle

Le flux unidirectionnel est la règle d'or de React : les données descendent du parent vers l'enfant via les props, jamais dans l'autre sens. Quand un enfant doit communiquer quelque chose à son parent — l'utilisateur a cliqué sur "Ajouter", a soumis un formulaire, a sélectionné une option — il appelle une fonction callback que le parent lui a passée en prop.

Le Lifting State Up (remonter l'état) est la technique qui découle de ce principe. Quand deux composants frères ont besoin du même état, cet état doit vivre dans leur ancêtre commun le plus proche. C'est lui qui possède l'état et distribue les données et les callbacks à ses enfants.

javascript
// ── Scénario DariGo : le panier est partagé entre CartePlat et PanierResume
// Les deux composants ont besoin de l'état panier.
// Solution : l'état vit dans PageMenu, leur ancêtre commun.

const PageMenu = () => {
  // État dans l'ancêtre commun
  const [panier, setPanier] = useState([]);
  const [plats, setPlats]   = useState([]);

  // Fonctions que les enfants appelleront via props (callbacks)
  const ajouterAuPanier = (plat, quantite = 1) => {
    setPanier(prev => {
      const existant = prev.find(p => p.id === plat.id);
      if (existant) {
        // Mettre à jour la quantité si déjà présent
        return prev.map(p =>
          p.id === plat.id ? { ...p, quantite: p.quantite + quantite } : p
        );
      }
      return [...prev, { ...plat, quantite }];
    });
  };

  const retirerDuPanier = (idPlat) => {
    setPanier(prev => prev.filter(p => p.id !== idPlat));
  };

  const viderPanier = () => setPanier([]);

  return (
    <div className="page-menu">
      <GrillePlats
        plats={plats}
        onAjouter={ajouterAuPanier}      // callback descendu vers l'enfant
      />
      <PanierResume
        panier={panier}                  // état descendu vers l'enfant
        onRetirer={retirerDuPanier}      // callback descendu vers l'enfant
        onVider={viderPanier}
      />
    </div>
  );
};

// CartePlat n'a PAS accès à setPanier — il appelle le callback
const CartePlat = ({ nom, prix, disponible, onAjouter }) => (
  <div className="carte-plat">
    <h3>{nom}</h3>
    <p>{prix} MAD</p>
    <button
      disabled={!disponible}
      onClick={() => onAjouter(1)}   // notifie le parent — c'est tout
    >
      Ajouter
    </button>
  </div>
);

// PanierResume affiche l'état et notifie les suppressions
const PanierResume = ({ panier, onRetirer, onVider }) => {
  const total = panier.reduce((acc, p) => acc + p.prix * p.quantite, 0);
  return (
    <aside className="panier">
      <h3>Panier ({panier.length})</h3>
      {panier.map(item => (
        <div key={item.id} className="panier__ligne">
          <span>{item.nom} × {item.quantite}</span>
          <span>{item.prix * item.quantite} MAD</span>
          <button onClick={() => onRetirer(item.id)}>✕</button>
        </div>
      ))}
      <div className="panier__total">Total : {total} MAD</div>
      <button onClick={onVider}>Vider le panier</button>
    </aside>
  );
};

3
PropTypes — valider les props
Runtime validation · Types · isRequired · shape · TypeScript

PropTypes est une bibliothèque de validation des props à l'exécution. Elle détecte les erreurs de type en développement et génère des avertissements dans la console. En production, les validations sont ignorées pour ne pas impacter les performances. PropTypes est une alternative à TypeScript pour les projets JavaScript — moins robuste (validation runtime vs compile time) mais plus simple à mettre en place.

javascript
import PropTypes from 'prop-types';

const CartePlat = ({ nom, prix, note, disponible, tags, onAjouter }) => (
  /* ... JSX ... */
);

CartePlat.propTypes = {
  nom:        PropTypes.string.isRequired,
  prix:       PropTypes.number.isRequired,
  note:       PropTypes.number,
  disponible: PropTypes.bool,
  tags:       PropTypes.arrayOf(PropTypes.string),
  onAjouter:  PropTypes.func.isRequired,

  // Objet avec forme précise
  restaurant: PropTypes.shape({
    nom:   PropTypes.string.isRequired,
    ville: PropTypes.string.isRequired,
    note:  PropTypes.number,
  }),

  // Une des valeurs possibles
  statut: PropTypes.oneOf(['disponible', 'indisponible', 'rupture']),

  // Un des types possibles
  identifiant: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

// Avertissement en console si nom n'est pas passé :
// Warning: Failed prop type: The prop `nom` is marked as required
// in `CartePlat`, but its value is `undefined`.

// ── En TypeScript (recommandé pour les nouveaux projets) ──────
interface CartePlatProps {
  nom:        string;
  prix:       number;
  note?:      number;         // ? = optionnel
  disponible?: boolean;
  tags?:      string[];
  onAjouter:  (quantite: number) => void;
  restaurant: {
    nom:   string;
    ville: string;
    note?: number;
  };
  statut?: 'disponible' | 'indisponible' | 'rupture';
}

const CartePlat: React.FC<CartePlatProps> = ({ nom, prix, ...rest }) => (
  /* ... */
);
// TypeScript détecte les erreurs au moment de la compilation, pas à l'exécution.

✓
Vérification des acquis
5 questions — corrigé intégré
Dans CartePlat, l'utilisateur clique sur "Ajouter". Le composant doit mettre à jour le panier. Comment procède-t-il dans le respect du flux unidirectionnel ?
Le composant FiltresCategorie et le composant GrillePlats doivent partager la catégorie active. Où doit vivre cet état selon le principe de Lifting State Up ?
On passe <CartePlat {...plat} onAjouter={handleAjouter} /> mais l'objet plat contient une propriété onClick héritée d'un traitement précédent. Quel risque y a-t-il ?
Quelle est la différence principale entre PropTypes et TypeScript pour valider les props ?
On définit onAjouter = () => {} comme valeur par défaut. Quel avantage cela apporte-t-il ?
Partie 3 — Composants, Props & State State useState · Immutabilité · Batch · Functional update
⏱ 22 min

State & useState

Le state (état) est la mémoire d'un composant — des données qui changent au fil du temps et dont les modifications déclenchent un re-rendu de l'interface. C'est la différence fondamentale entre les props (données passées depuis l'extérieur, immuables pour le composant) et le state (données internes, modifiables via la fonction setter). useState est le hook React qui permet à un composant fonctionnel de posséder et gérer son propre état.


1
useState — anatomie et comportement
Initialisation · Setter · Re-rendu · Async · Functional update

useState(valeurInitiale) retourne un tableau de deux éléments : la valeur actuelle de l'état et une fonction pour la mettre à jour. La valeur initiale n'est utilisée qu'au premier rendu — elle est ignorée lors des re-rendus suivants.

La mise à jour de l'état via le setter est asynchrone : React ne met pas à jour la valeur immédiatement. Il planifie un re-rendu, et la nouvelle valeur sera disponible lors du prochain rendu. Si vous lisez la variable d'état juste après avoir appelé le setter, vous obtenez encore l'ancienne valeur. Cette caractéristique est source de nombreux bugs pour les débutants.

Pour éviter ces bugs, React propose le functional update : passer une fonction au setter plutôt qu'une valeur directe. Cette fonction reçoit la valeur actuelle garantie et retourne la nouvelle valeur.

javascript
// ── useState — syntaxe de base ────────────────────────────────
const [valeur, setValeur] = useState(valeurInitiale);

// Exemples concrets DariGo
const [panier, setPanier]                 = useState([]);
const [villeLivraison, setVilleLivraison] = useState("Casablanca");
const [estConnecte, setEstConnecte]       = useState(false);
const [commandeEnCours, setCommande]      = useState(null);
const [nbArticles, setNbArticles]         = useState(0);

// ── Initialisation paresseuse ─────────────────────────────────
// Si la valeur initiale est coûteuse à calculer, passer une fonction
// Elle ne s'exécute QU'UNE FOIS au montage du composant
const [panier, setPanier] = useState(() => {
  const sauvegarde = localStorage.getItem('darigo-panier');
  return sauvegarde ? JSON.parse(sauvegarde) : [];
});

// ── Le setter est ASYNCHRONE — piège classique ────────────────
const ajouterArticle = () => {
  setNbArticles(nbArticles + 1);
  setNbArticles(nbArticles + 1); // nbArticles vaut encore l'ancienne valeur !
  console.log(nbArticles); // Affiche l'ANCIENNE valeur — pas la nouvelle
};
// Résultat : +1 au lieu de +2 — les deux appels utilisent la même valeur stale

// ── Functional update — solution au problème de l'état périmé ─
const ajouterArticleCorrect = () => {
  setNbArticles(prev => prev + 1); // prev = valeur GARANTIE la plus récente
  setNbArticles(prev => prev + 1); // s'exécute après le premier
};
// Résultat : +2 — chaque appel travaille sur la valeur précédente

// Toujours utiliser le functional update quand le nouvel état
// dépend de l'ancien état :
const ajouterAuPanier = (plat) => {
  setPanier(prev => {
    const existant = prev.find(p => p.id === plat.id);
    if (existant) {
      return prev.map(p =>
        p.id === plat.id ? { ...p, quantite: p.quantite + 1 } : p
      );
    }
    return [...prev, { ...plat, quantite: 1 }];
  });
};

const retirerDuPanier  = (id) => setPanier(prev => prev.filter(p => p.id !== id));
const viderPanier      = ()   => setPanier([]);
const incrementer      = ()   => setNbArticles(prev => prev + 1);

2
Immutabilité — la règle absolue
Référence vs valeur · Ne jamais muter · Spread · map() · filter()

React détecte les changements d'état par comparaison de références, pas par comparaison profonde des valeurs. Si vous mutez un tableau ou un objet existant sans créer une nouvelle référence, React ne détecte aucun changement et ne re-rend pas le composant. C'est le bug le plus fréquent chez les débutants React.

La règle est absolue : ne jamais muter l'état directement. Toujours créer un nouvel objet ou tableau avec les modifications.

javascript
// ══ TABLEAUX ═════════════════════════════════════════════════

// ── AJOUTER un élément ────────────────────────────────────────
// FAUX — mute le tableau original
const ajouterFaux = (plat) => {
  panier.push(plat);      // push mute — React ne voit aucun changement
  setPanier(panier);      // même référence — pas de re-rendu !
};
// CORRECT — nouveau tableau
const ajouter = (plat) => setPanier(prev => [...prev, plat]);

// ── SUPPRIMER un élément ──────────────────────────────────────
// FAUX
const supprimerFaux = (id) => {
  const idx = panier.findIndex(p => p.id === id);
  panier.splice(idx, 1);  // splice mute
  setPanier(panier);
};
// CORRECT
const supprimer = (id) => setPanier(prev => prev.filter(p => p.id !== id));

// ── MODIFIER un élément ───────────────────────────────────────
// FAUX
const modifierFaux = (id, nouvelleQte) => {
  const plat = panier.find(p => p.id === id);
  plat.quantite = nouvelleQte; // mute l'objet dans le tableau
  setPanier([...panier]);      // nouveau tableau mais objets mutés
};
// CORRECT — nouvel objet pour l'élément modifié
const modifier = (id, nouvelleQte) =>
  setPanier(prev => prev.map(p =>
    p.id === id ? { ...p, quantite: nouvelleQte } : p
  ));

// ── TRIER ─────────────────────────────────────────────────────
// FAUX — sort mute le tableau original
setPanier(prev => prev.sort((a, b) => a.prix - b.prix));
// CORRECT — copier avant de trier
setPanier(prev => [...prev].sort((a, b) => a.prix - b.prix));

// ══ OBJETS ═══════════════════════════════════════════════════

const [commande, setCommande] = useState({
  id: null, client: '', adresse: '', statut: 'en_attente',
  livreur: null, articles: []
});

// ── MODIFIER une propriété ────────────────────────────────────
// FAUX — mutation directe
commande.statut = 'confirmee';
setCommande(commande); // même référence

// CORRECT — nouvel objet avec spread
setCommande(prev => ({ ...prev, statut: 'confirmee' }));
setCommande(prev => ({ ...prev, statut: 'en_livraison', livreur: livreurAssigne }));

// ── MODIFIER un objet imbriqué ────────────────────────────────
// L'adresse est un objet dans la commande
// FAUX — mutation de l'objet imbriqué
commande.adresseObj.ville = 'Rabat';

// CORRECT — spread à chaque niveau
setCommande(prev => ({
  ...prev,
  adresseObj: {
    ...prev.adresseObj,
    ville: 'Rabat'
  }
}));

3
Gérer plusieurs états — useState vs useReducer
États multiples · Batching · useReducer · État dérivé
javascript
// ── Plusieurs useState indépendants — approche standard ───────
const FormulaireCommande = () => {
  const [nom, setNom]               = useState('');
  const [telephone, setTelephone]   = useState('');
  const [adresse, setAdresse]       = useState('');
  const [ville, setVille]           = useState('Casablanca');
  const [modePaiement, setMode]     = useState('CMI');
  const [erreurs, setErreurs]       = useState({});
  const [isLoading, setIsLoading]   = useState(false);

  // Règle : séparer les états indépendants en plusieurs useState.
  // Grouper uniquement les états fortement liés.
};

// ── Grouper des états liés en un seul objet ───────────────────
// Quand plusieurs états évoluent toujours ensemble
const FormulaireCommandeGroupe = () => {
  const [form, setForm] = useState({
    nom: '', telephone: '', adresse: '', ville: 'Casablanca', modePaiement: 'CMI'
  });
  const [ui, setUi] = useState({ isLoading: false, erreurs: {}, succes: false });

  // Mettre à jour un champ du formulaire
  const handleChange = (champ, valeur) =>
    setForm(prev => ({ ...prev, [champ]: valeur }));

  // Exemple d'utilisation :
  <input
    value={form.nom}
    onChange={e => handleChange('nom', e.target.value)}
    placeholder="Nadia Benali"
  />
};

// ── État dérivé — calculer depuis l'état existant ─────────────
// NE PAS stocker dans un useState ce qui peut être calculé
const ResumePanier = ({ panier }) => {
  // FAUX — état dérivé inutile et source de désynchronisation
  const [total, setTotal] = useState(0);
  useEffect(() => setTotal(panier.reduce(...)), [panier]); // redondant

  // CORRECT — calculer directement lors du rendu
  const total      = panier.reduce((acc, p) => acc + p.prix * p.quantite, 0);
  const nbArticles = panier.reduce((acc, p) => acc + p.quantite, 0);
  const aPanier    = panier.length > 0;
  const totalTTC   = (total * 1.2).toFixed(2);

  return (
    <div>
      <p>{nbArticles} article(s)</p>
      <p>Total HT : {total} MAD</p>
      <p>Total TTC : {totalTTC} MAD</p>
    </div>
  );
};
⚠ Batching React 18 — les mises à jour sont regroupées

React 18 regroupe automatiquement plusieurs appels setState dans un seul re-rendu, même dans les callbacks asynchrones (setTimeout, promesses, fetch). Si vous appelez setNom('Khalid') et setVille('Casablanca') dans le même gestionnaire d'événement, React ne re-rend le composant qu'une seule fois avec les deux changements. Ce comportement, appelé automatic batching, améliore les performances et n'a généralement pas d'impact visible sur votre code.


✓
Vérification des acquis
5 questions — corrigé intégré
On fait setNbArticles(nbArticles + 1) puis setNbArticles(nbArticles + 1) dans le même handler. Pourquoi le compteur n'augmente-t-il que de 1 ?
Le panier DariGo contient 3 plats. On fait panier.push(nouveauPlat); setPanier(panier). Que se passe-t-il ?
Doit-on créer un état const [total, setTotal] = useState(0) pour afficher le total du panier ?
La valeur initiale de useState(() => JSON.parse(localStorage.getItem('panier') || '[]')) est une fonction. Pourquoi ?
Comment mettre à jour correctement le statut dans const [commande, setCommande] = useState({ id: 1042, statut: 'en_attente', total: 145 }) ?
Partie 4 — Composants avancés Événements Cycle de vie · useEffect · Cleanup
⏱ 20 min

Événements & cycle de vie

Tout composant React traverse un cycle de vie : il est monté dans le DOM, peut se mettre à jour plusieurs fois, puis est démonté quand il n'est plus affiché. La gestion des événements utilisateur (clics, saisie, scroll) et la synchronisation avec des systèmes extérieurs (API, WebSocket, timers) doivent s'inscrire correctement dans ce cycle. useEffect est le hook qui orchestre ces opérations — son usage correct ou incorrect fait la différence entre une application fiable et une application avec des fuites mémoire et des bugs difficiles à reproduire.


1
Événements React — SyntheticEvent
onClick · onChange · onSubmit · preventDefault · stopPropagation

React enveloppe les événements natifs du navigateur dans un SyntheticEvent — un wrapper cross-browser qui garantit le même comportement sur tous les navigateurs. L'API est identique à celle des événements natifs : event.target, event.preventDefault(), event.stopPropagation(). Les handlers d'événements en React sont des props en camelCase : onClick, onChange, onSubmit, onKeyDown, onFocus, onBlur...

javascript
// ── Événements de base ────────────────────────────────────────
const BoutonAjouter = ({ plat, onAjouter }) => {
  // Handler défini en dehors du JSX — lisible, testable
  const handleClick = (event) => {
    event.stopPropagation(); // empêcher la propagation au parent
    onAjouter(plat);
  };

  return (
    <button className="btn-ajouter" onClick={handleClick}>
      Ajouter au panier
    </button>
  );
};

// ── onChange — champ texte ────────────────────────────────────
const ChampRecherche = ({ valeur, onChange }) => (
  <input
    type="search"
    value={valeur}
    onChange={(e) => onChange(e.target.value)}
    // e.target.value contient la valeur actuelle du champ
    placeholder="Chercher un plat..."
  />
);

// ── onSubmit — formulaire ─────────────────────────────────────
const FormulaireAdresse = ({ onSoumettre }) => {
  const [adresse, setAdresse] = useState('');
  const [ville, setVille]     = useState('Casablanca');

  const handleSubmit = (e) => {
    e.preventDefault(); // OBLIGATOIRE — empêche le rechargement de la page
    if (!adresse.trim()) return;
    onSoumettre({ adresse: adresse.trim(), ville });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={adresse}
        onChange={e => setAdresse(e.target.value)}
        placeholder="Votre adresse de livraison"
        required
      />
      <select value={ville} onChange={e => setVille(e.target.value)}>
        {['Casablanca','Rabat','Fès','Marrakech','Agadir'].map(v => (
          <option key={v} value={v}>{v}</option>
        ))}
      </select>
      <button type="submit">Confirmer la livraison</button>
    </form>
  );
};

// ── Événements clavier ────────────────────────────────────────
const SearchBar = ({ onSearch }) => {
  const [query, setQuery] = useState('');

  const handleKeyDown = (e) => {
    if (e.key === 'Enter') onSearch(query);
    if (e.key === 'Escape') setQuery('');
  };

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      onKeyDown={handleKeyDown}
      placeholder="Chercher..."
    />
  );
};

2
useEffect — synchroniser avec le monde extérieur
Montage · Dépendances · Cleanup · Pièges · AbortController

useEffect permet d'exécuter du code après que React a mis à jour le DOM. C'est l'endroit approprié pour les opérations qui ne peuvent pas (ou ne doivent pas) se faire pendant le rendu : appels API, abonnements à des événements, manipulation directe du DOM, timers. Le tableau de dépendances contrôle quand l'effet se ré-exécute.

javascript
// ── Tableau de dépendances — trois comportements ───────────────
useEffect(() => { /* ... */ });
// Pas de tableau → s'exécute après CHAQUE rendu. Rarement voulu.

useEffect(() => { /* ... */ }, []);
// Tableau vide → s'exécute UNE FOIS au montage. Équivalent componentDidMount.

useEffect(() => { /* ... */ }, [ville, categorie]);
// Avec dépendances → s'exécute au montage ET quand ville ou categorie change.

// ── Cas d'usage 1 : charger des données au montage ────────────
const PageMenu = ({ ville }) => {
  const [plats, setPlats]         = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [erreur, setErreur]       = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const charger = async () => {
      try {
        setIsLoading(true);
        setErreur(null);
        const data = await platsService.getParVille(ville, controller.signal);
        setPlats(data);
      } catch (err) {
        if (err.name !== 'AbortError') setErreur(err.message);
      } finally {
        setIsLoading(false);
      }
    };

    charger();
    return () => controller.abort(); // Cleanup : annuler la requête au démontage
  }, [ville]); // Re-exécuter quand ville change

  if (isLoading) return <Spinner />;
  if (erreur)    return <MessageErreur message={erreur} />;
  return <GrillePlats plats={plats} />;
};

// ── Cas d'usage 2 : suivi de livraison en temps réel ─────────
const SuiviLivraison = ({ idCommande }) => {
  const [position, setPosition] = useState(null);
  const [statut, setStatut]     = useState('en_livraison');

  useEffect(() => {
    // Polling toutes les 10 secondes
    const interval = setInterval(async () => {
      const data = await livraisonsService.getPosition(idCommande);
      setPosition(data.position);
      setStatut(data.statut);
      if (data.statut === 'livree') clearInterval(interval); // arrêter le polling
    }, 10000);

    return () => clearInterval(interval); // Cleanup OBLIGATOIRE
    // Sans cleanup, le timer continue après démontage → fuite mémoire
  }, [idCommande]);

  return (
    <div className="suivi">
      <CarteMap position={position} />
      <BadgeStatut statut={statut} />
    </div>
  );
};

// ── Cas d'usage 3 : synchroniser le titre de la page ──────────
const PageCommande = ({ commande }) => {
  useEffect(() => {
    document.title = `Commande #${commande.id} — ${commande.statut} | DariGo`;
    return () => { document.title = 'DariGo'; }; // Restaurer au démontage
  }, [commande.id, commande.statut]);

  return <div>{/* ... */}</div>;
};

// ── Cas d'usage 4 : sauvegarder le panier ────────────────────
const usePersistPanier = (panier) => {
  useEffect(() => {
    localStorage.setItem('darigo-panier', JSON.stringify(panier));
  }, [panier]); // Sauvegarder à chaque modification du panier
};
⚠ Dépendances manquantes — le bug le plus fréquent

Si votre effet utilise une variable ou une fonction définie dans le composant, elle doit figurer dans le tableau de dépendances. Omettre une dépendance crée un effet qui travaille avec des valeurs périmées (stale closure). ESLint avec la règle react-hooks/exhaustive-deps détecte automatiquement ces oublis — ne désactivez jamais ce warning sans comprendre pourquoi il apparaît.


✓
Vérification des acquis
5 questions — corrigé intégré
Dans le composant SuiviLivraison, si on omet la fonction de cleanup (return () => clearInterval(interval)), que se passe-t-il quand l'utilisateur navigue vers une autre page ?
On veut charger les plats quand la ville change. Quelle signature de useEffect utiliser ?
Pourquoi faut-il appeler e.preventDefault() dans le handler onSubmit d'un formulaire React ?
Qu'est-ce qu'une stale closure dans useEffect et comment l'éviter ?
On utilise useEffect(() => { document.title = `Commande #${id}`; }, [id]). La fonction de cleanup retourne () => { document.title = 'DariGo'; }. Quand cette cleanup s'exécute-t-elle ?
Partie 4 — Composants avancés Listes · key Rendu conditionnel · Images · Assets
⏱ 18 min

Listes, rendu conditionnel & assets

Afficher des listes de données et contrôler ce qui s'affiche selon l'état sont deux des tâches les plus fréquentes dans une application React. Le menu DariGo affiche une liste de plats filtrée par catégorie et recherche — et selon l'état de chargement, l'erreur ou le résultat vide, l'interface change complètement. Cette section couvre tous les patterns de rendu conditionnel et la gestion des assets (images, SVG, fichiers).


1
Rendu de listes — map() et key
key stable · Fragments · Listes imbriquées · Virtualisation
javascript
// ── Rendu de liste basique ────────────────────────────────────
const GrillePlats = ({ plats, onAjouter }) => (
  <div className="grille-plats">
    {plats.map(plat => (
      <CartePlat
        key={plat.id}            // OBLIGATOIRE — id stable de la BDD
        {...plat}
        onAjouter={() => onAjouter(plat)}
      />
    ))}
  </div>
);

// ── Liste avec Fragment — pas de div supplémentaire ───────────
const MenuTable = ({ categories }) => (
  <dl className="menu-table">
    {categories.map(cat => (
      <React.Fragment key={cat.id}>
        <dt className="categorie-titre">{cat.nom}</dt>
        {cat.plats.map(p => (
          <dd key={p.id} className="plat-ligne">
            <span>{p.nom}</span>
            <span>{p.prix} MAD</span>
          </dd>
        ))}
      </React.Fragment>
    ))}
  </dl>
);

// ── Liste avec index — quand c'est acceptable ─────────────────
// Acceptable SEULEMENT si la liste :
// - n'est jamais réordonnée ni filtrée
// - n'a pas d'éléments avec un état interne (formulaires, animations)
const ListeSteps = ({ etapes }) => (
  <ol>
    {etapes.map((etape, index) => (
      <li key={index}>{etape}</li> // OK — liste statique simple
    ))}
  </ol>
);

// ── Liste vide — toujours gérer ce cas ───────────────────────
const ListeCommandes = ({ commandes }) => {
  if (commandes.length === 0) {
    return (
      <div className="liste-vide">
        <p>Vous n'avez pas encore passé de commande</p>
        <button onClick={() => navigate('/menu')}>Voir le menu</button>
      </div>
    );
  }
  return (
    <div className="liste-commandes">
      {commandes.map(c => <CarteCommande key={c.id} {...c} />)}
    </div>
  );
};

2
Rendu conditionnel — tous les patterns
if/return · Ternaire · && · Nullish · Switch · Composant dédié
javascript
// ── PATTERN 1 : early return — pour les états de chargement ───
const PageMenu = ({ ville }) => {
  const [plats, setPlats]   = useState([]);
  const [loading, setLoading] = useState(true);
  const [erreur, setErreur]   = useState(null);

  // ... useEffect pour charger les plats

  // Retourner tôt pour les états alternatifs
  if (loading) return (
    <div className="page-chargement">
      <div className="spinner" aria-label="Chargement en cours" />
      <p>Chargement du menu de {ville}...</p>
    </div>
  );

  if (erreur) return (
    <div className="page-erreur">
      <p className="erreur-message">{erreur}</p>
      <button onClick={() => window.location.reload()}>Réessayer</button>
    </div>
  );

  if (plats.length === 0) return (
    <div className="page-vide">
      <p>Aucun plat disponible à {ville} pour le moment</p>
    </div>
  );

  // Cas nominal — liste des plats
  return <GrillePlats plats={plats} />;
};

// ── PATTERN 2 : ternaire — deux alternatives ──────────────────
const BoutonPanier = ({ disponible, onClick }) => (
  <div className="carte-action">
    {disponible
      ? <button className="btn-primary" onClick={onClick}>Ajouter</button>
      : <span className="badge-indispo">Indisponible</span>
    }
  </div>
);

// ── PATTERN 3 : && — afficher ou rien ────────────────────────
const CartePlat = ({ plat, estNouveau, remise }) => (
  <article className="carte-plat">
    <h3>{plat.nom}</h3>
    {estNouveau && <span className="badge-nouveau">Nouveau</span>}
    {remise > 0 && <span className="badge-remise">-{remise}%</span>}
    {plat.tags.length > 0 && (
      <div className="tags">
        {plat.tags.map(t => <span key={t} className="tag">{t}</span>)}
      </div>
    )}
  </article>
);

// ── PATTERN 4 : switch — états multiples ──────────────────────
const AfficheurStatut = ({ statut }) => {
  switch (statut) {
    case 'en_attente':
      return <div className="statut statut-attente">En attente de confirmation</div>;
    case 'confirmee':
      return <div className="statut statut-confirmee">Commande confirmée</div>;
    case 'en_preparation':
      return <div className="statut statut-preparation">En cours de préparation</div>;
    case 'en_livraison':
      return <div className="statut statut-livraison">En route vers vous</div>;
    case 'livree':
      return <div className="statut statut-livree">Livrée — Bon appétit !</div>;
    case 'annulee':
      return <div className="statut statut-annulee">Annulée</div>;
    default:
      return <div className="statut">{statut}</div>;
  }
};

// ── PATTERN 5 : objet de config — remplace les longues conditions
const CONFIG_STATUT = {
  en_attente:    { label: 'En attente',    classe: 'warning', icone: '⏳' },
  confirmee:     { label: 'Confirmée',     classe: 'info',    icone: '✓'  },
  en_preparation:{ label: 'En préparation',classe: 'primary', icone: '👨‍🍳' },
  en_livraison:  { label: 'En livraison',  classe: 'accent',  icone: '🛵' },
  livree:        { label: 'Livrée',        classe: 'success', icone: '✓'  },
  annulee:       { label: 'Annulée',       classe: 'danger',  icone: '✕'  },
};

const BadgeStatut = ({ statut }) => {
  const config = CONFIG_STATUT[statut] || { label: statut, classe: 'default', icone: '?' };
  return (
    <span className={`badge badge-${config.classe}`}>
      {config.icone} {config.label}
    </span>
  );
};

3
Assets — images, SVG et fichiers avec Vite
import image · URL publique · SVG inline · lazy loading
javascript
// ── Images dans src/assets — importées par Vite ───────────────
// Vite traite ces images : optimisation, hash dans le nom de fichier
import logoUrl       from './assets/darigo-logo.svg';
import tajineImg     from './assets/plats/tajine-kefta.jpg';
import placeholderImg from './assets/plat-placeholder.jpg';

const Header = () => (
  <header>
    <img src={logoUrl} alt="DariGo" width="120" height="40" />
  </header>
);

// ── Images dans public/ — URL directe ────────────────────────
// Fichiers dans public/ sont servis tels quels, pas traités par Vite
// URL construite depuis la racine — sans import
const CartePlat = ({ nom, imageUrl }) => (
  <img
    src={imageUrl || '/images/plat-placeholder.jpg'}
    alt={nom}
    loading="lazy"  // chargement différé pour les images hors viewport
    width="300"
    height="200"
  />
);

// ── Images depuis l'API — URL dynamique ───────────────────────
const ImagePlat = ({ plat }) => {
  const [erreur, setErreur] = useState(false);

  return (
    <img
      src={erreur ? '/images/plat-placeholder.jpg' : plat.imageUrl}
      alt={plat.nom}
      onError={() => setErreur(true)}  // fallback si l'image ne charge pas
      loading="lazy"
      className="carte-plat__image"
    />
  );
};

// ── SVG inline — icônes scalables et stylables en CSS ─────────
const IconePanier = ({ taille = 24, couleur = 'currentColor' }) => (
  <svg
    width={taille}
    height={taille}
    viewBox="0 0 24 24"
    fill="none"
    stroke={couleur}
    strokeWidth="2"
    aria-hidden="true"
  >
    <circle cx="9" cy="21" r="1"/>
    <circle cx="20" cy="21" r="1"/>
    <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
  </svg>
);

// ── Variables d'environnement Vite ────────────────────────────
// Les variables préfixées VITE_ sont exposées au code front-end
// Définies dans .env ou .env.local
const API_URL = import.meta.env.VITE_API_URL || 'https://api.darigo.ma';
const MODE    = import.meta.env.MODE; // 'development' ou 'production'

// .env.local (ne pas commiter dans Git)
// VITE_API_URL=http://localhost:8080
// .env.production
// VITE_API_URL=https://api.darigo.ma

✓
Vérification des acquis
5 questions — corrigé intégré
Le composant PageMenu doit afficher un spinner pendant le chargement, un message d'erreur si l'API échoue, et la liste sinon. Quel pattern utiliser ?
On affiche {commandes.length && <ListeCommandes />}. La liste est vide. Qu'est-ce qui s'affiche dans la page ?
Pourquoi importer une image depuis src/assets/ avec import img from './assets/tajine.jpg' plutôt que la mettre dans public/ et utiliser '/images/tajine.jpg' ?
On affiche <img src={plat.imageUrl} alt={plat.nom} />. Que se passe-t-il si l'URL de l'image est invalide ou le serveur retourne une 404 ?
Dans le pattern de config CONFIG_STATUT[statut] pour le badge, quel est l'avantage par rapport à un switch ou des if/else if ?
Partie 4 — Composants avancés Formulaires Contrôlés · Validation · React Hook Form
⏱ 20 min

Formulaires contrôlés

Les formulaires sont partout dans DariGo : inscription, connexion, adresse de livraison, avis sur un plat. React propose deux approches : les formulaires contrôlés où React est la source de vérité pour chaque champ (via useState), et les formulaires non contrôlés où le DOM garde la source de vérité (via useRef). Les formulaires contrôlés sont l'approche standard en React — ils permettent la validation temps réel, la transformation des données à la saisie, et la synchronisation parfaite avec l'état de l'application.


1
Formulaire contrôlé — le pattern complet
value + onChange · Tous les types · Validation · Soumission

Dans un formulaire contrôlé, chaque champ a un value lié à un état React et un onChange qui met à jour cet état. React est la source de vérité — la valeur affichée dans le champ est toujours exactement celle que React connaît. Ce contrôle total permet de valider en temps réel, de transformer la saisie (forcer des majuscules, limiter la longueur, formater un numéro de téléphone), et de soumettre des données déjà validées.

javascript
// ── Formulaire d'inscription DariGo — complet ─────────────────
const FormulaireInscription = ({ onInscription }) => {
  // État du formulaire
  const [form, setForm] = useState({
    prenom: '', nom: '', email: '', telephone: '',
    ville: 'Casablanca', motDePasse: '', confirmation: ''
  });

  // Erreurs de validation
  const [erreurs, setErreurs] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  // Handler générique — met à jour n'importe quel champ
  const handleChange = (champ) => (e) => {
    const valeur = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    setForm(prev => ({ ...prev, [champ]: valeur }));
    // Effacer l'erreur du champ dès que l'utilisateur corrige
    if (erreurs[champ]) setErreurs(prev => ({ ...prev, [champ]: '' }));
  };

  // Validation
  const valider = () => {
    const nouvellesErreurs = {};

    if (!form.prenom.trim())
      nouvellesErreurs.prenom = 'Le prénom est obligatoire';

    if (!form.email.includes('@'))
      nouvellesErreurs.email = 'Adresse email invalide';

    if (!/^(\+212|0)[5-7]\d{8}$/.test(form.telephone.replace(/\s/g, '')))
      nouvellesErreurs.telephone = 'Numéro marocain invalide (ex: 06 12 34 56 78)';

    if (form.motDePasse.length < 8)
      nouvellesErreurs.motDePasse = 'Minimum 8 caractères';

    if (form.motDePasse !== form.confirmation)
      nouvellesErreurs.confirmation = 'Les mots de passe ne correspondent pas';

    setErreurs(nouvellesErreurs);
    return Object.keys(nouvellesErreurs).length === 0;
  };

  // Soumission
  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!valider()) return;

    setIsLoading(true);
    try {
      await authService.register(form);
      onInscription();
    } catch (err) {
      setErreurs({ global: err.message });
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="formulaire-inscription" noValidate>
      {erreurs.global && (
        <div className="erreur-globale" role="alert">{erreurs.global}</div>
      )}

      {/* Champ texte */}
      <div className="champ">
        <label htmlFor="prenom">Prénom</label>
        <input
          id="prenom"
          type="text"
          value={form.prenom}
          onChange={handleChange('prenom')}
          placeholder="Khalid"
          className={erreurs.prenom ? 'input-erreur' : ''}
          aria-describedby={erreurs.prenom ? 'err-prenom' : undefined}
        />
        {erreurs.prenom && (
          <span id="err-prenom" className="msg-erreur" role="alert">
            {erreurs.prenom}
          </span>
        )}
      </div>

      {/* Select */}
      <div className="champ">
        <label htmlFor="ville">Ville</label>
        <select id="ville" value={form.ville} onChange={handleChange('ville')}>
          {['Casablanca','Rabat','Fès','Marrakech','Agadir','Tanger'].map(v => (
            <option key={v} value={v}>{v}</option>
          ))}
        </select>
      </div>

      {/* Mot de passe */}
      <div className="champ">
        <label htmlFor="mdp">Mot de passe</label>
        <input
          id="mdp"
          type="password"
          value={form.motDePasse}
          onChange={handleChange('motDePasse')}
          placeholder="Minimum 8 caractères"
          className={erreurs.motDePasse ? 'input-erreur' : ''}
        />
        {erreurs.motDePasse && (
          <span className="msg-erreur">{erreurs.motDePasse}</span>
        )}
      </div>

      <button type="submit" disabled={isLoading} className="btn btn-primary">
        {isLoading ? 'Inscription...' : "S'inscrire sur DariGo"}
      </button>
    </form>
  );
};

2
React Hook Form — la bibliothèque standard
register · handleSubmit · formState · Controller · Zod

Les formulaires contrôlés avec useState sont parfaits pour les petits formulaires. Pour les formulaires complexes avec de nombreux champs, validation avancée et performances optimisées, React Hook Form est la bibliothèque de référence. Elle utilise des formulaires non contrôlés en interne (refs au lieu de state) pour éviter les re-rendus à chaque frappe, ce qui améliore considérablement les performances sur les gros formulaires.

javascript
import { useForm, Controller } from 'react-hook-form';
import { z } from 'zod'; // bibliothèque de validation de schémas
import { zodResolver } from '@hookform/resolvers/zod';

// ── Définir le schéma de validation avec Zod ─────────────────
const schemaCommande = z.object({
  adresse:      z.string().min(10, 'Adresse trop courte (min. 10 caractères)'),
  ville:        z.enum(['Casablanca','Rabat','Fès','Marrakech','Agadir']),
  telephone:    z.string().regex(/^(\+212|0)[5-7]\d{8}$/, 'Numéro marocain invalide'),
  instructions: z.string().max(200).optional(),
  modePaiement: z.enum(['CMI', 'especes', 'virement']).default('CMI'),
});

// ── Formulaire avec React Hook Form ──────────────────────────
const FormulaireCommandeRHF = ({ panier, onCommander }) => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting, isValid },
    control, // pour les composants contrôlés (Select, DatePicker...)
    reset,
  } = useForm({
    resolver: zodResolver(schemaCommande),
    defaultValues: { ville: 'Casablanca', modePaiement: 'CMI' },
    mode: 'onChange', // valider à chaque changement (ou 'onBlur', 'onSubmit')
  });

  const onSubmit = async (data) => {
    // data est déjà validé et typé par Zod
    try {
      await commandesService.creer({ ...data, articles: panier });
      onCommander();
      reset();
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>

      {/* register() connecte le champ à RHF — pas de useState nécessaire */}
      <div className="champ">
        <label htmlFor="adresse">Adresse de livraison</label>
        <input
          id="adresse"
          {...register('adresse')}  // spread les props nécessaires (ref, onChange, onBlur, name)
          placeholder="123 Rue Mohammed V, Maarif"
        />
        {errors.adresse && (
          <span className="msg-erreur">{errors.adresse.message}</span>
        )}
      </div>

      <div className="champ">
        <label htmlFor="telephone">Téléphone</label>
        <input
          id="telephone"
          type="tel"
          {...register('telephone')}
          placeholder="06 12 34 56 78"
        />
        {errors.telephone && (
          <span className="msg-erreur">{errors.telephone.message}</span>
        )}
      </div>

      {/* Controller pour les composants non-natifs */}
      <Controller
        name="ville"
        control={control}
        render={({ field }) => (
          <select {...field}>
            {['Casablanca','Rabat','Fès','Marrakech','Agadir'].map(v => (
              <option key={v} value={v}>{v}</option>
            ))}
          </select>
        )}
      />

      <button
        type="submit"
        disabled={isSubmitting || !isValid}
        className="btn btn-primary"
      >
        {isSubmitting ? 'Commande en cours...' : 'Commander — Payer par CMI'}
      </button>

    </form>
  );
};

✓
Vérification des acquis
5 questions — corrigé intégré
Dans un formulaire contrôlé, un champ <input value={form.nom} /> sans onChange. Que se passe-t-il quand l'utilisateur tape ?
Pourquoi React Hook Form est-il plus performant qu'un formulaire contrôlé avec useState pour un formulaire de 20 champs ?
Quelle est la différence entre mode: 'onChange' et mode: 'onBlur' dans React Hook Form ?
Dans le handler handleChange générique, on vérifie e.target.type === 'checkbox' ? e.target.checked : e.target.value. Pourquoi ?
Dans le formulaire RHF, disabled={isSubmitting || !isValid} est appliqué au bouton submit. Qu'est-ce que isValid représente exactement ?
Partie 5 — API, Hooks & Routage API REST Axios · React Query · JWT · Intercepteurs
⏱ 22 min

Consommer des API REST

Une application React sans API est une maquette statique. DariGo tire toutes ses données du back-end : plats, restaurants, commandes, positions des livreurs. Cette section couvre l'architecture complète d'une couche API professionnelle — d'Axios avec intercepteurs jusqu'à React Query qui résout les problèmes de cache, de synchronisation et de loading state que vous gériez manuellement avec useEffect et useState.


1
Axios — client HTTP avec intercepteurs
Instance · Intercepteurs · JWT · Timeout · Gestion d'erreurs

Axios est le client HTTP standard dans l'écosystème React. Par rapport à fetch, il offre : la conversion automatique JSON (pas besoin de .json()), les intercepteurs pour ajouter automatiquement les headers d'auth, le rejet automatique pour les codes d'erreur HTTP (4xx, 5xx), et une syntaxe plus concise. On crée une instance Axios configurée une seule fois — tous les appels API de l'application passent par cette instance.

javascript
// ── src/services/api.js — instance Axios centrale ─────────────
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'https://api.darigo.ma',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' }
});

// ── Intercepteur requête — ajouter le token JWT ───────────────
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('darigo_token');
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  (error) => Promise.reject(error)
);

// ── Intercepteur réponse — gestion centralisée des erreurs ────
api.interceptors.response.use(
  (response) => response.data, // retourner directement les données
  async (error) => {
    const { response } = error;

    // Token expiré → tenter un refresh silencieux
    if (response?.status === 401) {
      try {
        const { data } = await axios.post(
          `${import.meta.env.VITE_API_URL}/auth/refresh`,
          { refreshToken: localStorage.getItem('darigo_refresh') }
        );
        localStorage.setItem('darigo_token', data.token);
        // Rejouer la requête originale avec le nouveau token
        error.config.headers.Authorization = `Bearer ${data.token}`;
        return api(error.config);
      } catch {
        localStorage.removeItem('darigo_token');
        localStorage.removeItem('darigo_refresh');
        window.location.href = '/login';
      }
    }

    // Construire un message d'erreur lisible
    const message = response?.data?.message
      || response?.data?.error
      || `Erreur ${response?.status || 'réseau'}`;

    return Promise.reject(new Error(message));
  }
);

export default api;

// ── src/services/platsService.js ──────────────────────────────
import api from './api';

export const platsService = {
  getAll:       (params)    => api.get('/plats', { params }),
  getById:      (id)        => api.get(`/plats/${id}`),
  getParVille:  (ville)     => api.get('/plats', { params: { ville } }),
  rechercher:   (q, ville)  => api.get('/plats', { params: { q, ville } }),
  creer:        (data)      => api.post('/plats', data),
  mettreAJour:  (id, data)  => api.put(`/plats/${id}`, data),
  supprimer:    (id)        => api.delete(`/plats/${id}`),
};

// ── src/services/commandesService.js ─────────────────────────
import api from './api';

export const commandesService = {
  creer:           (data)        => api.post('/commandes', data),
  getById:         (id)          => api.get(`/commandes/${id}`),
  getMesCommandes: (params)      => api.get('/commandes/me', { params }),
  updateStatut:    (id, statut)  => api.patch(`/commandes/${id}/statut`, { statut }),
  annuler:         (id, raison)  => api.delete(`/commandes/${id}`, { data: { raison } }),
  getPosition:     (id)          => api.get(`/commandes/${id}/position`),
};

// ── src/services/authService.js ───────────────────────────────
import api from './api';

export const authService = {
  login: async (email, motDePasse) => {
    const data = await api.post('/auth/login', { email, motDePasse });
    localStorage.setItem('darigo_token',   data.token);
    localStorage.setItem('darigo_refresh', data.refreshToken);
    return data.utilisateur;
  },
  register: (userData)  => api.post('/auth/register', userData),
  logout:   ()          => {
    localStorage.removeItem('darigo_token');
    localStorage.removeItem('darigo_refresh');
    return api.post('/auth/logout').catch(() => {});
  },
  getProfile: () => api.get('/auth/me'),
};

2
React Query (TanStack Query) — le game changer
useQuery · useMutation · Cache · Invalidation · Optimistic update

Chaque fois que vous écrivez useState([]) + useEffect(() => fetch...) + setLoading + setError, vous réinventez la roue. React Query encapsule ce pattern en un hook useQuery qui gère automatiquement le chargement, les erreurs, le cache, la revalidation, et la synchronisation entre onglets. C'est l'une des bibliothèques les plus adoptées de l'écosystème React depuis 2021.

javascript
// ── Configuration initiale — main.jsx ─────────────────────────
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime:   1000 * 60 * 5,  // données fraîches pendant 5 minutes
      gcTime:      1000 * 60 * 10, // garder en cache 10 minutes
      retry:       2,              // 2 tentatives en cas d'échec
      refetchOnWindowFocus: true,  // revalider quand l'onglet reprend le focus
    }
  }
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools /> {/* outil de debug — dev uniquement */}
  </QueryClientProvider>
);

// ── useQuery — charger des données ───────────────────────────
const GrillePlats = ({ ville, categorie }) => {
  const {
    data: plats = [],
    isLoading,
    isError,
    error,
    isFetching,       // true pendant une revalidation silencieuse
    refetch,          // déclencher un rechargement manuel
  } = useQuery({
    queryKey: ['plats', ville, categorie], // clé unique — changer = nouveau fetch
    queryFn:  () => platsService.getParVille(ville, categorie),
    enabled:  !!ville,                     // ne pas exécuter si ville est vide
  });

  if (isLoading) return <Spinner message={`Chargement du menu de ${ville}...`} />;
  if (isError)   return <ErreurAPI message={error.message} onRetry={refetch} />;

  return (
    <div className="grille-plats">
      {isFetching && <span className="badge-sync">Actualisation...</span>}
      {plats.map(p => <CartePlat key={p.id} {...p} />)}
    </div>
  );
};

// ── useMutation — modifier des données ───────────────────────
const BoutonCommander = ({ panier, adresse }) => {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (data) => commandesService.creer(data),
    onSuccess: (nouvelleCommande) => {
      // Invalider le cache des commandes → refetch automatique
      queryClient.invalidateQueries({ queryKey: ['mes-commandes'] });
      // Ajouter la nouvelle commande directement au cache sans refetch
      queryClient.setQueryData(['commande', nouvelleCommande.id], nouvelleCommande);
      navigate(`/suivi/${nouvelleCommande.id}`);
    },
    onError: (error) => {
      toast.error(`Commande impossible : ${error.message}`);
    }
  });

  const handleCommander = () => {
    mutation.mutate({
      articles:         panier.map(p => ({ idPlat: p.id, quantite: p.quantite })),
      adresseLivraison: adresse.adresse,
      villeLivraison:   adresse.ville,
    });
  };

  return (
    <button
      onClick={handleCommander}
      disabled={mutation.isPending || panier.length === 0}
      className="btn btn-primary btn-lg"
    >
      {mutation.isPending ? 'Commande en cours...' : `Commander — ${total} MAD`}
    </button>
  );
};

// ── Mise à jour optimiste ─────────────────────────────────────
// Afficher le résultat immédiatement, annuler si l'API échoue
const mutationNote = useMutation({
  mutationFn: ({ idPlat, note }) => platsService.noterPlat(idPlat, note),
  onMutate: async ({ idPlat, note }) => {
    await queryClient.cancelQueries({ queryKey: ['plats'] });
    const snapshot = queryClient.getQueryData(['plats', ville]);
    // Mettre à jour le cache immédiatement
    queryClient.setQueryData(['plats', ville], old =>
      old.map(p => p.id === idPlat ? { ...p, note } : p)
    );
    return { snapshot }; // contexte pour rollback
  },
  onError: (err, vars, context) => {
    queryClient.setQueryData(['plats', ville], context.snapshot); // rollback
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['plats'] });
  }
});

✓
Vérification des acquis
5 questions — corrigé intégré
L'intercepteur réponse Axios retourne response.data dans le cas de succès. Quelle conséquence cela a-t-il dans les services ?
Dans useQuery, queryKey: ['plats', ville, categorie]. Que se passe-t-il si ville change de "Casablanca" à "Rabat" ?
Quelle différence entre isLoading et isFetching dans useQuery ?
Après une commande réussie, on appelle queryClient.invalidateQueries({ queryKey: ['mes-commandes'] }). Que fait cette instruction ?
La mise à jour optimiste note un plat immédiatement puis fait un snapshot pour rollback. Pourquoi appelle-t-on cancelQueries avant la mise à jour optimiste ?
Partie 5 — API, Hooks & Routage useEffect useCallback · useMemo · useRef · useContext
⏱ 24 min

useEffect & hooks avancés

Les hooks sont la révolution React 16.8. Ils permettent d'utiliser l'état, le cycle de vie et d'autres fonctionnalités React dans des composants fonctionnels. Nous avons déjà couvert useState et les bases de useEffect. Cette section approfondit useEffect et couvre les hooks de performance et de référence — useCallback, useMemo, useRef, et useContext — que vous utiliserez dès que votre application DariGo commence à avoir des problèmes de performance ou des besoins de partage d'état.


1
useCallback & useMemo — optimiser les re-rendus
Mémoïsation · Référence stable · React.memo · Quand utiliser

À chaque rendu d'un composant React, toutes les fonctions et valeurs calculées sont recréées. En général c'est négligeable. Mais quand une fonction est passée en prop à un composant enfant mémoïsé avec React.memo, la recréation change sa référence — l'enfant pense que ses props ont changé et se re-rend inutilement. useCallback et useMemo mémoïsent respectivement une fonction et une valeur calculée pour éviter ces recréations inutiles.

Règle d'or : n'optimisez pas prématurément. Ajoutez useCallback et useMemo uniquement quand vous avez identifié un problème de performance réel avec les React DevTools — pas par habitude préventive.

javascript
// ── Problème sans mémoïsation ─────────────────────────────────
const PageMenu = ({ ville }) => {
  const [plats, setPlats] = useState([]);
  const [recherche, setRecherche] = useState('');

  // Cette fonction est RECRÉÉE à chaque rendu de PageMenu
  const ajouterAuPanier = (plat) => {
    setPanier(prev => [...prev, plat]);
  };

  // CartePlat avec React.memo se re-rend quand ses props changent.
  // Comme ajouterAuPanier est recréée → nouvelle référence → CartePlat se re-rend
  // même si l'utilisateur a juste tapé dans la barre de recherche !
  return (
    <div>
      <input value={recherche} onChange={e => setRecherche(e.target.value)} />
      {plats.map(p => <CartePlat key={p.id} {...p} onAjouter={ajouterAuPanier} />)}
    </div>
  );
};

// ── useCallback — référence stable pour les fonctions ─────────
const PageMenuOptimise = ({ ville }) => {
  const [plats, setPlats] = useState([]);
  const [recherche, setRecherche] = useState('');

  // useCallback mémoïse la fonction — référence stable entre les rendus
  // Elle n'est recréée que si ses dépendances changent
  const ajouterAuPanier = useCallback((plat) => {
    setPanier(prev => [...prev, plat]);
  }, []); // [] = jamais recréée (setPanier est stable)

  const supprimerDuPanier = useCallback((id) => {
    setPanier(prev => prev.filter(p => p.id !== id));
  }, []);

  return (
    <div>
      <input value={recherche} onChange={e => setRecherche(e.target.value)} />
      {plats.map(p => <CartePlat key={p.id} {...p} onAjouter={ajouterAuPanier} />)}
    </div>
  );
};

// ── useMemo — mémoïser des calculs coûteux ────────────────────
const ResumePanierAvance = ({ panier, promos }) => {
  // Ce calcul est complexe — il ne doit pas se refaire à chaque rendu
  const statsCommande = useMemo(() => {
    const sousTotal    = panier.reduce((acc, p) => acc + p.prix * p.quantite, 0);
    const nbArticles   = panier.reduce((acc, p) => acc + p.quantite, 0);
    const remiseTotale = promos.reduce((acc, promo) => {
      if (promo.applicable(panier)) return acc + promo.calculer(sousTotal);
      return acc;
    }, 0);
    const fraisLivraison = sousTotal > 200 ? 0 : 15; // gratuit au-dessus de 200 MAD
    const total          = sousTotal - remiseTotale + fraisLivraison;
    return { sousTotal, nbArticles, remiseTotale, fraisLivraison, total };
  }, [panier, promos]); // recalculer seulement si panier ou promos changent

  return (
    <div className="resume">
      <p>{statsCommande.nbArticles} article(s)</p>
      <p>Sous-total : {statsCommande.sousTotal} MAD</p>
      {statsCommande.remiseTotale > 0 && (
        <p className="remise">Remise : -{statsCommande.remiseTotale} MAD</p>
      )}
      <p>Livraison : {statsCommande.fraisLivraison === 0 ? 'Gratuite' : `${statsCommande.fraisLivraison} MAD`}</p>
      <strong>Total : {statsCommande.total} MAD</strong>
    </div>
  );
};

// ── React.memo — éviter le re-rendu si props identiques ───────
const CartePlat = React.memo(({ id, nom, prix, disponible, onAjouter }) => {
  console.log(`Rendu de CartePlat ${id}`); // Pour visualiser les re-rendus
  return (
    <article className="carte-plat">
      <h3>{nom}</h3>
      <p>{prix} MAD</p>
      <button onClick={() => onAjouter({ id, nom, prix })} disabled={!disponible}>
        Ajouter
      </button>
    </article>
  );
});
// CartePlat ne se re-rend que si ses props changent réellement.
// Avec useCallback sur onAjouter → référence stable → pas de re-rendu parasite.

2
useRef — références persistantes sans re-rendu
Accès DOM · Valeur mutable · Timer · Focus · Scroll

useRef retourne un objet { current: valeur } qui persiste entre les rendus. La différence fondamentale avec useState : modifier ref.current ne déclenche pas de re-rendu. C'est parfait pour stocker des valeurs qui doivent persister mais dont les changements ne doivent pas provoquer de re-rendu : timers, valeurs précédentes, indicateurs "monté/démonté", et surtout les références aux éléments DOM.

javascript
// ── Accès à un élément DOM ────────────────────────────────────
const BarreRecherche = () => {
  const inputRef = useRef(null);

  // Focus automatique au montage
  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  // Focus sur raccourci clavier (Ctrl+K)
  useEffect(() => {
    const handleKeydown = (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
        e.preventDefault();
        inputRef.current?.focus();
      }
    };
    window.addEventListener('keydown', handleKeydown);
    return () => window.removeEventListener('keydown', handleKeydown);
  }, []);

  return (
    <input
      ref={inputRef}
      type="search"
      placeholder="Chercher un plat... (Ctrl+K)"
    />
  );
};

// ── Stocker un timer sans provoquer de re-rendu ───────────────
const SuiviTempsReel = ({ idCommande }) => {
  const [position, setPosition] = useState(null);
  const intervalRef = useRef(null);     // ref pour stocker le timer

  const demarrerSuivi = useCallback(() => {
    intervalRef.current = setInterval(async () => {
      const data = await commandesService.getPosition(idCommande);
      setPosition(data);
      if (data.statut === 'livree') arreterSuivi();
    }, 5000);
  }, [idCommande]);

  const arreterSuivi = useCallback(() => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }, []);

  useEffect(() => {
    demarrerSuivi();
    return arreterSuivi; // cleanup
  }, [demarrerSuivi, arreterSuivi]);

  return <CartePosition position={position} />;
};

// ── Garder la valeur précédente ───────────────────────────────
const usePrevious = (valeur) => {
  const ref = useRef();
  useEffect(() => { ref.current = valeur; }); // s'exécute après le rendu
  return ref.current; // valeur du rendu précédent
};

const CompteurPanier = ({ panier }) => {
  const nbArticles     = panier.reduce((acc, p) => acc + p.quantite, 0);
  const nbPrecedent    = usePrevious(nbArticles);
  const articleAjoute  = nbArticles > (nbPrecedent || 0);

  return (
    <span className={`nb-articles ${articleAjoute ? 'pulse' : ''}`}>
      {nbArticles}
    </span>
  );
};

3
useContext — partager des données sans prop drilling
createContext · Provider · useContext · Auth · Thème

Quand des données doivent être accessibles par de nombreux composants à différents niveaux de l'arbre, les passer via les props à chaque niveau devient fastidieux — c'est le prop drilling. useContext permet à n'importe quel composant de lire des données depuis un contexte défini par un ancêtre, sans que les intermédiaires aient besoin de les connaître.

javascript
// ── src/contexts/AuthContext.jsx ──────────────────────────────
import { createContext, useContext, useState, useEffect } from 'react';
import { authService } from '../services/authService';

// 1. Créer le contexte
const AuthContext = createContext(null);

// 2. Créer le Provider — enveloppe les composants qui ont besoin du contexte
export const AuthProvider = ({ children }) => {
  const [utilisateur, setUtilisateur] = useState(null);
  const [isLoading, setIsLoading]     = useState(true);

  // Vérifier la session au démarrage
  useEffect(() => {
    const init = async () => {
      const token = localStorage.getItem('darigo_token');
      if (token) {
        try {
          const user = await authService.getProfile();
          setUtilisateur(user);
        } catch {
          localStorage.removeItem('darigo_token');
        }
      }
      setIsLoading(false);
    };
    init();
  }, []);

  const login = async (email, motDePasse) => {
    const user = await authService.login(email, motDePasse);
    setUtilisateur(user);
    return user;
  };

  const logout = async () => {
    await authService.logout();
    setUtilisateur(null);
  };

  const value = {
    utilisateur,
    isLoading,
    isAuthentifie: !!utilisateur,
    login,
    logout,
    estAdmin: utilisateur?.role === 'admin',
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

// 3. Hook personnalisé pour consommer le contexte
export const useAuth = () => {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth doit être utilisé dans AuthProvider');
  return ctx;
};

// ── main.jsx — envelopper l'app ───────────────────────────────
<QueryClientProvider client={queryClient}>
  <AuthProvider>
    <App />
  </AuthProvider>
</QueryClientProvider>

// ── Utilisation dans n'importe quel composant ─────────────────
const Header = () => {
  const { utilisateur, isAuthentifie, logout } = useAuth();

  return (
    <header>
      <img src="/logo.svg" alt="DariGo" />
      {isAuthentifie
        ? (
          <div className="user-menu">
            <span>Bienvenue, {utilisateur.prenom}</span>
            <button onClick={logout}>Déconnexion</button>
          </div>
        )
        : <button onClick={() => navigate('/login')}>Se connecter</button>
      }
    </header>
  );
};

const RouteProtegee = ({ children }) => {
  const { isAuthentifie, isLoading } = useAuth();
  if (isLoading) return <Spinner />;
  if (!isAuthentifie) return <Navigate to="/login" replace />;
  return children;
};

✓
Vérification des acquis
5 questions — corrigé intégré
On a const fn = useCallback(() => setPanier(p => [...p, plat]), [plat]). Si plat ne change pas entre deux rendus, que se passe-t-il pour fn ?
On veut stocker un ID de timer pour pouvoir l'annuler. Doit-on utiliser useState ou useRef ?
Qu'est-ce que le "prop drilling" et pourquoi useContext le résout-il ?
Quand doit-on ajouter useMemo à un calcul dans un composant React ?
Dans useAuth, on lève une erreur si le hook est utilisé hors du Provider. Pourquoi cette vérification est-elle importante ?
Partie 5 — API, Hooks & Routage Custom Hooks Réutilisabilité · Abstraction · Conventions
⏱ 20 min

Hooks personnalisés

Un hook personnalisé est une fonction JavaScript dont le nom commence par use et qui peut appeler d'autres hooks React. C'est le mécanisme principal de réutilisation de la logique dans React moderne — l'équivalent des mixins, des HOC et des render props, mais plus simple et plus lisible. Quand vous trouvez le même pattern de useState + useEffect dans plusieurs composants, c'est le signal pour extraire un hook personnalisé.


1
Créer et utiliser des hooks personnalisés
Convention use · Extraction · Retour · Tests
javascript
// ── hooks/usePlats.js ─────────────────────────────────────────
// Extrait le pattern répété : chargement + loading + erreur
export const usePlats = (ville, categorie) => {
  const [plats, setPlats]         = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [erreur, setErreur]       = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const charger = async () => {
      try {
        setIsLoading(true);
        setErreur(null);
        const data = await platsService.getAll({ ville, categorie }, controller.signal);
        setPlats(data);
      } catch (err) {
        if (err.name !== 'AbortError') setErreur(err.message);
      } finally {
        setIsLoading(false);
      }
    };
    charger();
    return () => controller.abort();
  }, [ville, categorie]);

  return { plats, isLoading, erreur };
};

// ── hooks/usePanier.js ────────────────────────────────────────
export const usePanier = () => {
  const [panier, setPanier] = useState(() => {
    const saved = localStorage.getItem('darigo-panier');
    return saved ? JSON.parse(saved) : [];
  });

  // Persister automatiquement
  useEffect(() => {
    localStorage.setItem('darigo-panier', JSON.stringify(panier));
  }, [panier]);

  const ajouter = useCallback((plat, quantite = 1) => {
    setPanier(prev => {
      const existe = prev.find(p => p.id === plat.id);
      if (existe) return prev.map(p => p.id === plat.id ? { ...p, quantite: p.quantite + quantite } : p);
      return [...prev, { ...plat, quantite }];
    });
  }, []);

  const retirer   = useCallback((id)  => setPanier(prev => prev.filter(p => p.id !== id)), []);
  const vider     = useCallback(()    => setPanier([]), []);
  const majQte    = useCallback((id, q) => setPanier(prev => prev.map(p => p.id === id ? { ...p, quantite: q } : p)), []);

  const total      = useMemo(() => panier.reduce((acc, p) => acc + p.prix * p.quantite, 0), [panier]);
  const nbArticles = useMemo(() => panier.reduce((acc, p) => acc + p.quantite, 0), [panier]);

  return { panier, ajouter, retirer, vider, majQte, total, nbArticles };
};

// ── hooks/useDebounce.js ──────────────────────────────────────
// Éviter de chercher à chaque frappe — attendre 300ms de pause
export const useDebounce = (valeur, delai = 300) => {
  const [valeurDifferee, setValeurDifferee] = useState(valeur);

  useEffect(() => {
    const timer = setTimeout(() => setValeurDifferee(valeur), delai);
    return () => clearTimeout(timer);
  }, [valeur, delai]);

  return valeurDifferee;
};

// ── hooks/useLocalStorage.js ──────────────────────────────────
export const useLocalStorage = (cle, valeurInitiale) => {
  const [valeur, setValeur] = useState(() => {
    try {
      const item = localStorage.getItem(cle);
      return item ? JSON.parse(item) : valeurInitiale;
    } catch { return valeurInitiale; }
  });

  const sauvegarder = useCallback((nouvelleValeur) => {
    const valAEnregistrer = typeof nouvelleValeur === 'function'
      ? nouvelleValeur(valeur) : nouvelleValeur;
    setValeur(valAEnregistrer);
    localStorage.setItem(cle, JSON.stringify(valAEnregistrer));
  }, [cle, valeur]);

  return [valeur, sauvegarder];
};

// ── Utilisation dans les composants ──────────────────────────
const PageMenu = ({ ville }) => {
  const [categorie, setCategorie] = useState('tous');
  const [recherche, setRecherche] = useState('');
  const rechercheDifferee         = useDebounce(recherche, 400);

  // Un seul hook remplace ~20 lignes de useState + useEffect
  const { plats, isLoading, erreur } = usePlats(ville, categorie);
  const { ajouter, nbArticles, total } = usePanier();

  const platsFiltres = useMemo(() =>
    plats.filter(p => p.nom.toLowerCase().includes(rechercheDifferee.toLowerCase())),
    [plats, rechercheDifferee]
  );

  if (isLoading) return <Spinner />;
  if (erreur)    return <ErreurAPI message={erreur} />;

  return (
    <div className="page-menu">
      <input value={recherche} onChange={e => setRecherche(e.target.value)} />
      <FiltresCategorie active={categorie} onChange={setCategorie} />
      <GrillePlats plats={platsFiltres} onAjouter={ajouter} />
      <BoutonPanier nbArticles={nbArticles} total={total} />
    </div>
  );
};

✓
Vérification des acquis
4 questions — corrigé intégré
Pourquoi le nom d'un hook personnalisé doit-il obligatoirement commencer par use ?
Dans useDebounce, la recherche "Tajine" a été saisie. L'utilisateur s'arrête de taper. Que se passe-t-il après 300ms ?
Deux composants utilisent usePanier(). Partagent-ils le même état de panier ?
Quel est le signal qui indique qu'il faut extraire un hook personnalisé ?
Partie 5 — API, Hooks & Routage React Router v6 BrowserRouter · Routes · Link · Params · Guards
⏱ 22 min

React Router — routage côté client

React Router v6 est la bibliothèque de routage standard pour React. Elle implémente le routage côté client — naviguer entre les pages de DariGo sans rechargement, protéger les routes qui nécessitent une authentification, passer des paramètres dynamiques dans les URLs (id de commande, ville, id de plat), et gérer les redirections. C'est la pièce qui transforme un composant React en une véritable application multi-pages.


1
Configuration des routes DariGo
BrowserRouter · Routes · Route · Nested · Index · Wildcard
javascript
// ── src/App.jsx — configuration complète des routes DariGo ────
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';

const App = () => (
  <BrowserRouter>
    <Routes>
      {/* Routes publiques — accessibles sans connexion */}
      <Route path="/" element={<Layout />}>

        {/* Route index — affiché quand path = "/" exactement */}
        <Route index element={<PageAccueil />} />

        {/* Paramètre dynamique :ville */}
        <Route path="menu"         element={<PageMenu />} />
        <Route path="menu/:ville"  element={<PageMenu />} />
        <Route path="plat/:id"     element={<PagePlat />} />

        <Route path="login"        element={<PageLogin />} />
        <Route path="inscription"  element={<PageInscription />} />

        {/* Routes protégées — nécessitent une authentification */}
        <Route element={<RouteProtegee />}>
          <Route path="commande/nouvelle"   element={<PageNouvelleCommande />} />
          <Route path="commande/:id"        element={<PageCommande />} />
          <Route path="suivi/:id"           element={<PageSuivi />} />
          <Route path="profil"              element={<PageProfil />} />
          <Route path="mes-commandes"       element={<PageMesCommandes />} />
        </Route>

        {/* Routes admin */}
        <Route element={<RouteAdmin />}>
          <Route path="admin"               element={<PageAdmin />} />
          <Route path="admin/plats"         element={<PageAdminPlats />} />
        </Route>

        {/* Redirection et 404 */}
        <Route path="accueil" element={<Navigate to="/" replace />} />
        <Route path="*"       element={<Page404 />} />
      </Route>
    </Routes>
  </BrowserRouter>
);

// ── Layout — Header et Footer persistent entre les pages ──────
import { Outlet } from 'react-router-dom';

const Layout = () => (
  <div className="app-layout">
    <Header />
    <main className="app-main">
      <Outlet /> {/* Page courante rendue ici */}
    </main>
    <Footer />
  </div>
);

// ── Route protégée avec Outlet ────────────────────────────────
const RouteProtegee = () => {
  const { isAuthentifie, isLoading } = useAuth();
  if (isLoading) return <Spinner />;
  return isAuthentifie ? <Outlet /> : <Navigate to="/login" replace />;
};

const RouteAdmin = () => {
  const { utilisateur } = useAuth();
  return utilisateur?.role === 'admin' ? <Outlet /> : <Navigate to="/" replace />;
};

2
Hooks de navigation — useNavigate, useParams, useSearchParams
Paramètres URL · Query string · Navigation programmatique · useLocation
javascript
import {
  Link, NavLink, useNavigate, useParams,
  useSearchParams, useLocation
} from 'react-router-dom';

// ── useParams — paramètres dynamiques de l'URL ────────────────
// Route : path="suivi/:id"
// URL   : /suivi/1042
const PageSuivi = () => {
  const { id } = useParams(); // { id: "1042" } — toujours string !
  const idNumerique = parseInt(id, 10); // convertir si nécessaire

  const { data: commande } = useQuery({
    queryKey: ['commande', id],
    queryFn:  () => commandesService.getById(id),
  });

  return <div>Suivi de la commande #{id}</div>;
};

// ── useSearchParams — query string (?ville=Casablanca&cat=plat) ──
const PageMenu = () => {
  const [searchParams, setSearchParams] = useSearchParams();

  const ville     = searchParams.get('ville')     || 'Casablanca';
  const categorie = searchParams.get('categorie') || 'tous';
  const page      = parseInt(searchParams.get('page') || '1', 10);

  const changerVille = (nouvelleVille) => {
    setSearchParams(prev => {
      prev.set('ville', nouvelleVille);
      prev.delete('page'); // réinitialiser la pagination
      return prev;
    });
  };

  // L'URL devient : /menu?ville=Rabat&categorie=plat
  return (
    <div>
      <FiltresVille ville={ville} onChange={changerVille} />
      <FiltresCategorie categorie={categorie} onChange={cat => setSearchParams({ ville, categorie: cat })} />
    </div>
  );
};

// ── useNavigate — navigation programmatique ───────────────────
const FormulaireCommande = ({ panier }) => {
  const navigate = useNavigate();

  const handleSubmit = async (data) => {
    const commande = await commandesService.creer(data);
    navigate(`/suivi/${commande.id}`);         // aller vers
    navigate(-1);                              // retour page précédente
    navigate('/menu', { replace: true });      // remplace l'historique (pas de retour)
    navigate('/login', {                       // avec state
      state: { redirectAfterLogin: '/commande/nouvelle' }
    });
  };
};

// ── Link et NavLink ───────────────────────────────────────────
const Navigation = () => {
  const { nbArticles } = usePanier();

  return (
    <nav>
      {/* NavLink — ajoute automatiquement la classe active sur la route courante */}
      <NavLink to="/"              className={({ isActive }) => isActive ? 'nav-actif' : ''}>Accueil</NavLink>
      <NavLink to="/menu"          className={({ isActive }) => isActive ? 'nav-actif' : ''}>Menu</NavLink>
      <NavLink to="/mes-commandes" className={({ isActive }) => isActive ? 'nav-actif' : ''}>Commandes</NavLink>

      {/* Link — navigation sans rechargement */}
      <Link to="/commande/nouvelle" className="btn btn-primary">
        Panier ({nbArticles})
      </Link>
    </nav>
  );
};

✓
Vérification des acquis
5 questions — corrigé intégré
Sur la route suivi/:id, l'URL est /suivi/1042. useParams() retourne { id: "1042" }. Comment récupérer ce paramètre comme un nombre ?
Dans la RouteProtegee, pourquoi utilise-t-on <Outlet /> au lieu de {children} ?
Quelle différence entre navigate('/menu') et navigate('/menu', { replace: true }) ?
Pourquoi utilise-t-on <NavLink> pour la navigation principale plutôt que <Link> ?
On veut que l'URL /menu?ville=Casablanca&categorie=plat soit bookmarkable — un utilisateur peut la partager et retrouver le même état de filtres. Quel hook utiliser ?
Partie 6 — Redux Redux Store · Action · Reducer · Flux
⏱ 20 min

Introduction à Redux

Redux est une bibliothèque de gestion d'état global pour JavaScript. Dans DariGo, le panier doit être accessible depuis le menu, le header, la page commande et le checkout — des composants éloignés dans l'arbre sans relation parent/enfant directe. L'utilisateur connecté doit être connu partout. Les notifications doivent s'afficher depuis n'importe quelle page. useContext peut gérer ces cas simples, mais Redux offre une architecture plus robuste, des outils de débogage puissants, et une approche prévisible pour les états complexes qui interagissent entre eux.


1
Le problème Redux résout — et quand l'utiliser
État global · Prop drilling · Quand pas Redux · Context vs Redux

Redux n'est pas toujours la bonne réponse. Avant d'adopter Redux, posez-vous ces questions : l'état doit-il être accessible depuis des composants sans relation parent/enfant ? Plusieurs parties de l'application modifient-elles le même état ? Avez-vous besoin de tracer l'historique des modifications ? Si vous répondez oui à au moins deux de ces questions, Redux est probablement justifié.

Pour DariGo, Redux est justifié pour le panier (modifié depuis le menu, le détail plat, et vidé à la commande), l'état d'authentification (lu par Header, RouteProtegee, profil), et les notifications (déclenchées depuis n'importe quelle opération async).

CritèreuseState localuseContextRedux
PortéeUn composantArbre de composantsToute l'application
ComplexitéMinimaleFaibleModérée (Redux Toolkit)
Outils de debugReact DevToolsReact DevToolsRedux DevTools (time travel)
PerformanceOptimaleRe-rendus si mal utiliséSélecteurs optimisés
Idéal pourÉtat UI local (modal, formulaire)Thème, langue, auth simplePanier, auth, notifications, données complexes

2
Concepts fondamentaux — Store, Action, Reducer
Single source of truth · Actions immuables · Reducers purs · Flux unidirectionnel

Redux repose sur trois principes et trois concepts fondamentaux :

Store : un objet JavaScript unique qui contient tout l'état de l'application. Il est la source de vérité unique (single source of truth). Tous les composants qui ont besoin d'état global lisent depuis ce store.

Action : un objet JavaScript qui décrit ce qui s'est passé. Format : { type: 'DOMAINE/evenement', payload: données }. Une action ne modifie rien — elle décrit seulement un événement.

Reducer : une fonction pure qui prend l'état actuel et une action, et retourne le nouvel état. Un reducer ne mute jamais l'état — il retourne toujours un nouvel objet. C'est l'immutabilité appliquée à l'échelle de l'application.

javascript
// ── Les 3 concepts Redux illustrés pour DariGo ────────────────

// ── 1. ACTIONS — décrire ce qui s'est passé ───────────────────
// Convention : { type: 'DOMAINE/EVENEMENT', payload: données }
const actionAjouter = {
  type: 'panier/ajouterArticle',
  payload: { id: 1, nom: 'Tajine Kefta', prix: 75, quantite: 2 }
};
const actionRetirer = {
  type: 'panier/retirerArticle',
  payload: { id: 1 }
};
const actionVider = {
  type: 'panier/vider'
};
const actionConnexion = {
  type: 'auth/connexionReussie',
  payload: { id: 42, prenom: 'Khalid', email: 'khalid@darigo.ma', role: 'client' }
};

// ── 2. REDUCER — calculer le nouvel état ─────────────────────
// (etatActuel, action) => nouvelEtat
// Fonction PURE : pas d'effets de bord, immutabilité obligatoire
const etatInitialPanier = { articles: [], total: 0, nbArticles: 0 };

const panierReducer = (state = etatInitialPanier, action) => {
  switch (action.type) {

    case 'panier/ajouterArticle': {
      const existe = state.articles.find(a => a.id === action.payload.id);
      const articles = existe
        ? state.articles.map(a =>
            a.id === action.payload.id
              ? { ...a, quantite: a.quantite + action.payload.quantite }
              : a
          )
        : [...state.articles, action.payload];

      const nbArticles = articles.reduce((acc, a) => acc + a.quantite, 0);
      const total      = articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
      return { ...state, articles, nbArticles, total };
    }

    case 'panier/retirerArticle': {
      const articles   = state.articles.filter(a => a.id !== action.payload.id);
      const nbArticles = articles.reduce((acc, a) => acc + a.quantite, 0);
      const total      = articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
      return { ...state, articles, nbArticles, total };
    }

    case 'panier/vider':
      return etatInitialPanier;

    default:
      return state; // TOUJOURS retourner l'état pour les actions inconnues
  }
};

// ── 3. STORE — contenir tout l'état ──────────────────────────
import { createStore, combineReducers } from 'redux';

const authReducer = (state = { utilisateur: null, isAuthentifie: false }, action) => {
  switch (action.type) {
    case 'auth/connexionReussie':
      return { ...state, utilisateur: action.payload, isAuthentifie: true };
    case 'auth/deconnexion':
      return { utilisateur: null, isAuthentifie: false };
    default:
      return state;
  }
};

const rootReducer = combineReducers({
  panier: panierReducer,
  auth:   authReducer,
  // notifs: notifsReducer,
});

const store = createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__?.() // Redux DevTools
);

// Structure du store DariGo :
// {
//   panier: { articles: [...], total: 275, nbArticles: 7 },
//   auth:   { utilisateur: { id:42, prenom:'Khalid'... }, isAuthentifie: true }
// }

// ── Le flux Redux ────────────────────────────────────────────
// Composant → dispatch(action) → reducer → nouvel état → store → composants re-rendus
store.dispatch(actionAjouter);
store.dispatch({ type: 'panier/retirerArticle', payload: { id: 1 } });
console.log(store.getState().panier); // nouvel état après les deux dispatches

✓
Vérification des acquis
5 questions — corrigé intégré
Pourquoi un reducer Redux doit-il être une fonction pure ?
Dans le reducer, que doit retourner le case default ?
DariGo a un panier, une authentification et des notifications. Comment organiser les reducers ?
Quelle est la différence entre Redux et useContext pour gérer le panier DariGo ?
On dispatch { type: 'panier/ajouterArticle', payload: {...} }. Quel reducer est appelé en premier ?
Partie 6 — Redux React-Redux Provider · useSelector · useDispatch · Sélecteurs
⏱ 22 min

Redux dans React — intégration avec react-redux

Redux est une bibliothèque JavaScript générique — elle peut fonctionner avec n'importe quel framework. La bibliothèque react-redux fait le pont entre Redux et React. Elle fournit le composant Provider pour rendre le store accessible à toute l'application, et les hooks useSelector et useDispatch pour lire et modifier l'état depuis n'importe quel composant.


1
Provider, useSelector & useDispatch
Connexion au store · Sélecteurs · dispatch · Sélecteurs mémoïsés
javascript
// ── main.jsx — connecter le store à React ────────────────────
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <App />
      </AuthProvider>
    </QueryClientProvider>
  </Provider>
);

// ── useSelector — lire l'état du store ───────────────────────
// useSelector prend un sélecteur : (état global) => valeur voulue
// Le composant se re-rend UNIQUEMENT si la valeur retournée change

const Header = () => {
  // Lire depuis le store
  const nbArticles   = useSelector(state => state.panier.nbArticles);
  const total        = useSelector(state => state.panier.total);
  const utilisateur  = useSelector(state => state.auth.utilisateur);
  const notifications = useSelector(state => state.notifs.liste);

  return (
    <header className="header">
      <img src="/logo.svg" alt="DariGo" />
      <nav>
        <NavLink to="/menu">Menu</NavLink>
        {utilisateur
          ? <span>Bonjour, {utilisateur.prenom}</span>
          : <NavLink to="/login">Connexion</NavLink>
        }
      </nav>
      <Link to="/commande/nouvelle" className="btn-panier">
        Panier — {nbArticles} article(s) — {total} MAD
      </Link>
    </header>
  );
};

// ── useDispatch — déclencher des actions ─────────────────────
const CartePlat = ({ plat }) => {
  const dispatch = useDispatch();

  const ajouterAuPanier = () => {
    dispatch({
      type: 'panier/ajouterArticle',
      payload: { ...plat, quantite: 1 }
    });
  };

  return (
    <article className="carte-plat">
      <h3>{plat.nom}</h3>
      <p>{plat.prix} MAD</p>
      <button onClick={ajouterAuPanier}>Ajouter au panier</button>
    </article>
  );
};

// ── Sélecteurs — centraliser et mémoïser les lectures ─────────
// Définir les sélecteurs dans un fichier séparé
// src/store/selectors.js

export const selPanier        = state => state.panier;
export const selArticlesPanier = state => state.panier.articles;
export const selTotalPanier   = state => state.panier.total;
export const selNbArticles    = state => state.panier.nbArticles;
export const selUtilisateur   = state => state.auth.utilisateur;
export const selEstAuthentifie = state => state.auth.isAuthentifie;

// Sélecteur dérivé — calculer depuis plusieurs parties de l'état
export const selPanierVide = state => state.panier.articles.length === 0;
export const selPeutCommander = state =>
  state.auth.isAuthentifie && state.panier.articles.length > 0;

// Utilisation avec les sélecteurs nommés — plus lisible
const PanierResume = () => {
  const articles     = useSelector(selArticlesPanier);
  const total        = useSelector(selTotalPanier);
  const peutCommander = useSelector(selPeutCommander);

  return (
    <div className="panier-resume">
      {articles.map(a => (
        <LignePanier key={a.id} article={a} />
      ))}
      <strong>Total : {total} MAD</strong>
      <button disabled={!peutCommander}>Commander</button>
    </div>
  );
};

✓
Vérification des acquis
4 questions — corrigé intégré
On a deux composants qui utilisent useSelector(state => state.panier.total). Une action ajoute un article, faisant passer total de 75 à 150 MAD. Combien de composants se re-rendent ?
Pourquoi définir des sélecteurs dans un fichier séparé plutôt que d'écrire state => state.panier.articles directement dans chaque composant ?
Le composant Footer n'utilise aucun état Redux. Est-il impacté par un dispatch dans le panier ?
On a const dispatch = useDispatch(). Est-il nécessaire d'appeler useDispatch dans chaque composant qui dispatch, ou peut-on partager la même instance ?
Partie 6 — Redux Redux Toolkit createSlice · configureStore · createAsyncThunk · Immer
⏱ 24 min

Redux Toolkit & middlewares

Redux Toolkit (RTK) est la façon officielle et recommandée d'écrire du Redux depuis 2020. Il élimine le boilerplate répétitif de Redux classique : plus besoin d'écrire les types d'actions manuellement, les créateurs d'actions, ou de s'inquiéter de l'immutabilité (Immer le gère en interne). RTK réduit de 50 à 70% le code Redux tout en rendant l'architecture plus claire. Toute nouvelle application React doit utiliser RTK — pas Redux "vanilla".


1
createSlice — reducers et actions ensemble
Slice · Immer · Action creators · extraReducers · configureStore

createSlice regroupe en un seul endroit : l'état initial, les reducers, et les créateurs d'actions — qui sont générés automatiquement. Immer est intégré, ce qui signifie que vous pouvez écrire du code qui semble muter l'état (avec state.articles.push(...)) mais Immer le convertit automatiquement en opération immutable. C'est la révolution du code Redux avec RTK.

javascript
// ── src/store/panierSlice.js ───────────────────────────────────
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { commandesService } from '../services/commandesService';

// createAsyncThunk — actions asynchrones (appels API)
export const passerCommande = createAsyncThunk(
  'panier/passerCommande',  // préfixe/nom — génère 3 actions : pending, fulfilled, rejected
  async ({ articles, adresse, ville }, { rejectWithValue }) => {
    try {
      const commande = await commandesService.creer({ articles, adresse, ville });
      return commande; // payload de fulfilled
    } catch (error) {
      return rejectWithValue(error.message); // payload de rejected
    }
  }
);

const panierSlice = createSlice({
  name: 'panier',
  initialState: {
    articles:    [],
    total:       0,
    nbArticles:  0,
    commandeEnCours: null,
    isLoading:   false,
    erreur:      null,
  },

  reducers: {
    // Avec Immer : on peut "muter" state directement !
    // Immer crée un nouvel objet immutable en interne.

    ajouterArticle: (state, action) => {
      const { id, nom, prix, quantite = 1 } = action.payload;
      const existe = state.articles.find(a => a.id === id);

      if (existe) {
        existe.quantite += quantite;  // mutation Immer — OK !
      } else {
        state.articles.push({ id, nom, prix, quantite }); // push Immer — OK !
      }

      // Recalculer total et nbArticles
      state.nbArticles = state.articles.reduce((acc, a) => acc + a.quantite, 0);
      state.total      = state.articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
    },

    retirerArticle: (state, action) => {
      state.articles = state.articles.filter(a => a.id !== action.payload.id);
      state.nbArticles = state.articles.reduce((acc, a) => acc + a.quantite, 0);
      state.total      = state.articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
    },

    majQuantite: (state, action) => {
      const { id, quantite } = action.payload;
      const article = state.articles.find(a => a.id === id);
      if (article) {
        article.quantite = quantite;  // mutation Immer
        state.nbArticles = state.articles.reduce((acc, a) => acc + a.quantite, 0);
        state.total      = state.articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
      }
    },

    vider: (state) => {
      state.articles   = [];
      state.total      = 0;
      state.nbArticles = 0;
    },
  },

  extraReducers: (builder) => {
    // Gérer les états de l'action asynchrone passerCommande
    builder
      .addCase(passerCommande.pending, (state) => {
        state.isLoading = true;
        state.erreur    = null;
      })
      .addCase(passerCommande.fulfilled, (state, action) => {
        state.isLoading       = false;
        state.commandeEnCours = action.payload;
        state.articles        = [];  // vider après commande réussie
        state.total           = 0;
        state.nbArticles      = 0;
      })
      .addCase(passerCommande.rejected, (state, action) => {
        state.isLoading = false;
        state.erreur    = action.payload;
      });
  }
});

// Les action creators sont générés automatiquement par createSlice
export const { ajouterArticle, retirerArticle, majQuantite, vider } = panierSlice.actions;

// Sélecteurs
export const selPanier       = state => state.panier;
export const selArticles     = state => state.panier.articles;
export const selTotal        = state => state.panier.total;
export const selNbArticles   = state => state.panier.nbArticles;
export const selPanierVide   = state => state.panier.articles.length === 0;

export default panierSlice.reducer;

// ── src/store/authSlice.js ─────────────────────────────────────
const authSlice = createSlice({
  name: 'auth',
  initialState: { utilisateur: null, isAuthentifie: false, isLoading: false },
  reducers: {
    connexionReussie: (state, action) => {
      state.utilisateur    = action.payload;
      state.isAuthentifie  = true;
    },
    deconnexion: (state) => {
      state.utilisateur   = null;
      state.isAuthentifie = false;
    }
  }
});
export const { connexionReussie, deconnexion } = authSlice.actions;
export default authSlice.reducer;

// ── src/store/index.js — configureStore ───────────────────────
import { configureStore } from '@reduxjs/toolkit';
import panierReducer from './panierSlice';
import authReducer   from './authSlice';

const store = configureStore({
  reducer: {
    panier: panierReducer,
    auth:   authReducer,
  },
  // configureStore intègre automatiquement :
  // - Redux DevTools (en développement)
  // - redux-thunk middleware (pour les actions asynchrones)
  // - Vérification d'immutabilité (en développement)
});

export default store;
export type RootState    = ReturnType<typeof store.getState>;
export type AppDispatch  = typeof store.dispatch;

2
Utilisation dans les composants avec RTK
Action creators · createAsyncThunk · useDispatch typé
javascript
// ── Composant avec Redux Toolkit ──────────────────────────────
import { useSelector, useDispatch } from 'react-redux';
import { ajouterArticle, retirerArticle, selArticles, selTotal, selNbArticles } from '../store/panierSlice';
import { passerCommande } from '../store/panierSlice';

const PageCheckout = ({ adresse }) => {
  const dispatch    = useDispatch();
  const articles    = useSelector(selArticles);
  const total       = useSelector(selTotal);
  const nbArticles  = useSelector(selNbArticles);
  const isLoading   = useSelector(state => state.panier.isLoading);
  const erreur      = useSelector(state => state.panier.erreur);

  const handleCommander = async () => {
    const result = await dispatch(passerCommande({
      articles: articles.map(a => ({ idPlat: a.id, quantite: a.quantite })),
      adresse:  adresse.adresse,
      ville:    adresse.ville,
    }));

    // Vérifier si l'action async a réussi
    if (passerCommande.fulfilled.match(result)) {
      navigate(`/suivi/${result.payload.id}`);
    }
  };

  return (
    <div className="checkout">
      <h2>Votre commande ({nbArticles} article(s))</h2>

      {articles.map(article => (
        <div key={article.id} className="ligne-checkout">
          <span>{article.nom} × {article.quantite}</span>
          <span>{article.prix * article.quantite} MAD</span>
          <button onClick={() => dispatch(retirerArticle({ id: article.id }))}>
            Retirer
          </button>
        </div>
      ))}

      <div className="checkout__total">
        <strong>Total : {total} MAD</strong>
      </div>

      {erreur && (
        <div className="erreur-commande" role="alert">{erreur}</div>
      )}

      <button
        onClick={handleCommander}
        disabled={isLoading || articles.length === 0}
        className="btn btn-primary btn-lg"
      >
        {isLoading ? 'Commande en cours...' : 'Confirmer et payer'}
      </button>
    </div>
  );
};
ℹ Redux Toolkit vs Redux classique — résumé
  • Redux classique : créer les types d'actions manuellement, créer les action creators, écrire les reducers avec spread immutable. ~100 lignes pour un slice simple.
  • Redux Toolkit : createSlice génère les types et action creators automatiquement, Immer gère l'immutabilité. ~40 lignes pour le même slice.
  • Règle : tout nouveau projet utilise RTK. Redux classique = legacy uniquement.

✓
Vérification des acquis
5 questions — corrigé intégré
Dans un reducer RTK, on écrit state.articles.push(nouveauPlat). Comment est-ce possible si Redux impose l'immutabilité ?
createAsyncThunk('panier/passerCommande', ...) génère combien et quelles actions ?
Dans extraReducers, pourquoi utilise-t-on le builder pattern (builder.addCase) plutôt que switch/case ?
Après un dispatch de passerCommande, comment savoir si l'action a réussi ou échoué pour naviguer vers la page de suivi uniquement en cas de succès ?
Quels middlewares configureStore intègre-t-il automatiquement sans configuration ?
Projet Final — DariGo Front-end Architecture · React · API · Vite
⏱ 15 min

DariGo Front-end — Présentation & Architecture

Le back-end DariGo est opérationnel — 5 microservices Node.js, Nginx, RabbitMQ, MongoDB, Docker Compose. Il est maintenant temps de construire l'interface cliente : une SPA React qui consomme ces services via le gateway Nginx, affiche les restaurants et plats, permet de passer une commande, de suivre sa livraison en temps réel, et reçoit les notifications. Ce projet mobilise toutes les compétences du module React : composants, hooks, state management Redux, React Query, React Router et les formulaires.

ℹ Compatibilité avec le back-end Cloud Native

Ce front-end React se connecte au back-end DariGo construit dans le module Cloud Native. Toutes les URLs, les formats JSON, les tokens JWT et les codes de statut utilisés ici correspondent exactement aux endpoints implémentés dans les 5 microservices. Si vous n'avez pas encore suivi le module Cloud Native, vous pouvez utiliser les données mockées fournies dans chaque section pour développer et tester le front-end de manière autonome.


1
Architecture complète — Back-end + Front-end
Nginx Gateway · Services · React SPA · Flux de données
text
┌─────────────────────────────────────────────────────────┐
│               DARIGO — ARCHITECTURE COMPLÈTE             │
├─────────────────────────────────────────────────────────┤
│                                                          │
│   darigo-frontend (React + Vite)                         │
│   http://localhost:5173    ←── Navigateur client         │
│         │                                                │
│         │ HTTP (fetch / Axios)                           │
│         ▼                                                │
│   NGINX :80  ←── Gateway reverse proxy                   │
│   /auth       → auth-service:3001                        │
│   /cuisines   → cuisine-service:3002                     │
│   /commandes  → commande-service:3003                    │
│   /livraisons → livraison-service:3004                   │
│   /notifs     → notif-service:3005                       │
│                                                          │
│   RABBITMQ :5672  ←── Messages asynchrones               │
│   exchange darigo (fanout)                               │
│   ├── queue-notifs   → notif-service (consumer)          │
│   └── queue-livreurs → livraison-service (consumer)      │
│                                                          │
│   MONGODB                                                │
│   ├── bddarigo-auth       (utilisateurs)                 │
│   ├── bddarigo-cuisines   (restaurants + plats)          │
│   ├── bddarigo-commandes  (commandes)                    │
│   ├── bddarigo-livraisons (missions livreurs)            │
│   └── bddarigo-notifs     (historique événements)        │
│                                                          │
└─────────────────────────────────────────────────────────┘

Flux d'une commande DariGo :
① Client React → POST /commandes (token JWT)
② commande-service → vérifie token via auth-service GET /auth/verify
③ commande-service → vérifie restaurant via cuisine-service GET /cuisines/:id
④ commande-service → insère en MongoDB, publie événement RabbitMQ
⑤ livraison-service (consumer) → crée mission "disponible"
⑥ notif-service (consumer) → enregistre la notification
⑦ React → polling GET /livraisons/:id → affiche le statut en temps réel

2
Initialiser le projet React DariGo
Vite · Dépendances · Structure · vite.config · Variables d'env
bash
# ── Créer le projet ───────────────────────────────────────────
npm create vite@latest darigo-frontend -- --template react
cd darigo-frontend
npm install

# ── Dépendances du projet ─────────────────────────────────────
npm install react-router-dom              # Routage
npm install @reduxjs/toolkit react-redux  # État global (panier, auth)
npm install @tanstack/react-query         # Chargement et cache des données
npm install axios                         # Client HTTP
npm install react-hook-form               # Formulaires
npm install zod @hookform/resolvers       # Validation des formulaires
text
darigo-frontend/
├── public/
│   └── images/          # Placeholders des plats
│
├── src/
│   ├── main.jsx          # Point d'entrée (Provider, QueryClient, BrowserRouter)
│   ├── App.jsx           # Routes React Router
│   │
│   ├── components/       # Composants réutilisables
│   │   ├── CartePlat.jsx
│   │   ├── CarteRestaurant.jsx
│   │   ├── BadgeStatut.jsx
│   │   ├── SuiviLivraison.jsx
│   │   ├── Header.jsx
│   │   └── ProtectedRoute.jsx
│   │
│   ├── pages/            # Pages (une par route)
│   │   ├── PageAccueil.jsx
│   │   ├── PageRestaurants.jsx
│   │   ├── PageMenu.jsx
│   │   ├── PageLogin.jsx
│   │   ├── PageRegister.jsx
│   │   ├── PageCommande.jsx
│   │   ├── PageSuivi.jsx
│   │   └── PageProfil.jsx
│   │
│   ├── services/         # Appels API — un fichier par microservice
│   │   ├── api.js        # Instance Axios + intercepteur JWT
│   │   ├── authService.js
│   │   ├── cuisineService.js
│   │   ├── commandeService.js
│   │   ├── livraisonService.js
│   │   └── notifService.js
│   │
│   ├── store/            # Redux Toolkit
│   │   ├── index.js
│   │   ├── panierSlice.js
│   │   └── authSlice.js
│   │
│   └── hooks/            # Hooks personnalisés
│       ├── useAuth.js
│       └── usePanier.js
│
├── .env.local            # Variables d'environnement locales
├── .env.production       # Variables de production
└── vite.config.js
javascript
// ── vite.config.js ────────────────────────────────────────────
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@'           : path.resolve(__dirname, './src'),
      '@components' : path.resolve(__dirname, './src/components'),
      '@pages'      : path.resolve(__dirname, './src/pages'),
      '@services'   : path.resolve(__dirname, './src/services'),
      '@store'      : path.resolve(__dirname, './src/store'),
      '@hooks'      : path.resolve(__dirname, './src/hooks'),
    }
  },
  server: {
    port: 5173,
    proxy: {
      // En développement, proxy vers Nginx qui redistribue aux microservices
      '/auth'      : { target: 'http://localhost:80', changeOrigin: true },
      '/cuisines'  : { target: 'http://localhost:80', changeOrigin: true },
      '/commandes' : { target: 'http://localhost:80', changeOrigin: true },
      '/livraisons': { target: 'http://localhost:80', changeOrigin: true },
      '/notifs'    : { target: 'http://localhost:80', changeOrigin: true },
    }
  }
});

// ── .env.local (développement — ne pas commiter) ──────────────
// VITE_API_URL=http://localhost:80
// VITE_POLLING_INTERVAL=10000

// ── .env.production ───────────────────────────────────────────
// VITE_API_URL=https://api.darigo.ma
javascript
// ── src/main.jsx — point d'entrée complet ─────────────────────
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import store from './store';
import './index.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 2,   // 2 min — menus changent rarement
      retry: 1,
      refetchOnWindowFocus: false, // éviter les appels trop fréquents
    }
  }
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </QueryClientProvider>
    </Provider>
  </React.StrictMode>
);
Projet Final — Étape 1 Auth UI · JWT · Redux · authSlice
⏱ 25 min

Étape 1 — Auth UI — consommer auth-service

L'auth-service expose trois endpoints : POST /auth/register, POST /auth/login (retourne un JWT), et GET /auth/verify (utilisé uniquement en inter-services). Le front-end React doit s'inscrire, se connecter, stocker le JWT, l'envoyer avec chaque requête protégée via l'intercepteur Axios, et gérer la déconnexion. L'état d'authentification vit dans Redux pour être accessible par le Header, les routes protégées et tous les services.

✓ Ce que vous allez construire
  • Instance Axios centrale avec intercepteur JWT automatique
  • authSlice Redux : login, logout, persistance localStorage
  • Page inscription — formulaire avec React Hook Form + Zod
  • Page connexion — formulaire avec gestion d'erreur API
  • ProtectedRoute — redirection automatique vers /login
  • Header — affiche l'état connecté/déconnecté depuis Redux

1
Couche API — Axios + authService
Instance · Intercepteur JWT · authService
javascript
// ── src/services/api.js — instance Axios centrale ─────────────
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:80',
  timeout: 8000,
  headers: { 'Content-Type': 'application/json' }
});

// Ajouter le token JWT à chaque requête automatiquement
api.interceptors.request.use(config => {
  const token = localStorage.getItem('darigo_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Gérer les erreurs HTTP globalement
api.interceptors.response.use(
  response => response.data,   // retourner directement les données
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('darigo_token');
      window.location.href = '/login';
    }
    const message = error.response?.data?.erreur
      || error.response?.data?.message
      || `Erreur ${error.response?.status || 'réseau'}`;
    return Promise.reject(new Error(message));
  }
);

export default api;

// ── src/services/authService.js ───────────────────────────────
import api from './api';

export const authService = {
  // POST /auth/register
  // Body : { nom, email, motDePasse, role? }
  // Réponse : { message: "Compte cree", userId: "..." }
  register: (data) => api.post('/auth/register', data),

  // POST /auth/login
  // Body : { email, motDePasse }
  // Réponse : { message: "Connexion reussie", token: "eyJ..." }
  login: async (email, motDePasse) => {
    const data = await api.post('/auth/login', { email, motDePasse });
    // Stocker le JWT — sera envoyé automatiquement par l'intercepteur
    localStorage.setItem('darigo_token', data.token);
    return data;
  },

  logout: () => {
    localStorage.removeItem('darigo_token');
  },

  // Décoder le JWT localement pour obtenir les infos utilisateur
  // ⚠ Pas de vérification de signature — uniquement pour l'affichage UI
  getUtilisateurLocal: () => {
    const token = localStorage.getItem('darigo_token');
    if (!token) return null;
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      // Vérifier l'expiration
      if (payload.exp * 1000 < Date.now()) {
        localStorage.removeItem('darigo_token');
        return null;
      }
      return { userId: payload.userId, nom: payload.nom, email: payload.email, role: payload.role };
    } catch {
      return null;
    }
  }
};

2
authSlice Redux — état d'authentification global
createSlice · Persistance · Sélecteurs · store/index.js
javascript
// ── src/store/authSlice.js ─────────────────────────────────────
import { createSlice } from '@reduxjs/toolkit';
import { authService } from '../services/authService';

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    // Récupérer l'utilisateur depuis le token stocké au démarrage
    utilisateur: authService.getUtilisateurLocal(),
    isAuthentifie: !!authService.getUtilisateurLocal(),
  },
  reducers: {
    connexionReussie: (state, action) => {
      // action.payload = { userId, nom, email, role } — décodé du JWT
      state.utilisateur   = action.payload;
      state.isAuthentifie = true;
    },
    deconnexion: (state) => {
      authService.logout(); // supprimer le JWT de localStorage
      state.utilisateur   = null;
      state.isAuthentifie = false;
    }
  }
});

export const { connexionReussie, deconnexion } = authSlice.actions;

// Sélecteurs
export const selUtilisateur    = state => state.auth.utilisateur;
export const selIsAuthentifie  = state => state.auth.isAuthentifie;
export const selRole           = state => state.auth.utilisateur?.role;
export const selEstLivreur     = state => state.auth.utilisateur?.role === 'livreur';
export const selEstAdmin       = state => state.auth.utilisateur?.role === 'admin';

export default authSlice.reducer;

// ── src/store/index.js ────────────────────────────────────────
import { configureStore } from '@reduxjs/toolkit';
import authReducer  from './authSlice';
import panierReducer from './panierSlice'; // voir étape cuisine

const store = configureStore({
  reducer: {
    auth:   authReducer,
    panier: panierReducer,
  }
});

export default store;

3
Pages Login & Register
React Hook Form · Zod · dispatch · navigate
javascript
// ── src/pages/PageLogin.jsx ───────────────────────────────────
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useDispatch } from 'react-redux';
import { useNavigate, Link } from 'react-router-dom';
import { authService } from '../services/authService';
import { connexionReussie } from '../store/authSlice';

const schemaLogin = z.object({
  email:      z.string().email('Email invalide'),
  motDePasse: z.string().min(1, 'Mot de passe requis'),
});

const PageLogin = () => {
  const dispatch  = useDispatch();
  const navigate  = useNavigate();

  const { register, handleSubmit, setError, formState: { errors, isSubmitting } } = useForm({
    resolver: zodResolver(schemaLogin)
  });

  const onSubmit = async ({ email, motDePasse }) => {
    try {
      // POST /auth/login → { message, token }
      await authService.login(email, motDePasse);
      // Décoder le JWT pour obtenir les infos et les stocker dans Redux
      const utilisateur = authService.getUtilisateurLocal();
      dispatch(connexionReussie(utilisateur));
      navigate('/restaurants');
    } catch (err) {
      // Afficher le message d'erreur du back-end (ex: "Identifiants incorrects")
      setError('root', { message: err.message });
    }
  };

  return (
    <div className="page-auth">
      <div className="auth-card">
        <img src="/logo.svg" alt="DariGo" className="auth-logo" />
        <h1>Se connecter</h1>

        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          {errors.root && (
            <div className="alerte alerte-erreur" role="alert">
              {errors.root.message}
            </div>
          )}

          <div className="champ">
            <label htmlFor="email">Email</label>
            <input id="email" type="email" {...register('email')}
              placeholder="khalid@example.com" />
            {errors.email && <span className="msg-erreur">{errors.email.message}</span>}
          </div>

          <div className="champ">
            <label htmlFor="motDePasse">Mot de passe</label>
            <input id="motDePasse" type="password" {...register('motDePasse')} />
            {errors.motDePasse && <span className="msg-erreur">{errors.motDePasse.message}</span>}
          </div>

          <button type="submit" disabled={isSubmitting} className="btn btn-primary btn-full">
            {isSubmitting ? 'Connexion...' : 'Se connecter'}
          </button>
        </form>

        <p className="auth-lien">
          Pas encore de compte ? <Link to="/register">S'inscrire</Link>
        </p>
      </div>
    </div>
  );
};

export default PageLogin;

// ── src/pages/PageRegister.jsx (extrait) ─────────────────────
const schemaRegister = z.object({
  nom:          z.string().min(2, 'Minimum 2 caractères'),
  email:        z.string().email('Email invalide'),
  motDePasse:   z.string().min(6, 'Minimum 6 caractères'),
  confirmation: z.string(),
  role:         z.enum(['client', 'livreur']).default('client'),
}).refine(d => d.motDePasse === d.confirmation, {
  message: 'Les mots de passe ne correspondent pas',
  path: ['confirmation']
});

// POST /auth/register — Body : { nom, email, motDePasse, role }
// Réponse succès : { message: "Compte cree", userId: "..." }
// Réponse erreur : { erreur: "Email deja utilise" } → status 409

4
Header & ProtectedRoute
useSelector · deconnexion · Navigate · Outlet
javascript
// ── src/components/Header.jsx ─────────────────────────────────
import { Link, NavLink } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { selUtilisateur, selIsAuthentifie, deconnexion } from '../store/authSlice';
import { selNbArticles } from '../store/panierSlice';

const Header = () => {
  const dispatch       = useDispatch();
  const utilisateur    = useSelector(selUtilisateur);
  const isAuthentifie  = useSelector(selIsAuthentifie);
  const nbArticles     = useSelector(selNbArticles);

  return (
    <header className="header">
      <Link to="/" className="header-logo">
        <img src="/logo.svg" alt="DariGo" />
      </Link>

      <nav className="header-nav">
        <NavLink to="/restaurants" className={({ isActive }) => isActive ? 'actif' : ''}>
          Restaurants
        </NavLink>
        {isAuthentifie && (
          <NavLink to="/mes-commandes" className={({ isActive }) => isActive ? 'actif' : ''}>
            Mes commandes
          </NavLink>
        )}
      </nav>

      <div className="header-actions">
        {isAuthentifie ? (
          <>
            <Link to="/commande" className="btn-panier">
              Panier {nbArticles > 0 && <span className="badge-nb">{nbArticles}</span>}
            </Link>
            <span className="header-user">{utilisateur?.nom}</span>
            <button onClick={() => dispatch(deconnexion())} className="btn btn-ghost">
              Déconnexion
            </button>
          </>
        ) : (
          <Link to="/login" className="btn btn-primary">Se connecter</Link>
        )}
      </div>
    </header>
  );
};

// ── src/components/ProtectedRoute.jsx ────────────────────────
import { Navigate, Outlet } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { selIsAuthentifie } from '../store/authSlice';

const ProtectedRoute = ({ rolesAutorises }) => {
  const isAuthentifie = useSelector(selIsAuthentifie);
  const utilisateur   = useSelector(state => state.auth.utilisateur);

  if (!isAuthentifie) return <Navigate to="/login" replace />;

  if (rolesAutorises && !rolesAutorises.includes(utilisateur?.role)) {
    return <Navigate to="/" replace />;
  }

  return <Outlet />;
};

// ── src/App.jsx — routes complètes ───────────────────────────
const App = () => (
  <>
    <Header />
    <Routes>
      <Route path="/"              element={<PageAccueil />} />
      <Route path="/restaurants"   element={<PageRestaurants />} />
      <Route path="/restaurants/:id/menu" element={<PageMenu />} />
      <Route path="/login"         element={<PageLogin />} />
      <Route path="/register"      element={<PageRegister />} />

      <Route element={<ProtectedRoute />}>
        <Route path="/commande"        element={<PageCommande />} />
        <Route path="/suivi/:id"       element={<PageSuivi />} />
        <Route path="/mes-commandes"   element={<PageMesCommandes />} />
        <Route path="/profil"          element={<PageProfil />} />
      </Route>

      <Route element={<ProtectedRoute rolesAutorises={['livreur', 'admin']} />}>
        <Route path="/livraisons"  element={<PageLivraisons />} />
      </Route>

      <Route path="*" element={<Navigate to="/" replace />} />
    </Routes>
  </>
);
Projet Final — Étape 2 Cuisine UI · React Query · Restaurants · Plats · Panier
⏱ 25 min

Étape 2 — Cuisine UI — consommer cuisine-service

Le cuisine-service expose les restaurants et leurs plats via des routes REST publiques (GET) et protégées (POST/PUT/DELETE). Le front-end React affiche la liste des restaurants avec pagination et filtrage par ville, le menu d'un restaurant avec les plats disponibles, et permet d'ajouter des plats au panier (état Redux). React Query gère le cache des données — les menus ne changent pas souvent et peuvent rester en cache plusieurs minutes.


1
cuisineService + panierSlice Redux
GET /cuisines · GET /cuisines/:id/plats · Redux Toolkit · Immer
javascript
// ── src/services/cuisineService.js ────────────────────────────
import api from './api';

export const cuisineService = {
  // GET /cuisines?ville=Casablanca&page=1&limit=12
  // Réponse : { restaurants: [...], total: 42, pages: 4 }
  getRestaurants: (params = {}) => api.get('/cuisines', { params }),

  // GET /cuisines/:id
  // Réponse : { _id, nom, adresse, ville, cuisine, note, imageUrl }
  getRestaurant: (id) => api.get(`/cuisines/${id}`),

  // GET /cuisines/:restaurantId/plats
  // Réponse : [{ _id, nom, description, prix, disponible, categorie }]
  getPlats: (restaurantId) => api.get(`/cuisines/${restaurantId}/plats`),

  // POST /cuisines — role restaurateur ou admin
  creerRestaurant: (data) => api.post('/cuisines', data),

  // POST /cuisines/:id/plats — role restaurateur ou admin
  creerPlat: (restaurantId, data) => api.post(`/cuisines/${restaurantId}/plats`, data),

  // PUT /cuisines/:id — role restaurateur ou admin
  majRestaurant: (id, data) => api.put(`/cuisines/${id}`, data),
};

// ── src/store/panierSlice.js ──────────────────────────────────
import { createSlice } from '@reduxjs/toolkit';

const panierSlice = createSlice({
  name: 'panier',
  initialState: {
    articles:       [],    // [{ _id, nom, prix, quantite, restaurantId, restaurantNom }]
    restaurantId:   null,  // Panier limité à un seul restaurant (contrainte métier)
    restaurantNom:  '',
    nbArticles:     0,
    total:          0,
  },
  reducers: {
    ajouterArticle: (state, action) => {
      const plat = action.payload;

      // Vider le panier si on change de restaurant
      if (state.restaurantId && state.restaurantId !== plat.restaurantId) {
        state.articles = [];
      }
      state.restaurantId  = plat.restaurantId;
      state.restaurantNom = plat.restaurantNom;

      const existe = state.articles.find(a => a._id === plat._id);
      if (existe) {
        existe.quantite += 1;  // Immer — mutation autorisée
      } else {
        state.articles.push({ ...plat, quantite: 1 });
      }

      state.nbArticles = state.articles.reduce((acc, a) => acc + a.quantite, 0);
      state.total      = state.articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
    },

    retirerArticle: (state, action) => {
      state.articles = state.articles.filter(a => a._id !== action.payload);
      if (state.articles.length === 0) state.restaurantId = null;
      state.nbArticles = state.articles.reduce((acc, a) => acc + a.quantite, 0);
      state.total      = state.articles.reduce((acc, a) => acc + a.prix * a.quantite, 0);
    },

    vider: (state) => {
      state.articles      = [];
      state.restaurantId  = null;
      state.restaurantNom = '';
      state.nbArticles    = 0;
      state.total         = 0;
    }
  }
});

export const { ajouterArticle, retirerArticle, vider } = panierSlice.actions;
export const selArticles     = state => state.panier.articles;
export const selTotal        = state => state.panier.total;
export const selNbArticles   = state => state.panier.nbArticles;
export const selRestaurantId = state => state.panier.restaurantId;
export default panierSlice.reducer;

2
PageRestaurants + PageMenu
useQuery · useSearchParams · CartePlat · dispatch
javascript
// ── src/pages/PageRestaurants.jsx ─────────────────────────────
import { useQuery } from '@tanstack/react-query';
import { useSearchParams, Link } from 'react-router-dom';
import { cuisineService } from '../services/cuisineService';

const VILLES = ['Casablanca', 'Rabat', 'Fès', 'Marrakech', 'Agadir', 'Tanger'];

const PageRestaurants = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const ville = searchParams.get('ville') || 'Casablanca';
  const page  = parseInt(searchParams.get('page') || '1', 10);

  // GET /cuisines?ville=Casablanca&page=1&limit=12
  const { data, isLoading, isError } = useQuery({
    queryKey: ['restaurants', ville, page],
    queryFn:  () => cuisineService.getRestaurants({ ville, page, limit: 12 }),
    keepPreviousData: true, // garder les données pendant le chargement de la page suivante
  });

  if (isLoading) return <div className="spinner">Chargement des restaurants...</div>;
  if (isError)   return <div className="erreur">Impossible de charger les restaurants</div>;

  return (
    <div className="page-restaurants">
      <h1>Restaurants à {ville}</h1>

      {/* Filtres par ville */}
      <div className="filtres-ville">
        {VILLES.map(v => (
          <button
            key={v}
            className={`filtre-btn ${ville === v ? 'actif' : ''}`}
            onClick={() => setSearchParams({ ville: v, page: '1' })}
          >
            {v}
          </button>
        ))}
      </div>

      {/* Grille des restaurants */}
      <div className="grille-restaurants">
        {data?.restaurants?.map(r => (
          <Link key={r._id} to={`/restaurants/${r._id}/menu`} className="carte-restaurant">
            <img
              src={r.imageUrl || '/images/restaurant-placeholder.jpg'}
              alt={r.nom}
              loading="lazy"
              onError={e => { e.target.src = '/images/restaurant-placeholder.jpg'; }}
            />
            <div className="carte-restaurant__infos">
              <h3>{r.nom}</h3>
              <p>{r.cuisine} · {r.adresse}</p>
              <span className="note">★ {r.note?.toFixed(1) || '—'}</span>
            </div>
          </Link>
        ))}
      </div>

      {/* Pagination */}
      {data?.pages > 1 && (
        <div className="pagination">
          {Array.from({ length: data.pages }, (_, i) => i + 1).map(p => (
            <button
              key={p}
              className={`page-btn ${page === p ? 'actif' : ''}`}
              onClick={() => setSearchParams({ ville, page: String(p) })}
            >
              {p}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

// ── src/pages/PageMenu.jsx ────────────────────────────────────
import { useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { ajouterArticle, selRestaurantId } from '../store/panierSlice';

const PageMenu = () => {
  const { id: restaurantId } = useParams();
  const dispatch              = useDispatch();
  const panierRestaurantId    = useSelector(selRestaurantId);

  // GET /cuisines/:id
  const { data: restaurant } = useQuery({
    queryKey: ['restaurant', restaurantId],
    queryFn:  () => cuisineService.getRestaurant(restaurantId),
  });

  // GET /cuisines/:id/plats
  const { data: plats = [], isLoading } = useQuery({
    queryKey: ['plats', restaurantId],
    queryFn:  () => cuisineService.getPlats(restaurantId),
    staleTime: 1000 * 60 * 5, // 5 min — menus stables
  });

  const [categorieActive, setCategorieActive] = useState('tous');

  const categories = [...new Set(plats.map(p => p.categorie))];
  const platsFiltres = plats.filter(p =>
    categorieActive === 'tous' || p.categorie === categorieActive
  );

  const handleAjouter = (plat) => {
    // Prévenir si le panier contient des plats d'un autre restaurant
    if (panierRestaurantId && panierRestaurantId !== restaurantId) {
      if (!confirm('Votre panier contient des plats d\'un autre restaurant. Le vider ?')) return;
    }
    dispatch(ajouterArticle({
      ...plat,
      restaurantId,
      restaurantNom: restaurant?.nom
    }));
  };

  if (isLoading) return <div className="spinner">Chargement du menu...</div>;

  return (
    <div className="page-menu">
      <div className="menu-header">
        <h1>{restaurant?.nom}</h1>
        <p>{restaurant?.cuisine} · {restaurant?.adresse}</p>
      </div>

      {/* Filtres par catégorie */}
      <div className="filtres-categorie">
        {['tous', ...categories].map(cat => (
          <button
            key={cat}
            className={`filtre-btn ${categorieActive === cat ? 'actif' : ''}`}
            onClick={() => setCategorieActive(cat)}
          >
            {cat}
          </button>
        ))}
      </div>

      {/* Liste des plats */}
      <div className="grille-plats">
        {platsFiltres.map(plat => (
          <div key={plat._id} className={`carte-plat ${!plat.disponible ? 'grise' : ''}`}>
            <div className="carte-plat__corps">
              <h3>{plat.nom}</h3>
              <p className="description">{plat.description}</p>
              <div className="carte-plat__footer">
                <strong>{plat.prix} MAD</strong>
                <button
                  onClick={() => handleAjouter(plat)}
                  disabled={!plat.disponible}
                  className="btn btn-primary btn-sm"
                >
                  {plat.disponible ? '+ Ajouter' : 'Indisponible'}
                </button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};
Projet Final — Étape 3 Commande UI · useMutation · JWT · RabbitMQ
⏱ 25 min

Étape 3 — Commande UI — consommer commande-service

Le commande-service est le cœur de DariGo. Un POST /commandes orchestre auth-service et cuisine-service, insère la commande en MongoDB, puis publie un événement RabbitMQ qui déclenchera livraison-service et notif-service. Le front-end React envoie cette requête avec le JWT, affiche la confirmation, puis navigue vers le suivi. Il affiche aussi l'historique des commandes de l'utilisateur connecté.


1
commandeService + PageCommande
POST /commandes · useMutation · vider le panier · navigate
javascript
// ── src/services/commandeService.js ──────────────────────────
import api from './api';

export const commandeService = {
  // POST /commandes — AUTHENTIFIÉ (token JWT requis)
  // Body : { restaurantId, articles: [{platId, nom, prix, quantite}], adresseLivraison, villeLivraison }
  // Réponse : { message, commandeId, statut: "en_attente" }
  // ⚠ commande-service vérifie auth ET cuisine avant d'insérer
  creer: (data) => api.post('/commandes', data),

  // GET /commandes — AUTHENTIFIÉ — commandes du client connecté
  // Réponse : [{ _id, restaurantNom, total, statut, createdAt, articles }]
  getMesCommandes: (params = {}) => api.get('/commandes', { params }),

  // GET /commandes/:id — AUTHENTIFIÉ
  getById: (id) => api.get(`/commandes/${id}`),

  // PATCH /commandes/:id/statut — AUTHENTIFIÉ
  // Body : { statut: "confirmee" | "en_preparation" | "prete" | "en_livraison" | "livree" | "annulee" }
  majStatut: (id, statut) => api.patch(`/commandes/${id}/statut`, { statut }),
};

// ── src/pages/PageCommande.jsx ────────────────────────────────
import { useSelector, useDispatch } from 'react-redux';
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { selArticles, selTotal, selRestaurantId, selNbArticles, vider } from '../store/panierSlice';
import { commandeService } from '../services/commandeService';

const schemaAdresse = z.object({
  adresseLivraison: z.string().min(10, 'Adresse trop courte (min. 10 caractères)'),
  villeLivraison:   z.enum(['Casablanca','Rabat','Fès','Marrakech','Agadir','Tanger']),
  telephone:        z.string().regex(/^(\+212|0)[5-7]\d{8}$/, 'Numéro marocain invalide'),
  instructions:     z.string().max(200).optional(),
});

const PageCommande = () => {
  const dispatch       = useDispatch();
  const navigate       = useNavigate();
  const articles       = useSelector(selArticles);
  const total          = useSelector(selTotal);
  const nbArticles     = useSelector(selNbArticles);
  const restaurantId   = useSelector(selRestaurantId);

  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schemaAdresse),
    defaultValues: { villeLivraison: 'Casablanca' }
  });

  const mutation = useMutation({
    mutationFn: (adresse) => commandeService.creer({
      restaurantId,
      articles: articles.map(a => ({
        platId:   a._id,
        nom:      a.nom,
        prix:     a.prix,
        quantite: a.quantite,
      })),
      adresseLivraison: adresse.adresseLivraison,
      villeLivraison:   adresse.villeLivraison,
    }),
    onSuccess: (data) => {
      // Vider le panier Redux après commande réussie
      dispatch(vider());
      // Naviguer vers le suivi — commande-service retourne commandeId
      navigate(`/suivi/${data.commandeId}`);
    },
  });

  if (articles.length === 0) {
    return (
      <div className="page-commande page-commande--vide">
        <p>Votre panier est vide</p>
        <button onClick={() => navigate('/restaurants')} className="btn btn-primary">
          Voir les restaurants
        </button>
      </div>
    );
  }

  return (
    <div className="page-commande">
      <h1>Confirmer votre commande</h1>

      {/* Récapitulatif panier */}
      <section className="recap-panier">
        <h2>Votre panier ({nbArticles} article(s))</h2>
        {articles.map(a => (
          <div key={a._id} className="ligne-panier">
            <span>{a.nom} × {a.quantite}</span>
            <span>{(a.prix * a.quantite).toFixed(2)} MAD</span>
          </div>
        ))}
        <div className="total-panier">
          <strong>Sous-total : {total.toFixed(2)} MAD</strong>
          <span>+ Livraison : 15 MAD</span>
          <strong className="total-final">Total : {(total + 15).toFixed(2)} MAD</strong>
        </div>
      </section>

      {/* Formulaire d'adresse */}
      <form onSubmit={handleSubmit(data => mutation.mutate(data))} noValidate>
        <h2>Adresse de livraison</h2>

        {mutation.isError && (
          <div className="alerte alerte-erreur">{mutation.error.message}</div>
        )}

        <div className="champ">
          <label htmlFor="adresse">Adresse complète</label>
          <input id="adresse" {...register('adresseLivraison')} placeholder="123 Rue Mohammed V, Maarif" />
          {errors.adresseLivraison && <span className="msg-erreur">{errors.adresseLivraison.message}</span>}
        </div>

        <div className="champ">
          <label htmlFor="ville">Ville</label>
          <select id="ville" {...register('villeLivraison')}>
            {['Casablanca','Rabat','Fès','Marrakech','Agadir','Tanger'].map(v => (
              <option key={v} value={v}>{v}</option>
            ))}
          </select>
        </div>

        <div className="champ">
          <label htmlFor="tel">Téléphone</label>
          <input id="tel" type="tel" {...register('telephone')} placeholder="06 12 34 56 78" />
          {errors.telephone && <span className="msg-erreur">{errors.telephone.message}</span>}
        </div>

        <button
          type="submit"
          disabled={mutation.isPending}
          className="btn btn-primary btn-lg btn-full"
        >
          {mutation.isPending ? 'Commande en cours...' : `Commander — ${(total + 15).toFixed(2)} MAD`}
        </button>
      </form>
    </div>
  );
};
Projet Final — Étape 4 Livraison UI · Polling · BadgeStatut · Livreur
⏱ 22 min

Étape 4 — Livraison UI — consommer livraison-service

Le livraison-service crée une mission "disponible" à la réception de l'événement RabbitMQ commande.creee. Un livreur peut l'accepter via PATCH /livraisons/:id/accepter, puis mettre à jour le statut en en_route et livree. Le front-end React implémente deux vues : la page de suivi pour le client (polling toutes les 10 secondes), et la page des missions disponibles pour les livreurs.


1
livraisonService + PageSuivi (client)
GET /commandes/:id · Polling useEffect · BadgeStatut · Timeline
javascript
// ── src/services/livraisonService.js ──────────────────────────
import api from './api';

export const livraisonService = {
  // GET /livraisons — AUTHENTIFIÉ
  // Pour un livreur : ses missions acceptées + missions disponibles
  // Pour un admin : toutes les missions
  getMissions: (params = {}) => api.get('/livraisons', { params }),

  // PATCH /livraisons/:id/accepter — rôle livreur uniquement
  // Met le statut de la mission à "acceptee" atomiquement
  // Retourne : { message: "Mission acceptee" }
  accepterMission: (id) => api.patch(`/livraisons/${id}/accepter`),

  // PATCH /livraisons/:id/statut — rôle livreur ou admin
  // Body : { statut: "en_route" | "livree" | "annulee" }
  majStatut: (id, statut) => api.patch(`/livraisons/${id}/statut`, { statut }),
};

// ── src/components/BadgeStatut.jsx ────────────────────────────
// Commun aux commandes et aux livraisons — statuts compatibles
const CONFIG_STATUT = {
  en_attente:    { label: 'En attente',     classe: 'warning',  icone: '⏳' },
  confirmee:     { label: 'Confirmée',      classe: 'info',     icone: '✓'  },
  en_preparation:{ label: 'En préparation', classe: 'primary',  icone: '👨‍🍳' },
  prete:         { label: 'Prête',          classe: 'accent',   icone: '🍽'  },
  disponible:    { label: 'En attente livreur', classe: 'warning', icone: '🛵' },
  acceptee:      { label: 'Livreur assigné',classe: 'info',     icone: '🛵'  },
  en_livraison:  { label: 'En livraison',   classe: 'primary',  icone: '🛵'  },
  en_route:      { label: 'En route',       classe: 'primary',  icone: '🛵'  },
  livree:        { label: 'Livrée ✓',       classe: 'success',  icone: '✓'  },
  annulee:       { label: 'Annulée',        classe: 'danger',   icone: '✕'  },
};

export const BadgeStatut = ({ statut }) => {
  const cfg = CONFIG_STATUT[statut] || { label: statut, classe: 'default', icone: '?' };
  return (
    <span className={`badge-statut badge-statut--${cfg.classe}`}>
      {cfg.icone} {cfg.label}
    </span>
  );
};

// ── src/pages/PageSuivi.jsx — vue client ─────────────────────
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { commandeService } from '../services/commandeService';
import { BadgeStatut } from '../components/BadgeStatut';

const STATUTS_ACTIFS = ['en_attente','confirmee','en_preparation','prete','en_livraison','en_route'];

const ETAPES = [
  { key: 'en_attente',    label: 'Reçue'         },
  { key: 'confirmee',     label: 'Confirmée'     },
  { key: 'en_preparation',label: 'Préparation'   },
  { key: 'en_livraison',  label: 'En livraison'  },
  { key: 'livree',        label: 'Livrée'        },
];

const PageSuivi = () => {
  const { id: commandeId } = useParams();

  const { data: commande, isLoading, isError } = useQuery({
    queryKey: ['commande', commandeId],
    queryFn:  () => commandeService.getById(commandeId),
    // Polling toutes les 10 secondes si la commande est encore en cours
    refetchInterval: (data) =>
      STATUTS_ACTIFS.includes(data?.statut)
        ? parseInt(import.meta.env.VITE_POLLING_INTERVAL || '10000', 10)
        : false,
  });

  if (isLoading) return <div className="spinner">Chargement du suivi...</div>;
  if (isError)   return <div className="erreur">Commande introuvable</div>;

  const indexActuel = ETAPES.findIndex(e => e.key === commande?.statut);

  return (
    <div className="page-suivi">
      <h1>Suivi commande #{commandeId.slice(-6).toUpperCase()}</h1>

      {/* Badge statut principal */}
      <div className="suivi-statut">
        <BadgeStatut statut={commande?.statut} />
        {STATUTS_ACTIFS.includes(commande?.statut) && (
          <span className="suivi-pulse">Mise à jour auto toutes les 10s</span>
        )}
      </div>

      {/* Timeline des étapes */}
      <div className="timeline">
        {ETAPES.map((etape, i) => (
          <div
            key={etape.key}
            className={`timeline-etape ${
              i < indexActuel ? 'passee' : i === indexActuel ? 'active' : 'future'
            }`}
          >
            <div className="timeline-point" />
            <span className="timeline-label">{etape.label}</span>
          </div>
        ))}
      </div>

      {/* Récapitulatif commande */}
      <section className="suivi-details">
        <h2>Détails</h2>
        <p><strong>Restaurant :</strong> {commande?.restaurantNom}</p>
        <p><strong>Adresse :</strong> {commande?.adresseLivraison}, {commande?.villeLivraison}</p>
        <p><strong>Total :</strong> {commande?.total?.toFixed(2)} MAD</p>

        {commande?.statut === 'livree' && (
          <div className="suivi-livree">
            <p>Votre commande a été livrée. Bon appétit !</p>
            <button onClick={() => navigate('/restaurants')} className="btn btn-primary">
              Commander à nouveau
            </button>
          </div>
        )}
      </section>
    </div>
  );
};

// ── src/pages/PageLivraisons.jsx — vue livreur ───────────────
// Accessible uniquement aux rôles : livreur, admin
const PageLivraisons = () => {
  const queryClient = useQueryClient();

  // GET /livraisons — missions disponibles + missions du livreur connecté
  const { data: missions = [], isLoading } = useQuery({
    queryKey: ['missions'],
    queryFn:  () => livraisonService.getMissions(),
    refetchInterval: 15000, // polling 15s pour les nouvelles missions
  });

  const mutAccepter = useMutation({
    mutationFn: (id) => livraisonService.accepterMission(id),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['missions'] }),
  });

  const mutStatut = useMutation({
    mutationFn: ({ id, statut }) => livraisonService.majStatut(id, statut),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['missions'] }),
  });

  return (
    <div className="page-livraisons">
      <h1>Mes missions</h1>
      {missions.map(mission => (
        <div key={mission._id} className="carte-mission">
          <BadgeStatut statut={mission.statut} />
          <p>{mission.adresseLivraison} — {mission.villeLivraison}</p>
          <p>Commande #{mission.commandeId?.slice(-6).toUpperCase()}</p>

          {mission.statut === 'disponible' && (
            <button
              onClick={() => mutAccepter.mutate(mission._id)}
              disabled={mutAccepter.isPending}
              className="btn btn-primary"
            >
              Accepter la mission
            </button>
          )}

          {mission.statut === 'acceptee' && (
            <button
              onClick={() => mutStatut.mutate({ id: mission._id, statut: 'en_route' })}
              className="btn btn-accent"
            >
              Je suis en route
            </button>
          )}

          {mission.statut === 'en_route' && (
            <button
              onClick={() => mutStatut.mutate({ id: mission._id, statut: 'livree' })}
              className="btn btn-success"
            >
              Confirmer la livraison
            </button>
          )}
        </div>
      ))}
    </div>
  );
};
Projet Final — Étape 5 Notif UI · Historique · Admin · useQuery
⏱ 20 min

Étape 5 — Notifications — consommer notif-service

Le notif-service est un consumer RabbitMQ pur — il enregistre tous les événements de l'exchange DariGo dans MongoDB. Le front-end React affiche l'historique des notifications pour les administrateurs via GET /notifs (route protégée admin). Pour tous les utilisateurs, des notifications toast s'affichent en temps réel quand le statut d'une commande active change — détectées par le polling de la PageSuivi.


1
notifService + PageNotifications (admin)
GET /notifs · Admin only · Tableau des événements
javascript
// ── src/services/notifService.js ──────────────────────────────
import api from './api';

export const notifService = {
  // GET /notifs — ADMIN uniquement
  // Retourne tous les événements enregistrés par le notif-service
  // Réponse : [{ _id, type, commandeId, createdAt, payload }]
  // Types possibles : commande.creee, commande.statut_mis_a_jour,
  //                   livraison.creee, livraison.acceptee, livraison.livree
  getHistorique: (params = {}) => api.get('/notifs', { params }),
};

// ── src/pages/PageNotifications.jsx — admin uniquement ────────
import { useQuery } from '@tanstack/react-query';
import { useSelector } from 'react-redux';
import { selEstAdmin } from '../store/authSlice';
import { notifService } from '../services/notifService';

const CONFIG_TYPE = {
  'commande.creee':             { label: 'Commande créée',    couleur: 'info'    },
  'commande.statut_mis_a_jour': { label: 'Statut commande',   couleur: 'primary' },
  'livraison.creee':            { label: 'Mission créée',     couleur: 'accent'  },
  'livraison.acceptee':         { label: 'Mission acceptée',  couleur: 'success' },
  'livraison.livree':           { label: 'Livraison confirmée', couleur: 'success' },
};

const PageNotifications = () => {
  const estAdmin = useSelector(selEstAdmin);

  const { data: notifs = [], isLoading, refetch } = useQuery({
    queryKey: ['notifs'],
    queryFn:  () => notifService.getHistorique({ limit: 50 }),
    enabled:  estAdmin, // ne charger que si admin
    refetchInterval: 30000, // rafraîchir toutes les 30s
  });

  if (!estAdmin) return <div className="erreur">Accès réservé aux administrateurs</div>;
  if (isLoading) return <div className="spinner">Chargement des notifications...</div>;

  return (
    <div className="page-notifs">
      <div className="page-notifs__header">
        <h1>Historique des événements DariGo</h1>
        <button onClick={() => refetch()} className="btn btn-ghost">↻ Actualiser</button>
      </div>

      <div className="notifs-liste">
        {notifs.map(notif => {
          const cfg = CONFIG_TYPE[notif.type] || { label: notif.type, couleur: 'default' };
          return (
            <div key={notif._id} className={`notif-ligne notif-ligne--${cfg.couleur}`}>
              <span className={`notif-badge badge-${cfg.couleur}`}>{cfg.label}</span>
              <span className="notif-commande">
                Commande #{notif.commandeId?.slice(-6).toUpperCase() || '—'}
              </span>
              <span className="notif-date">
                {new Date(notif.createdAt).toLocaleString('fr-MA', {
                  day: '2-digit', month: '2-digit', year: 'numeric',
                  hour: '2-digit', minute: '2-digit'
                })}
              </span>
              {notif.payload?.statut && (
                <span className="notif-statut">→ {notif.payload.statut}</span>
              )}
            </div>
          );
        })}
        {notifs.length === 0 && (
          <p className="notifs-vide">Aucun événement enregistré</p>
        )}
      </div>
    </div>
  );
};

// ── Notifications toast temps réel — intégration PageSuivi ────
// Le polling de PageSuivi détecte les changements de statut.
// On peut afficher un toast quand le statut change.

// src/hooks/useStatutChangeToast.js
export const useStatutChangeToast = (statut) => {
  const statutPrecedent = useRef(statut);

  useEffect(() => {
    if (statut && statut !== statutPrecedent.current) {
      const cfg = CONFIG_STATUT[statut];
      if (cfg) {
        // Afficher une notification native du navigateur (si permission accordée)
        if (Notification.permission === 'granted') {
          new Notification('DariGo — Mise à jour commande', {
            body: cfg.label,
            icon: '/favicon.ico'
          });
        }
      }
      statutPrecedent.current = statut;
    }
  }, [statut]);
};

// Demander la permission au montage de l'app
useEffect(() => {
  if ('Notification' in window && Notification.permission === 'default') {
    Notification.requestPermission();
  }
}, []);

2
Récapitulatif — DariGo Front-end complet
Architecture finale · Checklist · Déploiement
Page / ComposantService consomméEndpoints utilisésAuth requise
PageLoginauth-servicePOST /auth/loginNon
PageRegisterauth-servicePOST /auth/registerNon
HeaderRedux store——
ProtectedRouteRedux store—Oui
PageRestaurantscuisine-serviceGET /cuisinesNon
PageMenucuisine-serviceGET /cuisines/:id, GET /cuisines/:id/platsNon
PageCommandecommande-servicePOST /commandesOui
PageSuivicommande-serviceGET /commandes/:id (polling)Oui
PageMesCommandescommande-serviceGET /commandesOui
PageLivraisonslivraison-serviceGET /livraisons, PATCH /accepter, PATCH /statutLivreur
PageNotificationsnotif-serviceGET /notifsAdmin
bash
# ── Lancer l'application complète DariGo ─────────────────────

# 1. Démarrer le back-end (depuis le dossier darigo/)
docker-compose up -d

# Vérifier que tous les services sont up
curl http://localhost/auth/health      # {"service":"auth-service","statut":"actif"}
curl http://localhost/cuisines/health  # {"service":"cuisine-service","statut":"actif"}

# 2. Démarrer le front-end React
cd darigo-frontend
npm run dev

# L'application est disponible sur http://localhost:5173
# Les requêtes /auth, /cuisines, /commandes, /livraisons, /notifs
# sont proxifiées vers Nginx (port 80) par Vite

# 3. Créer un compte de test
# Via l'interface : http://localhost:5173/register
# Ou via Postman : POST http://localhost/auth/register
# { "nom": "Khalid El Amrani", "email": "khalid@test.ma",
#   "motDePasse": "test1234", "role": "client" }

# 4. Build pour la production
npm run build
# → génère le dossier dist/ — à servir via Nginx ou Azure Static Web Apps
✓ Checklist projet DariGo Front-end
  • Instance Axios centrale avec intercepteur JWT automatique
  • authSlice Redux — connexion, déconnexion, persistance localStorage
  • Page Login — POST /auth/login — JWT stocké et dispatché
  • Page Register — POST /auth/register — formulaire validé avec Zod
  • ProtectedRoute — redirection /login pour les routes privées
  • ProtectedRoute avec rôle — livreur et admin
  • Page Restaurants — GET /cuisines avec filtrage par ville et pagination
  • Page Menu — GET /cuisines/:id + GET /cuisines/:id/plats — ajout au panier
  • panierSlice Redux — contrainte un restaurant à la fois
  • Page Commande — POST /commandes avec formulaire d'adresse — vider panier après succès
  • Page Suivi — GET /commandes/:id avec polling automatique — timeline des étapes
  • Page Livraisons (livreur) — accepter et mettre à jour le statut des missions
  • Page Notifications (admin) — historique des événements RabbitMQ
  • BadgeStatut — composant réutilisable pour tous les statuts
  • Header — état Redux connecté/déconnecté et compteur panier