Table des matieres
- Introduction a React
- JSX
- Composants
- State avec useState
- Evenements en React
- useEffect
- Listes et rendu conditionnel
- Communication entre composants
- React Router
- Appels API avec React
- Hooks supplementaires
- Bonnes pratiques
- Exercices d'examen corriges
Introduction a React
Qu'est-ce que React ?
React est une bibliotheque JavaScript (pas un framework) creee par Facebook (Meta) en 2013. Elle sert a construire des interfaces utilisateur (UI) de maniere declarative et composable.
React est aujourd'hui la bibliotheque front-end la plus utilisee au monde. Elle est deployee sur Facebook, Instagram, Netflix, Airbnb, et des milliers d'autres applications.
Attention a la terminologie : React n'est pas un framework. Un framework impose une structure complete (comme Angular). React est une bibliotheque qui se concentre sur la couche vue (le rendu de l'interface). On peut l'integrer dans un projet existant ou l'utiliser comme base d'une application complete.
Le probleme : la manipulation manuelle du DOM
En JavaScript Vanilla, pour mettre a jour l'interface, on manipule directement le DOM :
// JavaScript Vanilla — manipulation manuelle du DOM
const liste = document.getElementById("liste");
function ajouterProduit(nom) {
const li = document.createElement("li");
li.textContent = nom;
liste.appendChild(li);
}
function supprimerProduit(index) {
const items = liste.querySelectorAll("li");
if (items[index]) {
liste.removeChild(items[index]);
}
}
function mettreAJourProduit(index, nouveauNom) {
const items = liste.querySelectorAll("li");
if (items[index]) {
items[index].textContent = nouveauNom;
}
}
Les problemes de cette approche :
- Fastidieux : il faut ecrire beaucoup de code pour chaque modification de l'interface.
- Source d'erreurs : on doit gerer manuellement la synchronisation entre les donnees et l'affichage. Si on oublie de mettre a jour un element, l'interface devient incoherente.
- Performances : chaque modification du DOM reel declenche un recalcul du layout par le navigateur (reflow/repaint), ce qui est couteux.
- Difficulte de maintenance : quand l'application grandit, la logique de mise a jour du DOM devient un enchevrement illisible.
La solution de React : le Virtual DOM
React introduit le concept de Virtual DOM (DOM virtuel). Le principe :
- React maintient en memoire une copie legere du DOM (un arbre d'objets JavaScript).
- Quand les donnees changent, React cree un nouveau Virtual DOM qui reflete le nouvel etat.
- React compare l'ancien et le nouveau Virtual DOM (algorithme de "diffing").
- React ne met a jour dans le DOM reel que les elements qui ont change.
Ce mecanisme s'appelle la reconciliation. Le developpeur n'a plus besoin de dire "modifie tel element". Il declare simplement "voici a quoi l'interface doit ressembler pour ces donnees", et React s'occupe du reste.
Donnees changent
|
v
Nouveau Virtual DOM genere
|
v
Comparaison avec l'ancien Virtual DOM (diffing)
|
v
Calcul des modifications minimales
|
v
Application au DOM reel (patching)
Creer un projet React
Il existe deux outils principaux pour demarrer un projet React :
Vite (recommande en 2024+) :
npm create vite@latest mon-projet -- --template react
cd mon-projet
npm install
npm run dev
Create React App (historique, de moins en moins utilise) :
npx create-react-app mon-projet
cd mon-projet
npm start
Vite est plus rapide au demarrage et au rechargement. C'est l'outil recommande pour les nouveaux projets.
Structure d'un projet React (avec Vite)
mon-projet/
node_modules/ <- dependances installees (ne pas modifier)
public/ <- fichiers statiques (images, favicon)
src/ <- code source de l'application
App.jsx <- composant principal
App.css <- styles du composant principal
main.jsx <- point d'entree (monte l'application dans le DOM)
index.css <- styles globaux
index.html <- page HTML unique
package.json <- dependances et scripts
vite.config.js <- configuration de Vite
Le fichier main.jsx est le point d'entree. Il monte le composant App dans l'element HTML #root :
// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
React.StrictMode est un composant qui active des verifications supplementaires en mode developpement (double rendu pour detecter les effets de bord involontaires, avertissements). Il ne produit aucun rendu visible et n'affecte pas la production.
Le fichier index.html contient un unique <div id="root"></div>. Toute l'application React sera injectee dans ce div. C'est une Single Page Application (SPA) : une seule page HTML, le contenu change dynamiquement via JavaScript.
JSX
Qu'est-ce que le JSX ?
JSX (JavaScript XML) est une extension de syntaxe qui permet d'ecrire du HTML directement dans du JavaScript. Ce n'est pas du HTML : c'est du JavaScript qui ressemble a du HTML.
// Ceci est du JSX, pas du HTML
const element = <h1>Bonjour le monde</h1>;
Le navigateur ne comprend pas le JSX. Un outil de compilation (Babel, via Vite) transforme le JSX en appels JavaScript :
// Le JSX ci-dessus est compile en :
const element = React.createElement("h1", null, "Bonjour le monde");
React.createElement cree un objet JavaScript qui represente un element du Virtual DOM. On n'ecrit jamais React.createElement directement — le JSX est beaucoup plus lisible.
Differences entre JSX et HTML
Le JSX n'est pas du HTML. Plusieurs attributs sont differents car le JSX est en realite du JavaScript, et certains mots sont reserves en JavaScript.
| HTML | JSX | Raison |
|---|---|---|
class | className | class est un mot reserve en JavaScript |
for | htmlFor | for est un mot reserve en JavaScript |
tabindex | tabIndex | camelCase pour tous les attributs |
onclick | onClick | camelCase pour les evenements |
style="color: red" | style={{ color: "red" }} | objet JavaScript, pas chaine |
<!-- commentaire --> | {/* commentaire */} | syntaxe de commentaire JSX |
Exemples :
// CORRECT en JSX
<label htmlFor="nom" className="label-form">Nom :</label>
<input id="nom" tabIndex={1} autoFocus />
<div style={{ backgroundColor: "blue", fontSize: "14px" }}>Contenu</div>
// INCORRECT en JSX (erreurs)
<label for="nom" class="label-form">Nom :</label> // ERREUR
Regles importantes :
- Tous les attributs sont en camelCase :
onClick,onChange,onSubmit,tabIndex,autoFocus. - L'attribut
styleprend un objet JavaScript (double accolades : la premiere pour l'expression JSX, la seconde pour l'objet). - Les proprietes CSS avec tiret deviennent camelCase :
background-colordevientbackgroundColor,font-sizedevientfontSize. - Toutes les balises doivent etre fermees :
<img />,<br />,<input />.
Expressions JavaScript dans le JSX
On peut inserer n'importe quelle expression JavaScript dans le JSX en utilisant des accolades {} :
const nom = "Alice";
const age = 25;
// Variable
const element1 = <p>Bonjour {nom}</p>;
// Expression calculee
const element2 = <p>Age dans 10 ans : {age + 10}</p>;
// Appel de fonction
const element3 = <p>Nom en majuscules : {nom.toUpperCase()}</p>;
// Ternaire (condition)
const element4 = <p>{age >= 18 ? "Majeur" : "Mineur"}</p>;
// ET logique (affichage conditionnel)
const element5 = <div>{age >= 18 && <p>Vous pouvez voter</p>}</div>;
Attention : on ne peut mettre que des expressions (qui retournent une valeur), pas des instructions. Les if, for, while ne sont pas autorises directement dans le JSX.
// INTERDIT dans le JSX :
<div>{if (condition) { return "oui" }}</div> // ERREUR
// CORRECT : utiliser un ternaire ou un && :
<div>{condition ? "oui" : "non"}</div>
<div>{condition && "oui"}</div>
Les Fragments
Un composant React doit retourner un seul element racine. Pour retourner plusieurs elements sans ajouter un <div> inutile, on utilise un Fragment :
// ERREUR : deux elements racine
function MonComposant() {
return (
<h1>Titre</h1>
<p>Paragraphe</p>
);
}
// CORRECT avec Fragment (syntaxe courte)
function MonComposant() {
return (
<>
<h1>Titre</h1>
<p>Paragraphe</p>
</>
);
}
// CORRECT avec Fragment (syntaxe longue — necessaire si on veut mettre une key)
import { Fragment } from "react";
function MonComposant() {
return (
<Fragment>
<h1>Titre</h1>
<p>Paragraphe</p>
</Fragment>
);
}
Les Fragments ne produisent aucun element HTML dans le DOM. Ils servent uniquement a regrouper des elements JSX.
Exercice JSX
Creer un composant CarteProduit qui affiche :
- Le nom du produit en gras
- Le prix (si superieur a 100, afficher "Prix eleve" en rouge)
- Un badge "Promo" uniquement si le produit est en promotion
function CarteProduit() {
const nom = "Clavier mecanique";
const prix = 129.99;
const enPromo = true;
return (
<div className="carte-produit">
<h2>{nom}</h2>
<p style={{ color: prix > 100 ? "red" : "black" }}>
{prix > 100 ? "Prix eleve : " : "Prix : "}{prix} EUR
</p>
{enPromo && <span className="badge-promo">Promo</span>}
</div>
);
}
Composants
Composants fonctionnels
En React, un composant est une fonction JavaScript qui retourne du JSX. C'est l'unite de base pour construire une interface.
// Un composant fonctionnel
function Bonjour() {
return <h1>Bonjour le monde</h1>;
}
Regles :
- Le nom d'un composant commence toujours par une majuscule (PascalCase) :
MonComposant,CarteProduit,ListeUtilisateurs. - Un composant retourne du JSX (un seul element racine, ou un Fragment).
- On utilise un composant comme une balise HTML :
<Bonjour />.
// Utilisation du composant
function App() {
return (
<div>
<Bonjour />
<Bonjour />
<Bonjour />
</div>
);
}
En BTS SIO, on utilise exclusivement des composants fonctionnels. Les "class components" sont une ancienne syntaxe que l'on ne doit plus utiliser.
Props : passer des donnees du parent a l'enfant
Les props (proprietes) permettent de passer des donnees d'un composant parent a un composant enfant. C'est le mecanisme fondamental de communication en React.
// Le parent passe des props
function App() {
return (
<div>
<Salutation nom="Alice" age={25} />
<Salutation nom="Bob" age={30} />
</div>
);
}
// L'enfant recoit les props en parametre
function Salutation(props) {
return (
<p>
Bonjour {props.nom}, vous avez {props.age} ans.
</p>
);
}
Regles sur les props :
- Les props sont en lecture seule. Un composant enfant ne doit jamais modifier ses props.
- Les props sont des expressions JavaScript quand elles sont entre
{}: nombres, booleens, objets, tableaux, fonctions. - Les chaines de caracteres peuvent etre passees sans
{}:nom="Alice".
Destructuration des props
La destructuration rend le code plus lisible. C'est la pratique standard :
// Sans destructuration
function Salutation(props) {
return <p>Bonjour {props.nom}, vous avez {props.age} ans.</p>;
}
// Avec destructuration (recommande)
function Salutation({ nom, age }) {
return <p>Bonjour {nom}, vous avez {age} ans.</p>;
}
Props par defaut
On peut definir des valeurs par defaut pour les props :
// Methode 1 : valeur par defaut dans la destructuration
function Bouton({ texte = "Cliquer", couleur = "blue" }) {
return <button style={{ backgroundColor: couleur }}>{texte}</button>;
}
// Utilisation
<Bouton /> // texte="Cliquer", couleur="blue"
<Bouton texte="Valider" /> // texte="Valider", couleur="blue"
<Bouton texte="Annuler" couleur="red" /> // texte="Annuler", couleur="red"
PropTypes : validation des props
PropTypes permet de valider le type des props recues. C'est une bonne pratique pour detecter les erreurs tot.
npm install prop-types
import PropTypes from "prop-types";
function CarteProduit({ nom, prix, enStock }) {
return (
<div>
<h2>{nom}</h2>
<p>Prix : {prix} EUR</p>
{enStock ? <p>En stock</p> : <p>Rupture de stock</p>}
</div>
);
}
CarteProduit.propTypes = {
nom: PropTypes.string.isRequired, // chaine, obligatoire
prix: PropTypes.number.isRequired, // nombre, obligatoire
enStock: PropTypes.bool, // booleen, optionnel
};
CarteProduit.defaultProps = {
enStock: true,
};
Les types disponibles dans PropTypes :
PropTypes.string— chaine de caracteresPropTypes.number— nombrePropTypes.bool— booleenPropTypes.array— tableauPropTypes.object— objetPropTypes.func— fonctionPropTypes.node— tout ce qui peut etre rendu (texte, element, fragment)PropTypes.element— un element ReactPropTypes.arrayOf(PropTypes.number)— tableau de nombresPropTypes.shape({ nom: PropTypes.string })— objet avec une forme precise.isRequired— rend la prop obligatoire
La prop children
La prop speciale children represente le contenu place entre les balises ouvrante et fermante d'un composant :
function Carte({ children, titre }) {
return (
<div className="carte">
<h2>{titre}</h2>
<div className="carte-contenu">
{children}
</div>
</div>
);
}
// Utilisation
function App() {
return (
<Carte titre="Mon profil">
<p>Nom : Alice</p>
<p>Age : 25 ans</p>
<img src="photo.jpg" alt="Photo de profil" />
</Carte>
);
}
Tout ce qui est entre <Carte> et </Carte> est passe comme prop children. Cela permet de creer des composants enveloppes (wrappers) reutilisables.
Composition : imbriquer les composants
Les composants s'imbriquent les uns dans les autres comme des briques :
function Avatar({ url, nom }) {
return <img src={url} alt={nom} className="avatar" />;
}
function InfoUtilisateur({ nom, email }) {
return (
<div>
<h3>{nom}</h3>
<p>{email}</p>
</div>
);
}
function CarteUtilisateur({ utilisateur }) {
return (
<div className="carte-utilisateur">
<Avatar url={utilisateur.avatar} nom={utilisateur.nom} />
<InfoUtilisateur nom={utilisateur.nom} email={utilisateur.email} />
</div>
);
}
function ListeUtilisateurs({ utilisateurs }) {
return (
<div>
{utilisateurs.map((u) => (
<CarteUtilisateur key={u.id} utilisateur={u} />
))}
</div>
);
}
Exercice : carte produit reutilisable
Creer un composant CarteProduit qui recoit en props : nom, prix, image, enStock. Il affiche l'image, le nom, le prix formate, et un indicateur de disponibilite. Utiliser PropTypes pour valider les props.
import PropTypes from "prop-types";
function CarteProduit({ nom, prix, image, enStock }) {
return (
<div className="carte-produit">
<img src={image} alt={nom} />
<h3>{nom}</h3>
<p className="prix">{prix.toFixed(2)} EUR</p>
<p className={enStock ? "en-stock" : "rupture"}>
{enStock ? "En stock" : "Rupture de stock"}
</p>
</div>
);
}
CarteProduit.propTypes = {
nom: PropTypes.string.isRequired,
prix: PropTypes.number.isRequired,
image: PropTypes.string.isRequired,
enStock: PropTypes.bool,
};
CarteProduit.defaultProps = {
enStock: true,
};
// Utilisation
function App() {
const produits = [
{ id: 1, nom: "Clavier", prix: 49.99, image: "/clavier.jpg", enStock: true },
{ id: 2, nom: "Souris", prix: 29.99, image: "/souris.jpg", enStock: false },
{ id: 3, nom: "Ecran", prix: 299.99, image: "/ecran.jpg", enStock: true },
];
return (
<div className="liste-produits">
{produits.map((p) => (
<CarteProduit
key={p.id}
nom={p.nom}
prix={p.prix}
image={p.image}
enStock={p.enStock}
/>
))}
</div>
);
}
State avec useState
Le state : des donnees qui changent
Le state (etat) est un mecanisme qui permet a un composant de stocker des donnees qui peuvent changer. Quand le state change, React re-rend le composant automatiquement pour refleter le nouvel etat.
La difference entre props et state :
- Props : donnees recues du parent, en lecture seule, le composant ne les modifie pas.
- State : donnees internes au composant, modifiables, declenchent un re-rendu quand elles changent.
useState : le Hook de base
useState est un Hook (une fonction speciale de React). Il permet de declarer une variable de state dans un composant fonctionnel.
import { useState } from "react";
function Compteur() {
const [compteur, setCompteur] = useState(0);
return (
<div>
<p>Compteur : {compteur}</p>
<button onClick={() => setCompteur(compteur + 1)}>+1</button>
<button onClick={() => setCompteur(compteur - 1)}>-1</button>
<button onClick={() => setCompteur(0)}>Reinitialiser</button>
</div>
);
}
Decomposition de useState :
useState(0): declare un state avec la valeur initiale0.[compteur, setCompteur]: destructuration du tableau retourne.compteur: la valeur actuelle du state.setCompteur: la fonction pour modifier le state.
- Quand on appelle
setCompteur(nouvelleValeur), React re-rend le composant avec la nouvelle valeur.
Convention de nommage : [valeur, setValeur] — le setter commence toujours par set suivi du nom de la variable avec une majuscule.
Regle fondamentale : le state est immutable
On ne modifie JAMAIS directement le state. On cree toujours une nouvelle valeur.
// ---- AVEC UN TABLEAU ----
const [fruits, setFruits] = useState(["pomme", "banane"]);
// MAUVAIS : modification directe (React ne detecte pas le changement)
fruits.push("orange"); // INTERDIT
setFruits(fruits); // React ne re-rend pas (meme reference)
// BON : creation d'un nouveau tableau
setFruits([...fruits, "orange"]); // spread + nouvel element
// ---- AVEC UN OBJET ----
const [utilisateur, setUtilisateur] = useState({ nom: "Alice", age: 25 });
// MAUVAIS : modification directe
utilisateur.nom = "Bob"; // INTERDIT
setUtilisateur(utilisateur); // React ne re-rend pas
// BON : creation d'un nouvel objet
setUtilisateur({ ...utilisateur, nom: "Bob" }); // spread + modification
Pourquoi ? React compare les references en memoire pour savoir si le state a change. Si on modifie un objet/tableau existant, la reference reste la meme, et React ne detecte pas le changement. Il faut creer un nouvel objet ou tableau pour que React detecte la difference.
State avec des objets
function FormulaireUtilisateur() {
const [utilisateur, setUtilisateur] = useState({
nom: "",
email: "",
age: 0,
});
// Modifier une seule propriete : spread + modification
const changerNom = (nouveauNom) => {
setUtilisateur({ ...utilisateur, nom: nouveauNom });
};
// Modifier plusieurs proprietes
const mettreAJour = (modifications) => {
setUtilisateur({ ...utilisateur, ...modifications });
};
return (
<div>
<input
value={utilisateur.nom}
onChange={(e) => changerNom(e.target.value)}
/>
<p>Nom : {utilisateur.nom}</p>
</div>
);
}
State avec des tableaux : operations courantes
function ListeTaches() {
const [taches, setTaches] = useState([
{ id: 1, texte: "Faire les courses", fait: false },
{ id: 2, texte: "Reviser React", fait: false },
]);
// AJOUTER un element
const ajouterTache = (texte) => {
const nouvelleTache = {
id: Date.now(), // identifiant unique simple
texte: texte,
fait: false,
};
setTaches([...taches, nouvelleTache]);
};
// SUPPRIMER un element (par id)
const supprimerTache = (id) => {
setTaches(taches.filter((t) => t.id !== id));
};
// MODIFIER un element (par id)
const basculerTache = (id) => {
setTaches(
taches.map((t) => {
if (t.id === id) {
return { ...t, fait: !t.fait };
}
return t;
})
);
};
return (
<div>
{taches.map((t) => (
<div key={t.id}>
<span
style={{ textDecoration: t.fait ? "line-through" : "none" }}
onClick={() => basculerTache(t.id)}
>
{t.texte}
</span>
<button onClick={() => supprimerTache(t.id)}>Supprimer</button>
</div>
))}
</div>
);
}
Resume des operations sur tableaux :
| Operation | Methode | Exemple |
|---|---|---|
| Ajouter | spread | [...tableau, nouvelElement] |
| Ajouter au debut | spread | [nouvelElement, ...tableau] |
| Supprimer | filter | tableau.filter(e => e.id !== id) |
| Modifier | map | tableau.map(e => e.id === id ? {...e, prop: val} : e) |
Mise a jour fonctionnelle
Quand la nouvelle valeur depend de l'ancienne, utiliser la forme fonctionnelle du setter :
// Probleme potentiel : si on appelle setCompteur 3 fois rapidement,
// les 3 appels utilisent la meme valeur de "compteur" (le state
// n'est pas encore mis a jour entre les appels)
setCompteur(compteur + 1);
setCompteur(compteur + 1);
setCompteur(compteur + 1);
// Resultat : compteur augmente de 1, pas de 3
// Solution : forme fonctionnelle
setCompteur((prev) => prev + 1);
setCompteur((prev) => prev + 1);
setCompteur((prev) => prev + 1);
// Resultat : compteur augmente de 3
La forme fonctionnelle setState(prev => ...) recoit toujours la valeur la plus recente du state. A utiliser systematiquement quand la nouvelle valeur depend de l'ancienne.
Exercices progressifs
Exercice 1 : Compteur simple — boutons +1, -1, reinitialiser.
Exercice 2 : Toggle (bascule) — un bouton qui alterne entre "Afficher" et "Masquer", et un paragraphe qui s'affiche/disparait.
function Toggle() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(!visible)}>
{visible ? "Masquer" : "Afficher"}
</button>
{visible && <p>Ce texte est visible !</p>}
</div>
);
}
Exercice 3 : Liste dynamique — un champ de saisie et un bouton pour ajouter des elements a une liste, avec un bouton de suppression pour chaque element.
function ListeDynamique() {
const [elements, setElements] = useState([]);
const [saisie, setSaisie] = useState("");
const ajouter = () => {
if (saisie.trim() === "") return;
setElements([...elements, { id: Date.now(), texte: saisie }]);
setSaisie("");
};
const supprimer = (id) => {
setElements(elements.filter((e) => e.id !== id));
};
return (
<div>
<input
value={saisie}
onChange={(e) => setSaisie(e.target.value)}
placeholder="Nouvel element"
/>
<button onClick={ajouter}>Ajouter</button>
<ul>
{elements.map((e) => (
<li key={e.id}>
{e.texte}
<button onClick={() => supprimer(e.id)}>X</button>
</li>
))}
</ul>
</div>
);
}
Evenements en React
Gestion des evenements
En React, les evenements sont geres de maniere similaire au DOM, mais avec des differences importantes :
| DOM natif | React |
|---|---|
onclick | onClick |
onchange | onChange |
onsubmit | onSubmit |
onclick="maFonction()" | onClick={maFonction} |
| chaine de caracteres | reference a une fonction |
event.preventDefault() | identique |
// DOM natif
<button onclick="handleClick()">Cliquer</button>
// React
<button onClick={handleClick}>Cliquer</button>
Attention : on passe une reference a la fonction (handleClick), pas un appel (handleClick()). Si on ecrit onClick={handleClick()}, la fonction est executee immediatement au rendu, pas au clic.
// CORRECT : reference a la fonction (executee au clic)
<button onClick={handleClick}>Cliquer</button>
// CORRECT : fonction anonyme (quand on veut passer des arguments)
<button onClick={() => handleClick(5)}>Cliquer</button>
// INCORRECT : appel immediat (execute au rendu, pas au clic)
<button onClick={handleClick()}>Cliquer</button>
Evenements courants
function ExemplesEvenements() {
// onClick : clic sur un element
const handleClick = () => {
console.log("Bouton clique");
};
// onChange : changement de valeur d'un input
const handleChange = (e) => {
console.log("Nouvelle valeur :", e.target.value);
};
// onSubmit : soumission d'un formulaire
const handleSubmit = (e) => {
e.preventDefault(); // empecher le rechargement de la page
console.log("Formulaire soumis");
};
// onKeyDown : touche pressee
const handleKeyDown = (e) => {
if (e.key === "Enter") {
console.log("Touche Entree pressee");
}
};
// onMouseEnter / onMouseLeave : survol
const handleMouseEnter = () => {
console.log("Souris entree");
};
return (
<div>
<button onClick={handleClick}>Cliquer</button>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<form onSubmit={handleSubmit}>
<button type="submit">Envoyer</button>
</form>
<div onMouseEnter={handleMouseEnter}>Survoler moi</div>
</div>
);
}
L'objet e (evenement) en React est un SyntheticEvent : un objet React qui enveloppe l'evenement natif du navigateur. Il a les memes proprietes (target, preventDefault(), stopPropagation(), etc.).
Formulaires controles (controlled components)
Un formulaire controle est un formulaire dont chaque champ est lie a un state. Le state est la "source de verite" : la valeur affichee dans l'input est toujours egale au state.
Le principe :
- Chaque input a un state associe.
- L'attribut
valuede l'input est lie au state. - L'evenement
onChangemet a jour le state. - React re-rend le composant, et l'input affiche la nouvelle valeur.
function FormulaireControle() {
const [nom, setNom] = useState("");
const [email, setEmail] = useState("");
return (
<form>
<input
type="text"
value={nom} // value = state
onChange={(e) => setNom(e.target.value)} // onChange met a jour le state
placeholder="Nom"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<p>Nom saisi : {nom}</p>
<p>Email saisi : {email}</p>
</form>
);
}
Formulaire complet avec un seul state objet
Quand un formulaire a plusieurs champs, on utilise souvent un seul objet state avec un handler generique :
function FormulaireInscription() {
const [formData, setFormData] = useState({
nom: "",
email: "",
motDePasse: "",
age: "",
});
const [erreurs, setErreurs] = useState({});
// Handler generique : utilise le name de l'input
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const valider = () => {
const nouvellesErreurs = {};
if (formData.nom.trim() === "") {
nouvellesErreurs.nom = "Le nom est obligatoire";
}
if (!formData.email.includes("@")) {
nouvellesErreurs.email = "Email invalide";
}
if (formData.motDePasse.length < 6) {
nouvellesErreurs.motDePasse = "Le mot de passe doit faire au moins 6 caracteres";
}
if (formData.age && (isNaN(formData.age) || formData.age < 0)) {
nouvellesErreurs.age = "Age invalide";
}
return nouvellesErreurs;
};
const handleSubmit = (e) => {
e.preventDefault();
const nouvellesErreurs = valider();
setErreurs(nouvellesErreurs);
if (Object.keys(nouvellesErreurs).length === 0) {
console.log("Formulaire valide :", formData);
// Envoyer les donnees au serveur
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="nom">Nom :</label>
<input
id="nom"
name="nom"
type="text"
value={formData.nom}
onChange={handleChange}
/>
{erreurs.nom && <span className="erreur">{erreurs.nom}</span>}
</div>
<div>
<label htmlFor="email">Email :</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
{erreurs.email && <span className="erreur">{erreurs.email}</span>}
</div>
<div>
<label htmlFor="motDePasse">Mot de passe :</label>
<input
id="motDePasse"
name="motDePasse"
type="password"
value={formData.motDePasse}
onChange={handleChange}
/>
{erreurs.motDePasse && <span className="erreur">{erreurs.motDePasse}</span>}
</div>
<div>
<label htmlFor="age">Age :</label>
<input
id="age"
name="age"
type="number"
value={formData.age}
onChange={handleChange}
/>
{erreurs.age && <span className="erreur">{erreurs.age}</span>}
</div>
<button type="submit">S'inscrire</button>
</form>
);
}
Point cle : l'utilisation de [name]: value (propriete calculee) permet d'avoir un seul handler pour tous les champs. L'attribut name de l'input doit correspondre a la cle dans l'objet formData.
Checkboxes et select
function FormulaireComplet() {
const [formData, setFormData] = useState({
nom: "",
newsletter: false,
pays: "france",
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
// Pour les checkboxes, utiliser checked au lieu de value
[name]: type === "checkbox" ? checked : value,
});
};
return (
<form>
<input
name="nom"
type="text"
value={formData.nom}
onChange={handleChange}
/>
<label>
<input
name="newsletter"
type="checkbox"
checked={formData.newsletter}
onChange={handleChange}
/>
S'abonner a la newsletter
</label>
<select name="pays" value={formData.pays} onChange={handleChange}>
<option value="france">France</option>
<option value="belgique">Belgique</option>
<option value="suisse">Suisse</option>
</select>
</form>
);
}
Exercice : formulaire de recherche avec filtrage en temps reel
function RechercheProduits() {
const [recherche, setRecherche] = useState("");
const produits = [
{ id: 1, nom: "Clavier mecanique", prix: 89.99 },
{ id: 2, nom: "Souris sans fil", prix: 45.99 },
{ id: 3, nom: "Ecran 27 pouces", prix: 349.99 },
{ id: 4, nom: "Casque audio", prix: 79.99 },
{ id: 5, nom: "Webcam HD", prix: 59.99 },
];
// Filtrage en temps reel : a chaque frappe, la liste se met a jour
const produitsFiltres = produits.filter((p) =>
p.nom.toLowerCase().includes(recherche.toLowerCase())
);
return (
<div>
<input
type="text"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
placeholder="Rechercher un produit..."
/>
<p>{produitsFiltres.length} resultat(s)</p>
<ul>
{produitsFiltres.map((p) => (
<li key={p.id}>
{p.nom} - {p.prix} EUR
</li>
))}
</ul>
</div>
);
}
Ce pattern est tres courant : on ne stocke pas les resultats filtres dans un state. On derive la liste filtree a chaque rendu a partir du state de recherche et des donnees. C'est le concept de donnee derivee (computed value).
useEffect
Les effets de bord
Un effet de bord (side effect) est toute operation qui interagit avec le monde exterieur au composant :
- Appeler une API (fetch)
- Modifier le titre de la page (
document.title) - Lire/ecrire dans le localStorage
- Creer un timer (setInterval, setTimeout)
- S'abonner a un evenement (addEventListener)
Ces operations ne peuvent pas etre faites directement dans le corps du composant (qui doit etre une fonction pure). On utilise le Hook useEffect.
Syntaxe de useEffect
import { useEffect } from "react";
useEffect(() => {
// Code de l'effet
// Execute apres le rendu du composant
}, [dependances]);
Le tableau de dependances controle quand l'effet est execute :
// 1. Tableau vide [] : execute UNE SEULE FOIS au montage
useEffect(() => {
console.log("Composant monte (equivalent de componentDidMount)");
}, []);
// 2. Avec dependances [a, b] : execute quand a OU b changent
useEffect(() => {
console.log("a ou b a change");
}, [a, b]);
// 3. Sans tableau : execute A CHAQUE RENDU (rarement souhaite)
useEffect(() => {
console.log("Rendu effectue");
});
Attention : oublier le tableau de dependances (cas 3) est une erreur frequente. L'effet s'execute a chaque rendu, ce qui peut creer des boucles infinies si l'effet modifie le state.
Exemples pratiques
Modifier le titre de la page :
function PageProduit({ nomProduit }) {
useEffect(() => {
document.title = `${nomProduit} - Ma Boutique`;
}, [nomProduit]);
return <h1>{nomProduit}</h1>;
}
Lire le localStorage au montage :
function Preferences() {
const [theme, setTheme] = useState(() => {
// Initialisation paresseuse du state :
// la fonction n'est executee qu'au premier rendu
const themeSauvegarde = localStorage.getItem("theme");
return themeSauvegarde || "clair";
});
// Sauvegarder dans le localStorage a chaque changement
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
return (
<div>
<p>Theme actuel : {theme}</p>
<button onClick={() => setTheme(theme === "clair" ? "sombre" : "clair")}>
Changer de theme
</button>
</div>
);
}
Fonction de nettoyage (cleanup)
useEffect peut retourner une fonction de nettoyage. Cette fonction est executee :
- Avant la re-execution de l'effet (si les dependances changent).
- Quand le composant est demonte (retire du DOM).
// Timer avec nettoyage
function Horloge() {
const [heure, setHeure] = useState(new Date());
useEffect(() => {
const intervalle = setInterval(() => {
setHeure(new Date());
}, 1000);
// Cleanup : supprimer l'intervalle quand le composant est demonte
return () => {
clearInterval(intervalle);
};
}, []); // [] = un seul intervalle cree au montage
return <p>Il est {heure.toLocaleTimeString()}</p>;
}
Sans le cleanup, l'intervalle continuerait de tourner meme apres la suppression du composant, causant une fuite memoire et des erreurs "setState on unmounted component".
Fetch de donnees : le pattern complet
C'est le pattern le plus courant et le plus important pour l'examen. Il combine useState et useEffect pour charger des donnees depuis une API.
function ListeUtilisateurs() {
const [utilisateurs, setUtilisateurs] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => {
if (!response.ok) {
throw new Error(`Erreur HTTP : ${response.status}`);
}
return response.json();
})
.then((data) => {
setUtilisateurs(data);
setChargement(false);
})
.catch((err) => {
setErreur(err.message);
setChargement(false);
});
}, []); // [] = fetch une seule fois au montage
if (chargement) return <p>Chargement en cours...</p>;
if (erreur) return <p>Erreur : {erreur}</p>;
return (
<ul>
{utilisateurs.map((u) => (
<li key={u.id}>{u.name} - {u.email}</li>
))}
</ul>
);
}
Version avec async/await :
function ListeUtilisateurs() {
const [utilisateurs, setUtilisateurs] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
// On ne peut pas mettre async directement sur le callback de useEffect
// Il faut creer une fonction async a l'interieur
const chargerDonnees = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`Erreur HTTP : ${response.status}`);
}
const data = await response.json();
setUtilisateurs(data);
} catch (err) {
setErreur(err.message);
} finally {
setChargement(false);
}
};
chargerDonnees();
}, []);
if (chargement) return <p>Chargement en cours...</p>;
if (erreur) return <p>Erreur : {erreur}</p>;
return (
<ul>
{utilisateurs.map((u) => (
<li key={u.id}>{u.name} - {u.email}</li>
))}
</ul>
);
}
Point important : on ne peut pas rendre le callback de useEffect directement async. La raison : useEffect attend que le callback retourne soit undefined, soit une fonction de cleanup. Une fonction async retourne une Promise, ce qui n'est pas valide. On cree donc une fonction async a l'interieur et on l'appelle immediatement.
Exercice : composant meteo
Creer un composant qui :
- Affiche un champ de saisie pour entrer une ville.
- Quand la ville change, fait un fetch pour recuperer les donnees (simulees).
- Gere les 3 etats : chargement, erreur, donnees.
function Meteo() {
const [ville, setVille] = useState("Paris");
const [meteo, setMeteo] = useState(null);
const [chargement, setChargement] = useState(false);
const [erreur, setErreur] = useState(null);
useEffect(() => {
if (ville.trim() === "") return;
setChargement(true);
setErreur(null);
fetch(`https://api.example.com/meteo?ville=${ville}`)
.then((res) => {
if (!res.ok) throw new Error("Ville non trouvee");
return res.json();
})
.then((data) => {
setMeteo(data);
setChargement(false);
})
.catch((err) => {
setErreur(err.message);
setMeteo(null);
setChargement(false);
});
}, [ville]); // Se declenche quand la ville change
return (
<div>
<input
value={ville}
onChange={(e) => setVille(e.target.value)}
placeholder="Entrer une ville"
/>
{chargement && <p>Chargement...</p>}
{erreur && <p>Erreur : {erreur}</p>}
{meteo && (
<div>
<p>Temperature : {meteo.temperature} C</p>
<p>Conditions : {meteo.conditions}</p>
</div>
)}
</div>
);
}
Listes et rendu conditionnel
Afficher une liste avec map()
Pour afficher une liste d'elements, on utilise la methode map() des tableaux :
function ListeNoms() {
const noms = ["Alice", "Bob", "Charlie", "Diana"];
return (
<ul>
{noms.map((nom, index) => (
<li key={index}>{nom}</li>
))}
</ul>
);
}
L'attribut key
Chaque element dans une liste doit avoir un attribut key unique. React utilise la key pour identifier chaque element lors de la reconciliation (comparaison du Virtual DOM).
// MAUVAIS : pas de key (avertissement React)
{produits.map((p) => <li>{p.nom}</li>)}
// ACCEPTABLE MAIS PAS IDEAL : index comme key
{produits.map((p, index) => <li key={index}>{p.nom}</li>)}
// CORRECT : identifiant unique comme key
{produits.map((p) => <li key={p.id}>{p.nom}</li>)}
Pourquoi eviter l'index comme key ? Si l'ordre des elements change (ajout, suppression, tri), l'index ne correspond plus au meme element. React pourrait reutiliser un composant avec le mauvais state. Utiliser un identifiant unique et stable (id de la base de donnees, par exemple).
Regles pour la key :
- Doit etre unique parmi les elements freres (pas globalement).
- Doit etre stable (ne change pas entre les rendus).
- Ne doit pas etre generee aleatoirement a chaque rendu (
Math.random()est interdit comme key). - N'est pas accessible comme prop dans le composant enfant (c'est une information interne a React).
Rendu conditionnel
Plusieurs techniques pour afficher ou masquer des elements :
function AffichageConditionnel({ utilisateur, estConnecte, messages }) {
// 1. Operateur && (ET logique)
// Si la condition est vraie, affiche l'element. Sinon, rien.
const notification = (
<div>
{messages.length > 0 && (
<p>Vous avez {messages.length} message(s)</p>
)}
</div>
);
// 2. Operateur ternaire
// Choisir entre deux elements
const statut = (
<p>{estConnecte ? "Bienvenue !" : "Veuillez vous connecter"}</p>
);
// 3. Early return
// Retourner tot si une condition n'est pas remplie
if (!utilisateur) {
return <p>Aucun utilisateur selectionne</p>;
}
return (
<div>
{statut}
{notification}
<h2>{utilisateur.nom}</h2>
</div>
);
}
Attention avec && et les nombres : si la valeur a gauche est 0 (falsy), React affiche 0 au lieu de ne rien afficher.
// PROBLEME : affiche "0" au lieu de rien
{messages.length && <p>{messages.length} messages</p>}
// SOLUTION : convertir en booleen
{messages.length > 0 && <p>{messages.length} messages</p>}
Exercice : liste de produits avec filtrage et tri
function CatalogueProduits() {
const [recherche, setRecherche] = useState("");
const [tri, setTri] = useState("nom"); // "nom" ou "prix"
const [ordre, setOrdre] = useState("asc"); // "asc" ou "desc"
const produits = [
{ id: 1, nom: "Clavier mecanique", prix: 89.99, categorie: "peripherique" },
{ id: 2, nom: "Souris sans fil", prix: 45.99, categorie: "peripherique" },
{ id: 3, nom: "Ecran 27 pouces", prix: 349.99, categorie: "affichage" },
{ id: 4, nom: "Casque audio", prix: 79.99, categorie: "audio" },
{ id: 5, nom: "Webcam HD", prix: 59.99, categorie: "peripherique" },
];
// 1. Filtrer
const produitsFiltres = produits.filter((p) =>
p.nom.toLowerCase().includes(recherche.toLowerCase())
);
// 2. Trier
const produitsTries = [...produitsFiltres].sort((a, b) => {
let comparaison = 0;
if (tri === "nom") {
comparaison = a.nom.localeCompare(b.nom);
} else if (tri === "prix") {
comparaison = a.prix - b.prix;
}
return ordre === "desc" ? -comparaison : comparaison;
});
return (
<div>
<input
type="text"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
placeholder="Rechercher..."
/>
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="nom">Trier par nom</option>
<option value="prix">Trier par prix</option>
</select>
<button onClick={() => setOrdre(ordre === "asc" ? "desc" : "asc")}>
{ordre === "asc" ? "Croissant" : "Decroissant"}
</button>
<p>{produitsTries.length} produit(s) trouve(s)</p>
{produitsTries.length === 0 ? (
<p>Aucun produit ne correspond a votre recherche.</p>
) : (
<ul>
{produitsTries.map((p) => (
<li key={p.id}>
{p.nom} - {p.prix.toFixed(2)} EUR ({p.categorie})
</li>
))}
</ul>
)}
</div>
);
}
Communication entre composants
Parent vers Enfant : les props
C'est le flux de donnees naturel en React. Le parent passe des donnees via les props :
function Parent() {
const message = "Bonjour depuis le parent";
return <Enfant message={message} />;
}
function Enfant({ message }) {
return <p>{message}</p>;
}
Enfant vers Parent : callback props
L'enfant ne peut pas modifier directement les donnees du parent. La solution : le parent passe une fonction (callback) a l'enfant. L'enfant appelle cette fonction pour communiquer avec le parent.
function Parent() {
const [message, setMessage] = useState("");
// Le parent definit une fonction et la passe a l'enfant
const recevoirMessage = (msg) => {
setMessage(msg);
};
return (
<div>
<p>Message recu : {message}</p>
<Enfant onEnvoyerMessage={recevoirMessage} />
</div>
);
}
function Enfant({ onEnvoyerMessage }) {
const [saisie, setSaisie] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
// L'enfant appelle la fonction du parent
onEnvoyerMessage(saisie);
setSaisie("");
};
return (
<form onSubmit={handleSubmit}>
<input
value={saisie}
onChange={(e) => setSaisie(e.target.value)}
placeholder="Votre message"
/>
<button type="submit">Envoyer</button>
</form>
);
}
Convention de nommage : les props de type callback commencent souvent par on : onAjouter, onSupprimer, onChangement, onEnvoyerMessage.
Lifting state up : remonter le state
Quand deux composants freres doivent partager des donnees, on remonte le state au parent commun. Le parent devient le proprietaire du state et le transmet aux enfants via les props.
// AVANT : chaque composant a son propre state (pas partage)
// APRES : le state est dans le parent commun
function App() {
const [produits, setProduits] = useState([
{ id: 1, nom: "Clavier", prix: 49.99 },
{ id: 2, nom: "Souris", prix: 29.99 },
]);
const ajouterProduit = (nouveauProduit) => {
setProduits([...produits, { id: Date.now(), ...nouveauProduit }]);
};
const supprimerProduit = (id) => {
setProduits(produits.filter((p) => p.id !== id));
};
return (
<div>
<h1>Gestion des produits</h1>
<FormulaireAjout onAjouter={ajouterProduit} />
<ListeProduits produits={produits} onSupprimer={supprimerProduit} />
</div>
);
}
function FormulaireAjout({ onAjouter }) {
const [nom, setNom] = useState("");
const [prix, setPrix] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (nom.trim() === "" || prix === "") return;
onAjouter({ nom, prix: parseFloat(prix) });
setNom("");
setPrix("");
};
return (
<form onSubmit={handleSubmit}>
<input
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Nom du produit"
/>
<input
type="number"
value={prix}
onChange={(e) => setPrix(e.target.value)}
placeholder="Prix"
step="0.01"
/>
<button type="submit">Ajouter</button>
</form>
);
}
function ListeProduits({ produits, onSupprimer }) {
if (produits.length === 0) {
return <p>Aucun produit.</p>;
}
return (
<ul>
{produits.map((p) => (
<li key={p.id}>
{p.nom} - {p.prix.toFixed(2)} EUR
<button onClick={() => onSupprimer(p.id)}>Supprimer</button>
</li>
))}
</ul>
);
}
Ce schema est fondamental. Il represente le flux de donnees unidirectionnel de React :
- Les donnees descendent (parent vers enfant via props).
- Les actions remontent (enfant vers parent via callback props).
React Router
Installation
React Router permet de gerer la navigation dans une SPA (Single Page Application). L'URL change, mais la page ne se recharge pas.
npm install react-router-dom
Configuration de base
// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
// src/App.jsx
import { Routes, Route } from "react-router-dom";
import Accueil from "./pages/Accueil";
import Produits from "./pages/Produits";
import DetailProduit from "./pages/DetailProduit";
import Contact from "./pages/Contact";
import NotFound from "./pages/NotFound";
import Navigation from "./components/Navigation";
function App() {
return (
<div>
<Navigation />
<main>
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/produits" element={<Produits />} />
<Route path="/produits/:id" element={<DetailProduit />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
);
}
export default App;
Composants cles :
BrowserRouter: englobe toute l'application, active le routage.Routes: conteneur des routes, affiche la premiere route correspondante.Route: definit une association entre un chemin (path) et un composant (element).path="*": route par defaut (404), correspond a toute URL non definie.path="/produits/:id": route avec parametre dynamique.
Navigation avec Link et NavLink
On ne doit jamais utiliser <a href="..."> pour la navigation interne. Cela provoquerait un rechargement complet de la page. On utilise Link ou NavLink.
import { Link, NavLink } from "react-router-dom";
function Navigation() {
return (
<nav>
{/* Link : navigation simple */}
<Link to="/">Accueil</Link>
{/* NavLink : ajoute automatiquement une classe "active"
quand l'URL correspond */}
<NavLink
to="/produits"
className={({ isActive }) => (isActive ? "lien-actif" : "")}
>
Produits
</NavLink>
<NavLink to="/contact">Contact</NavLink>
</nav>
);
}
NavLink est utile pour les menus de navigation : il permet de styler le lien actif differemment.
useNavigate : navigation programmatique
Pour naviguer depuis le code (apres une soumission de formulaire, par exemple) :
import { useNavigate } from "react-router-dom";
function FormulaireConnexion() {
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
// ... verification des identifiants ...
// Redirection vers l'accueil
navigate("/");
// Ou remplacer l'historique (l'utilisateur ne peut pas revenir en arriere)
// navigate("/", { replace: true });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">Se connecter</button>
</form>
);
}
useParams : recuperer les parametres d'URL
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
function DetailProduit() {
const { id } = useParams(); // Recupere le :id de l'URL
const [produit, setProduit] = useState(null);
const [chargement, setChargement] = useState(true);
useEffect(() => {
fetch(`/api/produits/${id}`)
.then((res) => res.json())
.then((data) => {
setProduit(data);
setChargement(false);
})
.catch(() => setChargement(false));
}, [id]); // Re-fetch si l'id change
if (chargement) return <p>Chargement...</p>;
if (!produit) return <p>Produit non trouve.</p>;
return (
<div>
<h1>{produit.nom}</h1>
<p>Prix : {produit.prix} EUR</p>
<p>{produit.description}</p>
</div>
);
}
Routes imbriquees et Layout
On peut creer un layout (disposition commune) avec des routes imbriquees :
import { Routes, Route, Outlet } from "react-router-dom";
// Layout : structure commune (header, footer, navigation)
function Layout() {
return (
<div>
<header>
<Navigation />
</header>
<main>
{/* Outlet affiche le composant de la route enfant correspondante */}
<Outlet />
</main>
<footer>
<p>Mon application - 2024</p>
</footer>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
{/* Routes enfants : affichees dans le <Outlet /> du Layout */}
<Route index element={<Accueil />} />
<Route path="produits" element={<Produits />} />
<Route path="produits/:id" element={<DetailProduit />} />
<Route path="contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
<Route index> correspond au chemin parent exact (/ ici). C'est equivalent a <Route path="" element={...} />.
Page 404
function NotFound() {
return (
<div>
<h1>404 - Page non trouvee</h1>
<p>La page que vous cherchez n'existe pas.</p>
<Link to="/">Retourner a l'accueil</Link>
</div>
);
}
Exercice : application multi-pages
Creer une application avec :
- Une page d'accueil
- Une page "Produits" qui affiche une liste (donnees statiques)
- Une page "Detail Produit" accessible en cliquant sur un produit (route dynamique
/produits/:id) - Une page "A propos"
- Une navigation avec NavLink (lien actif stylise)
- Une page 404
La solution combine tous les elements vus ci-dessus. La structure des fichiers :
src/
components/
Navigation.jsx
pages/
Accueil.jsx
Produits.jsx
DetailProduit.jsx
APropos.jsx
NotFound.jsx
App.jsx
main.jsx
Appels API avec React
Le pattern standard
Le pattern pour les appels API combine trois elements : useState (3 states), useEffect (declenchement), et fetch (requete).
function useDonnees(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const charger = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erreur ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
charger();
}, [url]);
return { data, loading, error };
}
Service API separe
Pour une application structuree, on cree un module de service API qui centralise tous les appels :
// src/services/api.js
const API_URL = "http://localhost:3000/api";
// Fonction utilitaire pour gerer les reponses
async function handleResponse(response) {
if (!response.ok) {
const erreur = await response.json().catch(() => ({}));
throw new Error(erreur.message || `Erreur HTTP ${response.status}`);
}
return response.json();
}
// --- PRODUITS ---
export async function getProduits() {
const response = await fetch(`${API_URL}/produits`);
return handleResponse(response);
}
export async function getProduitById(id) {
const response = await fetch(`${API_URL}/produits/${id}`);
return handleResponse(response);
}
export async function creerProduit(produit) {
const response = await fetch(`${API_URL}/produits`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(produit),
});
return handleResponse(response);
}
export async function modifierProduit(id, modifications) {
const response = await fetch(`${API_URL}/produits/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(modifications),
});
return handleResponse(response);
}
export async function supprimerProduit(id) {
const response = await fetch(`${API_URL}/produits/${id}`, {
method: "DELETE",
});
return handleResponse(response);
}
CRUD complet avec React
Application complete connectee a un backend Express :
// src/pages/GestionProduits.jsx
import { useState, useEffect } from "react";
import {
getProduits,
creerProduit,
modifierProduit,
supprimerProduit,
} from "../services/api";
function GestionProduits() {
const [produits, setProduits] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
// State pour le formulaire
const [formData, setFormData] = useState({ nom: "", prix: "" });
const [enEdition, setEnEdition] = useState(null); // id du produit en edition
// READ : charger les produits au montage
useEffect(() => {
chargerProduits();
}, []);
const chargerProduits = async () => {
try {
setChargement(true);
const data = await getProduits();
setProduits(data);
} catch (err) {
setErreur(err.message);
} finally {
setChargement(false);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
// CREATE ou UPDATE
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (enEdition) {
// UPDATE
await modifierProduit(enEdition, {
nom: formData.nom,
prix: parseFloat(formData.prix),
});
setEnEdition(null);
} else {
// CREATE
await creerProduit({
nom: formData.nom,
prix: parseFloat(formData.prix),
});
}
setFormData({ nom: "", prix: "" });
await chargerProduits(); // Recharger la liste
} catch (err) {
setErreur(err.message);
}
};
// Preparer l'edition
const commencerEdition = (produit) => {
setEnEdition(produit.id);
setFormData({ nom: produit.nom, prix: produit.prix.toString() });
};
const annulerEdition = () => {
setEnEdition(null);
setFormData({ nom: "", prix: "" });
};
// DELETE
const handleSupprimer = async (id) => {
if (!window.confirm("Confirmer la suppression ?")) return;
try {
await supprimerProduit(id);
await chargerProduits();
} catch (err) {
setErreur(err.message);
}
};
if (chargement) return <p>Chargement...</p>;
if (erreur) return <p>Erreur : {erreur}</p>;
return (
<div>
<h1>Gestion des produits</h1>
<form onSubmit={handleSubmit}>
<input
name="nom"
value={formData.nom}
onChange={handleChange}
placeholder="Nom du produit"
required
/>
<input
name="prix"
type="number"
step="0.01"
value={formData.prix}
onChange={handleChange}
placeholder="Prix"
required
/>
<button type="submit">
{enEdition ? "Modifier" : "Ajouter"}
</button>
{enEdition && (
<button type="button" onClick={annulerEdition}>
Annuler
</button>
)}
</form>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prix</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{produits.map((p) => (
<tr key={p.id}>
<td>{p.nom}</td>
<td>{p.prix.toFixed(2)} EUR</td>
<td>
<button onClick={() => commencerEdition(p)}>Modifier</button>
<button onClick={() => handleSupprimer(p.id)}>Supprimer</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default GestionProduits;
Ce composant illustre le pattern CRUD complet :
- Create : formulaire de creation, appel POST.
- Read : chargement au montage avec useEffect, affichage dans un tableau.
- Update : mode edition (le formulaire se remplit, le bouton change de texte), appel PUT.
- Delete : confirmation, appel DELETE, rechargement de la liste.
Hooks supplementaires
useContext : partager des donnees sans prop drilling
Le prop drilling est le probleme ou l'on doit passer des props a travers plusieurs niveaux de composants intermediaires qui n'en ont pas besoin. useContext permet de partager des donnees directement avec les composants qui en ont besoin.
import { createContext, useContext, useState } from "react";
// 1. Creer le contexte
const ThemeContext = createContext();
// 2. Creer le Provider (composant qui fournit la valeur)
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("clair");
const basculerTheme = () => {
setTheme((prev) => (prev === "clair" ? "sombre" : "clair"));
};
// La valeur partagee avec tous les enfants
const valeur = { theme, basculerTheme };
return (
<ThemeContext.Provider value={valeur}>
{children}
</ThemeContext.Provider>
);
}
// 3. Utiliser le contexte dans n'importe quel composant enfant
function BoutonTheme() {
const { theme, basculerTheme } = useContext(ThemeContext);
return (
<button onClick={basculerTheme}>
Theme actuel : {theme}
</button>
);
}
function Contenu() {
const { theme } = useContext(ThemeContext);
return (
<div className={theme === "sombre" ? "fond-sombre" : "fond-clair"}>
<p>Contenu de la page</p>
<BoutonTheme />
</div>
);
}
// 4. Envelopper l'application avec le Provider
function App() {
return (
<ThemeProvider>
<Contenu />
</ThemeProvider>
);
}
Cas d'utilisation courants de useContext :
- Theme (clair/sombre)
- Utilisateur connecte (authentification)
- Langue (internationalisation)
- Panier d'achat
Exemple complet : contexte d'authentification
import { createContext, useContext, useState } from "react";
const AuthContext = createContext();
function AuthProvider({ children }) {
const [utilisateur, setUtilisateur] = useState(null);
const connexion = (email, motDePasse) => {
// Simuler une connexion
setUtilisateur({ email, nom: email.split("@")[0] });
};
const deconnexion = () => {
setUtilisateur(null);
};
return (
<AuthContext.Provider value={{ utilisateur, connexion, deconnexion }}>
{children}
</AuthContext.Provider>
);
}
// Hook personnalise pour simplifier l'utilisation
function useAuth() {
const contexte = useContext(AuthContext);
if (!contexte) {
throw new Error("useAuth doit etre utilise dans un AuthProvider");
}
return contexte;
}
// Utilisation dans un composant
function ProfilUtilisateur() {
const { utilisateur, deconnexion } = useAuth();
if (!utilisateur) {
return <p>Non connecte</p>;
}
return (
<div>
<p>Connecte en tant que : {utilisateur.nom}</p>
<button onClick={deconnexion}>Se deconnecter</button>
</div>
);
}
useRef
useRef a deux usages principaux :
1. Acceder a un element du DOM :
import { useRef } from "react";
function FormulaireAvecFocus() {
const inputRef = useRef(null);
const handleClick = () => {
// Acceder directement a l'element DOM
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} placeholder="Cliquer le bouton pour focus" />
<button onClick={handleClick}>Focus sur l'input</button>
</div>
);
}
2. Stocker une valeur persistante sans declencher de re-rendu :
function Chronometre() {
const [temps, setTemps] = useState(0);
const [enCours, setEnCours] = useState(false);
const intervalRef = useRef(null);
const demarrer = () => {
if (enCours) return;
setEnCours(true);
intervalRef.current = setInterval(() => {
setTemps((prev) => prev + 1);
}, 1000);
};
const arreter = () => {
clearInterval(intervalRef.current);
setEnCours(false);
};
const reinitialiser = () => {
clearInterval(intervalRef.current);
setEnCours(false);
setTemps(0);
};
return (
<div>
<p>{temps} secondes</p>
<button onClick={demarrer}>Demarrer</button>
<button onClick={arreter}>Arreter</button>
<button onClick={reinitialiser}>Reinitialiser</button>
</div>
);
}
La difference entre useRef et useState :
useState: quand la valeur change, le composant se re-rend.useRef: quand la valeur change (ref.current = ...), aucun re-rendu. Utile pour stocker des valeurs qui ne doivent pas declencher de mise a jour visuelle (references a des timers, valeurs precedentes, etc.).
useMemo et useCallback (notions)
Ces hooks sont des outils d'optimisation. Ils evitent des calculs ou creations inutiles lors des re-rendus.
useMemo : memorise le resultat d'un calcul couteux.
import { useMemo } from "react";
function ListeTriee({ produits, critere }) {
// Le tri n'est recalcule que si produits ou critere changent
const produitsTries = useMemo(() => {
console.log("Tri en cours...");
return [...produits].sort((a, b) => a[critere] - b[critere]);
}, [produits, critere]);
return (
<ul>
{produitsTries.map((p) => (
<li key={p.id}>{p.nom}</li>
))}
</ul>
);
}
useCallback : memorise une fonction pour eviter qu'elle soit recreee a chaque rendu.
import { useCallback } from "react";
function Parent() {
const [compteur, setCompteur] = useState(0);
// Sans useCallback, cette fonction est recreee a chaque rendu
// Avec useCallback, elle n'est recreee que si necessaire
const handleClick = useCallback(() => {
console.log("Clic");
}, []);
return (
<div>
<p>{compteur}</p>
<button onClick={() => setCompteur(compteur + 1)}>+1</button>
<EnfantOptimise onClick={handleClick} />
</div>
);
}
En BTS SIO, il suffit de connaitre l'existence de ces hooks et leur principe. On ne les utilise que pour des optimisations specifiques, pas systematiquement.
Custom Hooks : factoriser la logique reutilisable
Un custom hook est une fonction JavaScript dont le nom commence par use et qui utilise d'autres hooks. C'est le mecanisme standard pour reutiliser de la logique entre composants.
// Hook personnalise pour le fetch de donnees
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const charger = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) throw new Error(`Erreur ${response.status}`);
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
charger();
}, [url]);
return { data, loading, error };
}
// Utilisation dans un composant — beaucoup plus simple
function ListeUtilisateurs() {
const { data, loading, error } = useFetch("/api/utilisateurs");
if (loading) return <p>Chargement...</p>;
if (error) return <p>Erreur : {error}</p>;
return (
<ul>
{data.map((u) => (
<li key={u.id}>{u.nom}</li>
))}
</ul>
);
}
// Hook personnalise pour le localStorage
function useLocalStorage(cle, valeurInitiale) {
const [valeur, setValeur] = useState(() => {
const sauvegarde = localStorage.getItem(cle);
return sauvegarde !== null ? JSON.parse(sauvegarde) : valeurInitiale;
});
useEffect(() => {
localStorage.setItem(cle, JSON.stringify(valeur));
}, [cle, valeur]);
return [valeur, setValeur];
}
// Utilisation
function Preferences() {
const [theme, setTheme] = useLocalStorage("theme", "clair");
const [langue, setLangue] = useLocalStorage("langue", "fr");
return (
<div>
<button onClick={() => setTheme(theme === "clair" ? "sombre" : "clair")}>
Theme : {theme}
</button>
<select value={langue} onChange={(e) => setLangue(e.target.value)}>
<option value="fr">Francais</option>
<option value="en">English</option>
</select>
</div>
);
}
// Hook personnalise pour un formulaire
function useFormulaire(valeursInitiales) {
const [valeurs, setValeurs] = useState(valeursInitiales);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setValeurs({
...valeurs,
[name]: type === "checkbox" ? checked : value,
});
};
const reinitialiser = () => {
setValeurs(valeursInitiales);
};
return { valeurs, handleChange, reinitialiser, setValeurs };
}
// Utilisation
function FormulaireContact() {
const { valeurs, handleChange, reinitialiser } = useFormulaire({
nom: "",
email: "",
message: "",
});
const handleSubmit = (e) => {
e.preventDefault();
console.log(valeurs);
reinitialiser();
};
return (
<form onSubmit={handleSubmit}>
<input name="nom" value={valeurs.nom} onChange={handleChange} />
<input name="email" value={valeurs.email} onChange={handleChange} />
<textarea name="message" value={valeurs.message} onChange={handleChange} />
<button type="submit">Envoyer</button>
</form>
);
}
Bonnes pratiques
Organisation des fichiers
src/
components/ <- composants reutilisables
Bouton.jsx
Carte.jsx
Navigation.jsx
FormulaireRecherche.jsx
pages/ <- composants de pages (une par route)
Accueil.jsx
Produits.jsx
DetailProduit.jsx
Contact.jsx
NotFound.jsx
services/ <- appels API
api.js
produitService.js
utilisateurService.js
hooks/ <- hooks personnalises
useFetch.js
useLocalStorage.js
useFormulaire.js
contexts/ <- contextes React
AuthContext.jsx
ThemeContext.jsx
App.jsx
main.jsx
Conventions de nommage
| Element | Convention | Exemple |
|---|---|---|
| Composant | PascalCase | CarteProduit, ListeUtilisateurs |
| Fichier de composant | PascalCase.jsx | CarteProduit.jsx |
| Fonction, variable | camelCase | handleClick, nomUtilisateur |
| Hook personnalise | use + camelCase | useFetch, useLocalStorage |
| Constante | SCREAMING_SNAKE_CASE | API_URL, MAX_ITEMS |
| Props callback | on + Action | onAjouter, onSupprimer |
| Handler interne | handle + Action | handleClick, handleSubmit |
Principes fondamentaux
Un composant = une responsabilite. Si un composant fait trop de choses, le decouper en sous-composants.
Composants petits et reutilisables. Un composant de 200 lignes est trop long. Le decouper.
Pas de logique metier dans le JSX. Extraire les calculs dans des variables ou des fonctions avant le return.
// MAUVAIS : logique complexe dans le JSX
function ListeProduits({ produits }) {
return (
<div>
{produits
.filter((p) => p.prix > 10)
.sort((a, b) => a.nom.localeCompare(b.nom))
.map((p) => (
<div key={p.id}>
<h3>{p.nom}</h3>
<p>{p.prix > 100 ? "Cher" : "Abordable"} - {p.prix.toFixed(2)} EUR</p>
</div>
))}
</div>
);
}
// BON : logique separee du JSX
function ListeProduits({ produits }) {
const produitsFiltres = produits.filter((p) => p.prix > 10);
const produitsTries = [...produitsFiltres].sort((a, b) =>
a.nom.localeCompare(b.nom)
);
return (
<div>
{produitsTries.map((p) => (
<CarteProduit key={p.id} produit={p} />
))}
</div>
);
}
Toujours gerer les 3 etats d'un appel API : chargement, erreur, donnees. Ne jamais supposer que le fetch reussira.
Exercices d'examen corriges
Exercice 1 : Composant Carte Utilisateur
Enonce : Creer un composant CarteUtilisateur qui recoit en props un objet utilisateur (nom, email, role) et affiche ces informations. Si le role est "admin", afficher un badge rouge. Utiliser PropTypes.
Correction :
import PropTypes from "prop-types";
function CarteUtilisateur({ utilisateur }) {
return (
<div className="carte-utilisateur">
<h3>{utilisateur.nom}</h3>
<p>{utilisateur.email}</p>
<p>
Role : {utilisateur.role}
{utilisateur.role === "admin" && (
<span style={{ color: "red", fontWeight: "bold" }}> [Admin]</span>
)}
</p>
</div>
);
}
CarteUtilisateur.propTypes = {
utilisateur: PropTypes.shape({
nom: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
role: PropTypes.string.isRequired,
}).isRequired,
};
export default CarteUtilisateur;
Exercice 2 : Compteur avec limites
Enonce : Creer un composant Compteur qui affiche un nombre. Deux boutons : +1 et -1. Le compteur ne peut pas descendre en dessous de 0 ni depasser 10. Afficher un message quand la limite est atteinte.
Correction :
import { useState } from "react";
function Compteur() {
const [valeur, setValeur] = useState(0);
const MIN = 0;
const MAX = 10;
const incrementer = () => {
if (valeur < MAX) {
setValeur(valeur + 1);
}
};
const decrementer = () => {
if (valeur > MIN) {
setValeur(valeur - 1);
}
};
return (
<div>
<h2>Compteur : {valeur}</h2>
<button onClick={decrementer} disabled={valeur === MIN}>
-1
</button>
<button onClick={incrementer} disabled={valeur === MAX}>
+1
</button>
{valeur === MIN && <p>Minimum atteint</p>}
{valeur === MAX && <p>Maximum atteint</p>}
</div>
);
}
export default Compteur;
Exercice 3 : Liste de taches (Todo List)
Enonce : Creer une application de gestion de taches. Fonctionnalites : ajouter une tache, marquer comme terminee (barrer le texte), supprimer une tache, afficher le nombre de taches restantes.
Correction :
import { useState } from "react";
function TodoList() {
const [taches, setTaches] = useState([]);
const [saisie, setSaisie] = useState("");
const ajouterTache = (e) => {
e.preventDefault();
if (saisie.trim() === "") return;
setTaches([
...taches,
{ id: Date.now(), texte: saisie.trim(), terminee: false },
]);
setSaisie("");
};
const basculerTache = (id) => {
setTaches(
taches.map((t) =>
t.id === id ? { ...t, terminee: !t.terminee } : t
)
);
};
const supprimerTache = (id) => {
setTaches(taches.filter((t) => t.id !== id));
};
const tachesRestantes = taches.filter((t) => !t.terminee).length;
return (
<div>
<h1>Liste de taches</h1>
<p>{tachesRestantes} tache(s) restante(s) sur {taches.length}</p>
<form onSubmit={ajouterTache}>
<input
value={saisie}
onChange={(e) => setSaisie(e.target.value)}
placeholder="Nouvelle tache"
/>
<button type="submit">Ajouter</button>
</form>
<ul>
{taches.map((t) => (
<li key={t.id}>
<span
onClick={() => basculerTache(t.id)}
style={{
textDecoration: t.terminee ? "line-through" : "none",
cursor: "pointer",
}}
>
{t.texte}
</span>
<button onClick={() => supprimerTache(t.id)}>Supprimer</button>
</li>
))}
</ul>
</div>
);
}
export default TodoList;
Exercice 4 : Formulaire d'inscription controle
Enonce : Creer un formulaire d'inscription avec les champs : nom, email, mot de passe, confirmation du mot de passe. Valider que tous les champs sont remplis, que l'email contient un @, que le mot de passe fait au moins 8 caracteres, et que la confirmation correspond. Afficher les erreurs sous chaque champ.
Correction :
import { useState } from "react";
function FormulaireInscription() {
const [formData, setFormData] = useState({
nom: "",
email: "",
motDePasse: "",
confirmation: "",
});
const [erreurs, setErreurs] = useState({});
const [soumis, setSoumis] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
// Effacer l'erreur du champ modifie
if (erreurs[name]) {
setErreurs({ ...erreurs, [name]: "" });
}
};
const valider = () => {
const e = {};
if (formData.nom.trim() === "") {
e.nom = "Le nom est obligatoire";
}
if (!formData.email.includes("@")) {
e.email = "L'email doit contenir un @";
}
if (formData.motDePasse.length < 8) {
e.motDePasse = "Le mot de passe doit contenir au moins 8 caracteres";
}
if (formData.confirmation !== formData.motDePasse) {
e.confirmation = "Les mots de passe ne correspondent pas";
}
return e;
};
const handleSubmit = (e) => {
e.preventDefault();
const nouvellesErreurs = valider();
setErreurs(nouvellesErreurs);
if (Object.keys(nouvellesErreurs).length === 0) {
setSoumis(true);
console.log("Inscription :", formData);
}
};
if (soumis) {
return <p>Inscription reussie pour {formData.nom} !</p>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="nom">Nom :</label>
<input id="nom" name="nom" value={formData.nom} onChange={handleChange} />
{erreurs.nom && <p style={{ color: "red" }}>{erreurs.nom}</p>}
</div>
<div>
<label htmlFor="email">Email :</label>
<input id="email" name="email" type="email" value={formData.email} onChange={handleChange} />
{erreurs.email && <p style={{ color: "red" }}>{erreurs.email}</p>}
</div>
<div>
<label htmlFor="motDePasse">Mot de passe :</label>
<input id="motDePasse" name="motDePasse" type="password" value={formData.motDePasse} onChange={handleChange} />
{erreurs.motDePasse && <p style={{ color: "red" }}>{erreurs.motDePasse}</p>}
</div>
<div>
<label htmlFor="confirmation">Confirmer le mot de passe :</label>
<input id="confirmation" name="confirmation" type="password" value={formData.confirmation} onChange={handleChange} />
{erreurs.confirmation && <p style={{ color: "red" }}>{erreurs.confirmation}</p>}
</div>
<button type="submit">S'inscrire</button>
</form>
);
}
export default FormulaireInscription;
Exercice 5 : Fetch et affichage de posts
Enonce : Creer un composant qui charge et affiche la liste des posts depuis https://jsonplaceholder.typicode.com/posts (limiter aux 10 premiers). Gerer le chargement et les erreurs. Chaque post affiche le titre et le corps.
Correction :
import { useState, useEffect } from "react";
function ListePosts() {
const [posts, setPosts] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
const charger = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=10"
);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
const data = await response.json();
setPosts(data);
} catch (err) {
setErreur(err.message);
} finally {
setChargement(false);
}
};
charger();
}, []);
if (chargement) return <p>Chargement des posts...</p>;
if (erreur) return <p>Erreur : {erreur}</p>;
return (
<div>
<h1>Posts ({posts.length})</h1>
{posts.map((post) => (
<article key={post.id} style={{ marginBottom: "20px" }}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
export default ListePosts;
Exercice 6 : Filtrage en temps reel
Enonce : A partir de la liste de posts de l'exercice 5, ajouter un champ de recherche qui filtre les posts par titre en temps reel.
Correction :
import { useState, useEffect } from "react";
function PostsAvecRecherche() {
const [posts, setPosts] = useState([]);
const [recherche, setRecherche] = useState("");
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
const charger = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
);
if (!response.ok) throw new Error(`Erreur ${response.status}`);
const data = await response.json();
setPosts(data);
} catch (err) {
setErreur(err.message);
} finally {
setChargement(false);
}
};
charger();
}, []);
// Donnee derivee : on ne stocke PAS les resultats filtres dans un state
const postsFiltres = posts.filter((p) =>
p.title.toLowerCase().includes(recherche.toLowerCase())
);
if (chargement) return <p>Chargement...</p>;
if (erreur) return <p>Erreur : {erreur}</p>;
return (
<div>
<h1>Posts</h1>
<input
type="text"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
placeholder="Rechercher par titre..."
/>
<p>{postsFiltres.length} resultat(s)</p>
{postsFiltres.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
export default PostsAvecRecherche;
Exercice 7 : Navigation avec React Router
Enonce : Creer une application avec 3 pages : Accueil, Produits, Contact. La navigation utilise NavLink avec un style pour le lien actif. La page Produits affiche une liste avec des liens vers le detail de chaque produit (route dynamique /produits/:id).
Correction :
// src/App.jsx
import { Routes, Route } from "react-router-dom";
import Navigation from "./components/Navigation";
import Accueil from "./pages/Accueil";
import Produits from "./pages/Produits";
import DetailProduit from "./pages/DetailProduit";
import Contact from "./pages/Contact";
import NotFound from "./pages/NotFound";
function App() {
return (
<div>
<Navigation />
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/produits" element={<Produits />} />
<Route path="/produits/:id" element={<DetailProduit />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
export default App;
// src/components/Navigation.jsx
import { NavLink } from "react-router-dom";
function Navigation() {
const styleActif = ({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
color: isActive ? "blue" : "black",
});
return (
<nav>
<NavLink to="/" style={styleActif}>Accueil</NavLink>
{" | "}
<NavLink to="/produits" style={styleActif}>Produits</NavLink>
{" | "}
<NavLink to="/contact" style={styleActif}>Contact</NavLink>
</nav>
);
}
export default Navigation;
// src/pages/Produits.jsx
import { Link } from "react-router-dom";
const produits = [
{ id: 1, nom: "Clavier", prix: 49.99 },
{ id: 2, nom: "Souris", prix: 29.99 },
{ id: 3, nom: "Ecran", prix: 299.99 },
];
function Produits() {
return (
<div>
<h1>Nos produits</h1>
<ul>
{produits.map((p) => (
<li key={p.id}>
<Link to={`/produits/${p.id}`}>{p.nom}</Link> - {p.prix} EUR
</li>
))}
</ul>
</div>
);
}
export default Produits;
// src/pages/DetailProduit.jsx
import { useParams, Link } from "react-router-dom";
const produits = [
{ id: 1, nom: "Clavier", prix: 49.99, description: "Clavier mecanique RGB" },
{ id: 2, nom: "Souris", prix: 29.99, description: "Souris sans fil ergonomique" },
{ id: 3, nom: "Ecran", prix: 299.99, description: "Ecran 27 pouces 4K" },
];
function DetailProduit() {
const { id } = useParams();
const produit = produits.find((p) => p.id === parseInt(id));
if (!produit) {
return (
<div>
<p>Produit non trouve.</p>
<Link to="/produits">Retour aux produits</Link>
</div>
);
}
return (
<div>
<h1>{produit.nom}</h1>
<p>Prix : {produit.prix} EUR</p>
<p>{produit.description}</p>
<Link to="/produits">Retour aux produits</Link>
</div>
);
}
export default DetailProduit;
Exercice 8 : Completer du code React existant
Enonce : Le code suivant est incomplet. Completer les parties marquees /* TODO */ pour que l'application fonctionne : une liste de fruits avec ajout et suppression.
import { useState } from "react";
function ListeFruits() {
const [fruits, setFruits] = useState(["Pomme", "Banane", "Orange"]);
const [nouveauFruit, setNouveauFruit] = useState("");
const ajouter = () => {
/* TODO : ajouter le fruit et vider le champ */
};
const supprimer = (index) => {
/* TODO : supprimer le fruit a l'index donne */
};
return (
<div>
<input
value={nouveauFruit}
onChange={/* TODO */}
placeholder="Nouveau fruit"
/>
<button onClick={ajouter}>Ajouter</button>
<ul>
{fruits.map((fruit, index) => (
<li key={/* TODO */}>
{fruit}
<button onClick={/* TODO */}>X</button>
</li>
))}
</ul>
</div>
);
}
Correction :
import { useState } from "react";
function ListeFruits() {
const [fruits, setFruits] = useState(["Pomme", "Banane", "Orange"]);
const [nouveauFruit, setNouveauFruit] = useState("");
const ajouter = () => {
if (nouveauFruit.trim() === "") return;
setFruits([...fruits, nouveauFruit.trim()]);
setNouveauFruit("");
};
const supprimer = (index) => {
setFruits(fruits.filter((_, i) => i !== index));
};
return (
<div>
<input
value={nouveauFruit}
onChange={(e) => setNouveauFruit(e.target.value)}
placeholder="Nouveau fruit"
/>
<button onClick={ajouter}>Ajouter</button>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>
{fruit}
<button onClick={() => supprimer(index)}>X</button>
</li>
))}
</ul>
</div>
);
}
export default ListeFruits;
Exercice 9 : useContext pour le theme
Enonce : Creer un contexte de theme (clair/sombre). Un bouton permet de basculer. Tous les composants doivent reagir au changement de theme.
Correction :
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("clair");
const basculer = () => {
setTheme((prev) => (prev === "clair" ? "sombre" : "clair"));
};
return (
<ThemeContext.Provider value={{ theme, basculer }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
return useContext(ThemeContext);
}
function EnTete() {
const { theme, basculer } = useTheme();
return (
<header style={{
backgroundColor: theme === "sombre" ? "#333" : "#fff",
color: theme === "sombre" ? "#fff" : "#333",
padding: "10px",
}}>
<h1>Mon application</h1>
<button onClick={basculer}>
Passer en mode {theme === "clair" ? "sombre" : "clair"}
</button>
</header>
);
}
function Contenu() {
const { theme } = useTheme();
return (
<main style={{
backgroundColor: theme === "sombre" ? "#222" : "#f5f5f5",
color: theme === "sombre" ? "#eee" : "#222",
padding: "20px",
minHeight: "200px",
}}>
<p>Ceci est le contenu principal.</p>
<p>Le theme actuel est : {theme}</p>
</main>
);
}
function App() {
return (
<ThemeProvider>
<EnTete />
<Contenu />
</ThemeProvider>
);
}
export default App;
Exercice 10 : Application CRUD complete
Enonce : Creer une application de gestion d'etudiants. Fonctionnalites : afficher la liste (depuis une API simulee), ajouter un etudiant, modifier un etudiant, supprimer un etudiant. Utiliser un service API separe.
Correction :
// src/services/etudiantService.js
const API_URL = "http://localhost:3000/api/etudiants";
export async function getEtudiants() {
const response = await fetch(API_URL);
if (!response.ok) throw new Error("Erreur lors du chargement");
return response.json();
}
export async function creerEtudiant(etudiant) {
const response = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(etudiant),
});
if (!response.ok) throw new Error("Erreur lors de la creation");
return response.json();
}
export async function modifierEtudiant(id, etudiant) {
const response = await fetch(`${API_URL}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(etudiant),
});
if (!response.ok) throw new Error("Erreur lors de la modification");
return response.json();
}
export async function supprimerEtudiant(id) {
const response = await fetch(`${API_URL}/${id}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Erreur lors de la suppression");
return response.json();
}
// src/pages/GestionEtudiants.jsx
import { useState, useEffect } from "react";
import {
getEtudiants,
creerEtudiant,
modifierEtudiant,
supprimerEtudiant,
} from "../services/etudiantService";
function GestionEtudiants() {
const [etudiants, setEtudiants] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
const [formData, setFormData] = useState({ nom: "", prenom: "", classe: "" });
const [enEdition, setEnEdition] = useState(null);
const charger = async () => {
try {
setChargement(true);
const data = await getEtudiants();
setEtudiants(data);
setErreur(null);
} catch (err) {
setErreur(err.message);
} finally {
setChargement(false);
}
};
useEffect(() => {
charger();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (enEdition) {
await modifierEtudiant(enEdition, formData);
setEnEdition(null);
} else {
await creerEtudiant(formData);
}
setFormData({ nom: "", prenom: "", classe: "" });
await charger();
} catch (err) {
setErreur(err.message);
}
};
const commencerEdition = (etudiant) => {
setEnEdition(etudiant.id);
setFormData({
nom: etudiant.nom,
prenom: etudiant.prenom,
classe: etudiant.classe,
});
};
const annulerEdition = () => {
setEnEdition(null);
setFormData({ nom: "", prenom: "", classe: "" });
};
const handleSupprimer = async (id) => {
if (!window.confirm("Confirmer la suppression ?")) return;
try {
await supprimerEtudiant(id);
await charger();
} catch (err) {
setErreur(err.message);
}
};
if (chargement) return <p>Chargement...</p>;
return (
<div>
<h1>Gestion des etudiants</h1>
{erreur && <p style={{ color: "red" }}>Erreur : {erreur}</p>}
<form onSubmit={handleSubmit}>
<input
name="nom"
value={formData.nom}
onChange={handleChange}
placeholder="Nom"
required
/>
<input
name="prenom"
value={formData.prenom}
onChange={handleChange}
placeholder="Prenom"
required
/>
<input
name="classe"
value={formData.classe}
onChange={handleChange}
placeholder="Classe"
required
/>
<button type="submit">{enEdition ? "Modifier" : "Ajouter"}</button>
{enEdition && (
<button type="button" onClick={annulerEdition}>Annuler</button>
)}
</form>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prenom</th>
<th>Classe</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{etudiants.map((e) => (
<tr key={e.id}>
<td>{e.nom}</td>
<td>{e.prenom}</td>
<td>{e.classe}</td>
<td>
<button onClick={() => commencerEdition(e)}>Modifier</button>
<button onClick={() => handleSupprimer(e.id)}>Supprimer</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default GestionEtudiants;
Exercice 11 : Custom Hook useFormulaire
Enonce : Creer un hook personnalise useFormulaire qui gere l'etat d'un formulaire generique (valeurs, handleChange, reinitialisation). L'utiliser dans un formulaire de contact.
Correction :
import { useState } from "react";
function useFormulaire(valeursInitiales) {
const [valeurs, setValeurs] = useState(valeursInitiales);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setValeurs((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
const reinitialiser = () => {
setValeurs(valeursInitiales);
};
return { valeurs, handleChange, reinitialiser };
}
// Utilisation
function FormulaireContact() {
const { valeurs, handleChange, reinitialiser } = useFormulaire({
nom: "",
email: "",
sujet: "",
message: "",
});
const handleSubmit = (e) => {
e.preventDefault();
console.log("Donnees envoyees :", valeurs);
reinitialiser();
};
return (
<form onSubmit={handleSubmit}>
<input name="nom" value={valeurs.nom} onChange={handleChange} placeholder="Nom" />
<input name="email" value={valeurs.email} onChange={handleChange} placeholder="Email" />
<input name="sujet" value={valeurs.sujet} onChange={handleChange} placeholder="Sujet" />
<textarea name="message" value={valeurs.message} onChange={handleChange} placeholder="Message" />
<button type="submit">Envoyer</button>
</form>
);
}
export default FormulaireContact;
Exercice 12 : Completer un composant React
Enonce : Le code suivant est un composant de panier d'achat incomplet. Completer les fonctions manquantes.
import { useState } from "react";
function Panier() {
const [articles, setArticles] = useState([
{ id: 1, nom: "T-shirt", prix: 19.99, quantite: 1 },
{ id: 2, nom: "Jean", prix: 49.99, quantite: 2 },
]);
// TODO : augmenter la quantite d'un article
const augmenterQuantite = (id) => {
/* A completer */
};
// TODO : diminuer la quantite (minimum 1)
const diminuerQuantite = (id) => {
/* A completer */
};
// TODO : supprimer un article
const supprimerArticle = (id) => {
/* A completer */
};
// TODO : calculer le total du panier
const calculerTotal = () => {
/* A completer */
};
return (
<div>
<h1>Mon panier</h1>
{articles.map((a) => (
<div key={a.id}>
<span>{a.nom} - {a.prix} EUR x {a.quantite}</span>
<button onClick={() => diminuerQuantite(a.id)}>-</button>
<button onClick={() => augmenterQuantite(a.id)}>+</button>
<button onClick={() => supprimerArticle(a.id)}>Supprimer</button>
</div>
))}
<p>Total : {calculerTotal().toFixed(2)} EUR</p>
</div>
);
}
Correction :
import { useState } from "react";
function Panier() {
const [articles, setArticles] = useState([
{ id: 1, nom: "T-shirt", prix: 19.99, quantite: 1 },
{ id: 2, nom: "Jean", prix: 49.99, quantite: 2 },
]);
const augmenterQuantite = (id) => {
setArticles(
articles.map((a) =>
a.id === id ? { ...a, quantite: a.quantite + 1 } : a
)
);
};
const diminuerQuantite = (id) => {
setArticles(
articles.map((a) =>
a.id === id && a.quantite > 1
? { ...a, quantite: a.quantite - 1 }
: a
)
);
};
const supprimerArticle = (id) => {
setArticles(articles.filter((a) => a.id !== id));
};
const calculerTotal = () => {
return articles.reduce((total, a) => total + a.prix * a.quantite, 0);
};
return (
<div>
<h1>Mon panier</h1>
{articles.length === 0 ? (
<p>Le panier est vide.</p>
) : (
<>
{articles.map((a) => (
<div key={a.id}>
<span>
{a.nom} - {a.prix} EUR x {a.quantite} = {(a.prix * a.quantite).toFixed(2)} EUR
</span>
<button onClick={() => diminuerQuantite(a.id)} disabled={a.quantite === 1}>
-
</button>
<button onClick={() => augmenterQuantite(a.id)}>+</button>
<button onClick={() => supprimerArticle(a.id)}>Supprimer</button>
</div>
))}
<p>Total : {calculerTotal().toFixed(2)} EUR</p>
</>
)}
</div>
);
}
export default Panier;
Fin du playbook React. Ce document couvre l'integralite du programme React pour le BTS SIO SLAM : composants, props, state, evenements, formulaires controles, useEffect, appels API, React Router, hooks avances, et bonnes pratiques. Les 12 exercices corriges representent les types de questions que l'on retrouve a l'examen.