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.
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é.
| Version | Année | Apports principaux utilisés en React |
|---|---|---|
| ES6 / ES2015 | 2015 | let, const, arrow functions, classes, modules, destructuring, spread, template literals, Promise |
| ES2017 | 2017 | async/await |
| ES2018 | 2018 | Spread/rest sur les objets, Promise.finally() |
| ES2020 | 2020 | Optional chaining ?., nullish coalescing ??, Promise.allSettled() |
| ES2021+ | 2021+ | Promise.any(), logical assignment operators |
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.
// ── 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;
- Section 00 —
let/const: portée de bloc, différences avecvar, immutabilité de référence - Section 01 — Arrow functions : syntaxe, retour implicite,
thislexical - 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
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.
- Savoir ce qu'est une variable en JavaScript
- Comprendre le concept de fonction
- Pourquoi
varest dangereux et abandonné en React - La portée de bloc (block scope) de
letet comment elle évite les bugs - L'immutabilité de référence de
constet 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 :
constpar défaut,letuniquement quand nécessaire
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.
// ── 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.
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.
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.
// ── 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
}
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.
// ── 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ère | var | let | const |
|---|---|---|---|
| Portée | Fonctionnelle | Bloc | Bloc |
| Hoisting | Oui (undefined) | Oui (TDZ — erreur) | Oui (TDZ — erreur) |
| Réassignation | Oui | Oui | Non |
| Redéclaration | Oui | Non | Non |
| Utilisé en React | Jamais | Parfois (compteurs, boucles) | Toujours (90% des cas) |
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.
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 ?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 ?const livreur = { nom: "Khalid", ville: "Casablanca" }. Laquelle de ces opérations lève une erreur ?let ?const [plats, setPlats] = useState([]). On veut ajouter un plat. Pourquoi plats.push(nouveauPlat) ne fonctionne-t-il pas en React ?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.
- 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
thislexical — la différence fondamentale avec les fonctions classiques - Pourquoi les arrow functions résolvent le problème de
thisen React
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.
// ── 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} />);
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.
// ── 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ère | Fonction classique | Arrow function |
|---|---|---|
| this | Dynamique — dépend de l'appelant | Lexical — capturé à la définition |
| Constructeur | Oui — new MaFonction() | Non — TypeError |
| arguments | Oui — objet arguments implicite | Non — utiliser ...args |
| bind() nécessaire | Souvent (callbacks, handlers) | Jamais |
| Usage React | Rare — composants classe legacy | Omniprésent |
const creerCommande = (client, ville) => ({ client, ville, statut: "en_attente" }). Pourquoi l'objet est-il entre parenthèses ?setInterval avec une fonction classique ne peut pas accéder à this.livreur. Pourquoi une arrow function résout ce problème ?function formaterVille(v) { return v.toUpperCase(); } ?bind(this) ?const Livreur = (nom) => { this.nom = nom; }; new Livreur("Khalid") ?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.
- 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
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.
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>
);
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.
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);
const { total: montant = 0, urgent = false } = { total: 145, ville: "Rabat" }. Quelles sont les valeurs de montant et urgent ?const CartePlat = ({ nom, prix, disponible = true }) => .... Que se passe-t-il si disponible n'est pas passée ?const notes = [18, 14, 12, 16, 9]. Comment extraire la première et la cinquième note en une ligne par destructuring ?const [plats, setPlats] = useState([]) avec des crochets et non des accolades ?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.
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.
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([]);
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.
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
}
});
// ── 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"
/>
setPanier([...panier, nouveauPlat]). Pourquoi cette syntaxe déclenche un re-rendu React, contrairement à panier.push(nouveauPlat) ?{ ...{ ville: "Fès", note: 4.5 }, ville: "Meknès", actif: true } ?const somme = (...nombres) => nombres.reduce((a, b) => a + b, 0). Quel est le type de nombres ?BoutonDariGo, la syntaxe {...autresProps} dans le JSX sert à quoi ?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.
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é.
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" : ""}`;
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.
// ── 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
}
}
`;
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.
`Commande #${3 * 7} — ${["Casablanca", "Rabat"][1]}` ?const Titre = styled.h1`color: ${props => props.couleur};`. Que reconnaissez-vous ?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.
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.
// ── 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';
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...
CartePlat.jsx contient export default CartePlat. On l'importe avec import { CartePlat } from './CartePlat'. Que se passe-t-il ?import React, { useState, useEffect } from 'react'. Que sont React, useState et useEffect ?import { VILLES_LIVRAISON as villes } from './utils/prix'. Pourquoi utiliser un alias ?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.
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.
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
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.
// ── 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"]
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.
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>
);
};
key={plat.id}. Que se passe-t-il si on utilise key={index} alors que la liste peut être filtrée ?plats.filter(p => p.disponible).filter(p => p.prix < 80).map(p => p.nom). Combien de nouveaux tableaux sont créés ?[{ prix: 75, qte: 2 }, { prix: 65, qte: 1 }].reduce((acc, item) => acc + item.prix * item.qte, 0) ?plats. Quelle méthode ?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.
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.
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).
// ── 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);
}
});
});
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.
// ── 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
}, []);
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.
fetch() rejette-t-il automatiquement la Promise ?useEffect(async () => { const data = await fetchPlats(); setPlats(data); }, []) ?async sans return explicite retourne quoi ?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.
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é.
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
// ── 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ère | Composant de classe (legacy) | Composant fonctionnel + Hooks |
|---|---|---|
| Syntaxe | Verbeux — extends, constructor, super, render() | Concis — simple fonction arrow |
| État | this.state + this.setState() | useState() par variable |
| Cycle de vie | componentDidMount, componentWillUnmount... | useEffect() unifié |
| Problème de this | Fréquent — bind() nécessaire | Inexistant — pas de this |
| Logique réutilisable | HOC ou Render Props — complexe | Custom hooks — élégant |
| Recommandé en 2025 | Non — maintenance uniquement | Oui — standard de l'industrie |
super(props) et pas juste super() ?componentDidMount en composant fonctionnel ?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.
| Méthode HTTP | Opération CRUD | Exemple DariGo | Body ? |
|---|---|---|---|
| GET | Read | GET /plats — lister les plats | Non |
| POST | Create | POST /commandes — nouvelle commande | Oui |
| PUT | Update complet | PUT /commandes/42 — remplacer tout | Oui |
| PATCH | Update partiel | PATCH /commandes/42/statut — modifier le statut | Oui |
| DELETE | Delete | DELETE /commandes/42 — annuler | Non |
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();
};
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.
// ── 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]);
// ...
};
Content-Type: application/json est omis. Que se passe-t-il côté serveur Express.js ?encodeURIComponent(terme) pour construire une URL de recherche ?controller.abort() est appelé après timeout, que se passe-t-il pour la Promise fetch ?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.
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.
// ── 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.
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.
| Outil | Rôle | Exemple dans DariGo |
|---|---|---|
| npm / yarn | Gestionnaire de paquets — installer et gérer les dépendances du projet | npm install react react-dom react-router-dom axios |
| Vite | Bundler moderne — assemble tous les fichiers JS/CSS en bundles optimisés pour la production | npm create vite@latest darigo-frontend -- --template react |
| Babel | Transpileur — convertit ES6+ et JSX en ES5 lisible par tous les navigateurs (intégré à Vite) | Transforme const CartePlat = () => ... en code ES5 |
| ESLint | Linter — analyse le code statiquement et signale les erreurs et mauvaises pratiques | Signale var, clés manquantes dans les listes, hooks mal utilisés |
| Prettier | Formateur de code — applique un style cohérent automatiquement | Indentation, guillemets, virgules finales — uniformes dans toute l'équipe |
| TypeScript | Sur-ensemble typé de JavaScript — détecte les erreurs de type à la compilation | Typer les props des composants, les réponses API, le store Redux |
| React DevTools | Extension navigateur — inspecter les composants, l'état, les props, les re-rendus | Voir pourquoi un composant se re-rend trop souvent |
# ── 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
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 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.
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 ?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.
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.
── 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
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.
── 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é)
// ── 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>;
};
| Critère | SPA (React) | MPA (Laravel, Django...) |
|---|---|---|
| Expérience utilisateur | Navigation instantanée, état persistant, expérience "app native" | Rechargement complet à chaque page, flash blanc |
| Premier chargement | Lent — bundle JS complet à télécharger (~200KB-1MB) | Rapide — HTML généré serveur, pas de JS requis pour voir le contenu |
| SEO | Problé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éseau | Après le chargement initial, seules les données API transitent — économique | Chaque page = nouveau document HTML complet |
| Complexité | Plus complexe — gestion du routing, de l'état, du cycle de vie côté client | Plus simple pour les CRUD basiques |
| Cas d'usage idéal | Applications interactives : dashboards, réseaux sociaux, apps de livraison, SaaS | Sites 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.
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.
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.
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.
// ── 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.
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.
| Besoin | Bibliothèque standard | Alternative | Usage DariGo |
|---|---|---|---|
| Routage | React Router v6 | TanStack Router | Navigation entre pages Menu, Commande, Suivi, Profil |
| État global | Redux Toolkit | Zustand, Jotai, Context API | Panier partagé, utilisateur connecté, notifications |
| Requêtes serveur | TanStack Query (React Query) | SWR, RTK Query | Chargement plats, commandes avec cache et synchronisation |
| Formulaires | React Hook Form | Formik | Formulaire de commande, inscription, adresse de livraison |
| HTTP client | Axios | Fetch natif | Appels API DariGo avec intercepteurs et gestion d'erreurs |
| CSS-in-JS | styled-components | Emotion, CSS Modules | Composants stylés réutilisables |
| UI Components | shadcn/ui | MUI, Ant Design, Chakra | Boutons, modals, toasts, formulaires |
| Tests | Jest + Testing Library | Vitest | Tests unitaires des composants et des services |
| Build | Vite | Create React App (déprécié) | Serveur dev + build production |
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.
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.
// ── 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
plats de DariGo. Combien d'éléments DOM React doit-il créer/modifier si on utilise key={plat.id} ?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.
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.
# ── 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
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.
# ── 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
# ── 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
// ── 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;
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ère | Vite | Create React App (CRA) |
|---|---|---|
| Bundler | Rollup (prod) + ESBuild (dev) | Webpack |
| Démarrage serveur dev | < 1 seconde | 15-60 secondes selon projet |
| HMR (rechargement à chaud) | Quasi-instantané | 1-10 secondes |
| Port par défaut | 5173 | 3000 |
| Config exposée | vite.config.js simple | Cachée (ou eject pour l'exposer) |
| Statut | Actif — recommandé 2025 | Déprécié depuis 2023 |
| Extension fichiers | .jsx ou .js | .js ou .jsx |
# ── 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
# ── 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
// ── 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/, '')
}
}
}
});
index.html par rapport à CRA ?ReactDOM.createRoot() à la place de l'ancien ReactDOM.render() ?React.StrictMode et quel est son impact en production ?vite.config.js, on configure l'alias '@components': path.resolve(__dirname, './src/components'). Quel en est l'avantage concret ?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.
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.
// ── 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.
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.
// ── 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>
));
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.
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>
);
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.
// ── 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>
);
};
className au lieu de class en JSX ?{commandes.length && <ListeCommandes data={commandes} />}. Que s'affiche-t-il quand commandes est un tableau vide ?Modal doit afficher du contenu variable selon l'endroit où il est utilisé. Comment passer ce contenu dynamique ?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.
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.
// ── 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%
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.
// ── 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>
);
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.
// ── 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>
);
};
const listePlats = ({ plats }) => ... et on l'utilise avec <listePlats plats={data} />. Que se passe-t-il ?this.setState({ ajoute: true }) devient quoi ?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 ?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.
// ── 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>
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.
// ── 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>
);
};
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.
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.
CartePlat, l'utilisateur clique sur "Ajouter". Le composant doit mettre à jour le panier. Comment procède-t-il dans le respect du flux unidirectionnel ?FiltresCategorie et le composant GrillePlats doivent partager la catégorie active. Où doit vivre cet état selon le principe de Lifting State Up ?<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 ?onAjouter = () => {} comme valeur par défaut. Quel avantage cela apporte-t-il ?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.
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.
// ── 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);
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.
// ══ 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'
}
}));
// ── 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>
);
};
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.
setNbArticles(nbArticles + 1) puis setNbArticles(nbArticles + 1) dans le même handler. Pourquoi le compteur n'augmente-t-il que de 1 ?panier.push(nouveauPlat); setPanier(panier). Que se passe-t-il ?const [total, setTotal] = useState(0) pour afficher le total du panier ?useState(() => JSON.parse(localStorage.getItem('panier') || '[]')) est une fonction. Pourquoi ?const [commande, setCommande] = useState({ id: 1042, statut: 'en_attente', total: 145 }) ?É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.
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...
// ── É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..."
/>
);
};
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.
// ── 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
};
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.
return () => clearInterval(interval)), que se passe-t-il quand l'utilisateur navigue vers une autre page ?e.preventDefault() dans le handler onSubmit d'un formulaire React ?useEffect(() => { document.title = `Commande #${id}`; }, [id]). La fonction de cleanup retourne () => { document.title = 'DariGo'; }. Quand cette cleanup s'exécute-t-elle ?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).
// ── 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>
);
};
// ── 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>
);
};
// ── 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
{commandes.length && <ListeCommandes />}. La liste est vide. Qu'est-ce qui s'affiche dans la page ?src/assets/ avec import img from './assets/tajine.jpg' plutôt que la mettre dans public/ et utiliser '/images/tajine.jpg' ?<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 ?CONFIG_STATUT[statut] pour le badge, quel est l'avantage par rapport à un switch ou des if/else if ?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.
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.
// ── 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>
);
};
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.
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>
);
};
<input value={form.nom} /> sans onChange. Que se passe-t-il quand l'utilisateur tape ?mode: 'onChange' et mode: 'onBlur' dans React Hook Form ?handleChange générique, on vérifie e.target.type === 'checkbox' ? e.target.checked : e.target.value. Pourquoi ?disabled={isSubmitting || !isValid} est appliqué au bouton submit. Qu'est-ce que isValid représente exactement ?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.
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.
// ── 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'),
};
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.
// ── 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'] });
}
});
response.data dans le cas de succès. Quelle conséquence cela a-t-il dans les services ?queryKey: ['plats', ville, categorie]. Que se passe-t-il si ville change de "Casablanca" à "Rabat" ?isLoading et isFetching dans useQuery ?queryClient.invalidateQueries({ queryKey: ['mes-commandes'] }). Que fait cette instruction ?cancelQueries avant la mise à jour optimiste ?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.
À 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.
// ── 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.
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.
// ── 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>
);
};
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.
// ── 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;
};
const fn = useCallback(() => setPanier(p => [...p, plat]), [plat]). Si plat ne change pas entre deux rendus, que se passe-t-il pour fn ?useState ou useRef ?useMemo à un calcul dans un composant React ?useAuth, on lève une erreur si le hook est utilisé hors du Provider. Pourquoi cette vérification est-elle importante ?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é.
// ── 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>
);
};
use ?useDebounce, la recherche "Tajine" a été saisie. L'utilisateur s'arrête de taper. Que se passe-t-il après 300ms ?usePanier(). Partagent-ils le même état de panier ?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.
// ── 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 />;
};
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>
);
};
suivi/:id, l'URL est /suivi/1042. useParams() retourne { id: "1042" }. Comment récupérer ce paramètre comme un nombre ?<Outlet /> au lieu de {children} ?navigate('/menu') et navigate('/menu', { replace: true }) ?<NavLink> pour la navigation principale plutôt que <Link> ?/menu?ville=Casablanca&categorie=plat soit bookmarkable — un utilisateur peut la partager et retrouver le même état de filtres. Quel hook utiliser ?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.
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ère | useState local | useContext | Redux |
|---|---|---|---|
| Portée | Un composant | Arbre de composants | Toute l'application |
| Complexité | Minimale | Faible | Modérée (Redux Toolkit) |
| Outils de debug | React DevTools | React DevTools | Redux DevTools (time travel) |
| Performance | Optimale | Re-rendus si mal utilisé | Sélecteurs optimisés |
| Idéal pour | État UI local (modal, formulaire) | Thème, langue, auth simple | Panier, auth, notifications, données complexes |
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.
// ── 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
default ?{ type: 'panier/ajouterArticle', payload: {...} }. Quel reducer est appelé en premier ?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.
// ── 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>
);
};
useSelector(state => state.panier.total). Une action ajoute un article, faisant passer total de 75 à 150 MAD. Combien de composants se re-rendent ?state => state.panier.articles directement dans chaque composant ?const dispatch = useDispatch(). Est-il nécessaire d'appeler useDispatch dans chaque composant qui dispatch, ou peut-on partager la même instance ?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".
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.
// ── 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;
// ── 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 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 :
createSlicegé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.
state.articles.push(nouveauPlat). Comment est-ce possible si Redux impose l'immutabilité ?createAsyncThunk('panier/passerCommande', ...) génère combien et quelles actions ?builder.addCase) plutôt que switch/case ?passerCommande, comment savoir si l'action a réussi ou échoué pour naviguer vers la page de suivi uniquement en cas de succès ?configureStore intègre-t-il automatiquement sans configuration ?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.
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.
┌─────────────────────────────────────────────────────────┐
│ 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
# ── 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
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
// ── 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
// ── 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>
);
É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.
- 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
// ── 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;
}
}
};
// ── 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;
// ── 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
// ── 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>
</>
);
É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.
// ── 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;
// ── 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>
);
};
É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é.
// ── 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>
);
};
É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.
// ── 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>
);
};
É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.
// ── 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();
}
}, []);
| Page / Composant | Service consommé | Endpoints utilisés | Auth requise |
|---|---|---|---|
| PageLogin | auth-service | POST /auth/login | Non |
| PageRegister | auth-service | POST /auth/register | Non |
| Header | Redux store | — | — |
| ProtectedRoute | Redux store | — | Oui |
| PageRestaurants | cuisine-service | GET /cuisines | Non |
| PageMenu | cuisine-service | GET /cuisines/:id, GET /cuisines/:id/plats | Non |
| PageCommande | commande-service | POST /commandes | Oui |
| PageSuivi | commande-service | GET /commandes/:id (polling) | Oui |
| PageMesCommandes | commande-service | GET /commandes | Oui |
| PageLivraisons | livraison-service | GET /livraisons, PATCH /accepter, PATCH /statut | Livreur |
| PageNotifications | notif-service | GET /notifs | Admin |
# ── 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
- 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