Chapitre 216 minIntermédiaire
JSX et composants React
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire du JSX valide et y insérer des expressions JavaScript ;
- créer, exporter et assembler des composants fonctions ;
- découper un écran en composants qui ont chacun une responsabilité ;
- reconnaître un composant impur et le corriger.
4 exercices corrigés
Un composant React est une fonction JavaScript dont le nom commence par une majuscule et qui renvoie un morceau d'interface. Ce morceau s'écrit en JSX, une syntaxe qui ressemble à du HTML mais qui est du JavaScript. Ce chapitre donne les règles du JSX, montre comment écrire, découper et assembler des composants, et explique pourquoi un composant doit rester une fonction « pure ».
C'est quoi le JSX ?
Le JSX (JavaScript XML) permet d'écrire du balisage directement dans le code :
const titre = <h1>Mes fiches de frais</h1>;
Ce n'est ni une chaîne de caractères ni du HTML. À la construction, Vite (avec le plugin React) transforme chaque balise en un appel de fonction qui crée un objet décrivant l'élément :
// Ce que devient la ligne précédente après compilation (simplifié)
import { jsx as _jsx } from 'react/jsx-runtime';
const titre = _jsx('h1', { children: 'Mes fiches de frais' });
// titre vaut un objet : { type: 'h1', props: { children: 'Mes fiches de frais' }, … }
Conséquence importante : un élément JSX est une valeur. Tu peux le ranger dans une variable, le passer à une fonction, le renvoyer, le mettre dans un tableau.
Insérer du JavaScript avec les accolades
Entre accolades { }, tu places n'importe quelle expression JavaScript : une variable, un calcul, un appel de fonction, un ternaire.
const prenom = 'Lucas';
const montant = 23.5;
const message = (
<p>
Bonjour {prenom}, ton frais de {montant.toFixed(2)} € représente {Math.round((montant / 120) * 100)} % du plafond.
</p>
);
// Affiche : Bonjour Lucas, ton frais de 23.50 € représente 20 % du plafond.
Une instruction (if, for, const x = …) n'est pas une expression : elle ne peut pas se mettre entre accolades. On la remplace par un ternaire, par && ou par map (chapitre 5), ou on fait le calcul avant le return.
Les accolades servent aussi pour les attributs dont la valeur est calculée :
<img src={photo.url} alt={`Justificatif du ${frais.date}`} />
<input type="number" min={0} step={0.01} />
Une valeur entre guillemets est une chaîne (type="number") ; une valeur entre accolades est une expression (min={0} passe le nombre 0).
Les règles du JSX
Le JSX ressemble au HTML, mais ce n'est pas du HTML. Six différences reviennent sans cesse :
| Règle | HTML | JSX |
|---|---|---|
| Un seul élément racine | Plusieurs éléments côte à côte | Les envelopper dans un parent ou un fragment <>…</> |
| Toutes les balises fermées | <br>, <img src="…"> | <br />, <img src="…" /> |
| Classe CSS | class="badge" | className="badge" (class est un mot réservé de JavaScript) |
| Étiquette de formulaire | <label for="libelle"> | <label htmlFor="libelle"> |
| Attributs en camelCase | onclick, tabindex, maxlength | onClick, tabIndex, maxLength (sauf aria-* et data-*, qui gardent leurs tirets) |
| Style en ligne | style="color: red; font-size: 14px" | style={{ color: 'red', fontSize: 14 }} : un objet, propriétés en camelCase |
Un composant qui renvoie deux éléments frères doit les envelopper. Si tu ne veux pas de <div> en trop dans la page, utilise un fragment :
// Erreur : « Adjacent JSX elements must be wrapped in an enclosing tag »
return (
<h1>Fiche d'octobre 2026</h1>
<p>Saisie en cours</p>
);
// Correct : le fragment <>…</> n'ajoute aucun élément au DOM
return (
<>
<h1>Fiche d'octobre 2026</h1>
<p>Saisie en cours</p>
</>
);
Les parenthèses autour d'un JSX sur plusieurs lignes ne sont pas décoratives : sans elles, un return suivi d'un retour à la ligne renvoie undefined (JavaScript ajoute un point-virgule automatique après return).
Les commentaires s'écrivent entre accolades : {/* À compléter */}.
Le JSX protège contre les injections
Tout ce que tu insères entre accolades est inséré comme texte, jamais comme du HTML :
const libelle = '<b>Taxi</b> <i>urgent</i>';
<td>{libelle}</td>
// La cellule affiche littéralement : <b>Taxi</b> <i>urgent</i> (pas de gras, pas d'italique)
React encode les caractères spéciaux. C'est une protection importante contre les failles XSS. Elle tombe dans quelques cas qu'il faut connaître (l'attribut dangerouslySetInnerHTML, une URL javascript: dans un href) : le cours Sécurité les détaille dans XSS et frameworks.
Écrire un composant
Un composant est une fonction qui :
- porte un nom qui commence par une majuscule (
BadgeEtat, pasbadgeEtat) ; - renvoie du JSX (ou
nullpour ne rien afficher).
// src/components/BadgeEtat.tsx
export function BadgeEtat() {
return <span className="badge badge-cr">Saisie en cours</span>;
}
On l'utilise ensuite comme une balise dans un autre composant :
// src/App.tsx
import { BadgeEtat } from './components/BadgeEtat';
export default function App() {
return (
<main>
<h1>Fiche de frais – octobre 2026</h1>
<BadgeEtat />
</main>
);
}
La majuscule n'est pas une convention de style : c'est elle qui permet à React de distinguer une balise HTML (<span>, en minuscule) d'un composant (<BadgeEtat>). Un composant nommé badgeEtat serait pris pour une balise HTML inconnue et n'afficherait rien d'utile.
Exports nommés et exports par défaut
Chaque composant vit en général dans son propre fichier, au nom du composant : BadgeEtat.tsx. Deux façons de l'exporter :
// Export nommé : on importe avec le même nom, entre accolades
export function BadgeEtat() { … }
import { BadgeEtat } from './components/BadgeEtat';
// Export par défaut : un seul par fichier, importé sans accolades, sous le nom qu'on veut
export default function App() { … }
import App from './App';
Ce cours utilise des exports nommés pour les composants (l'éditeur les retrouve et les renomme mieux) et l'export par défaut seulement pour App, comme le modèle de Vite.
Découper une interface en composants
Avant d'écrire un écran, on le découpe en composants. La règle : un composant a une seule responsabilité. Voici l'écran « Mes fiches » d'AppliFrais et son découpage :
App
└── Layout en-tête, navigation, zone principale
└── PageFiches l'écran « Mes fiches de frais »
├── FiltreEtat la liste déroulante des états
└── (liste)
└── CarteFiche une fiche : mois, état, nombre de frais
└── BadgeEtat le libellé coloré de l'état
Quelques critères pour savoir quand créer un composant :
- un morceau d'interface se répète (une carte par fiche) ;
- un morceau a sa propre logique (le filtre et sa liste d'options) ;
- un composant devient trop long à lire : au-delà d'une centaine de lignes, cherche ce qui peut sortir ;
- un morceau sert sur plusieurs écrans (le badge d'état apparaît dans la liste et dans le détail).
À l'inverse, ne crée pas un composant par balise : un composant de trois lignes utilisé une seule fois n'apporte rien.
Composer les composants
Les composants s'emboîtent comme des briques. Voici une première version de CarteFiche, encore avec des données écrites en dur, qui utilise BadgeEtat :
// src/components/CarteFiche.tsx
import { BadgeEtat } from './BadgeEtat';
export function CarteFiche() {
return (
<article className="carte-fiche">
<h2>Fiche de frais – octobre 2026</h2>
<BadgeEtat />
<p>2 frais hors forfait</p>
</article>
);
}
Elle est encore inutilisable : toutes les cartes afficheraient octobre 2026. Pour qu'un composant affiche des données différentes à chaque usage, il lui faut des props : c'est le chapitre 3.
Un composant doit être pur
React peut appeler la fonction d'un composant autant de fois qu'il veut : à chaque changement d'état, quand un parent est redessiné, deux fois de suite en mode strict. Pour que tout reste prévisible, un composant doit être une fonction pure :
- avec les mêmes entrées (props, état), il renvoie le même JSX ;
- il ne modifie rien qui existait avant son appel : pas de variable globale, pas de requête réseau, pas de DOM, pas d'écriture dans
localStorage.
let compteur = 0;
// Impur : chaque rendu modifie une variable extérieure
function Ligne() {
compteur = compteur + 1;
return <li>Ligne n° {compteur}</li>; // le numéro change à chaque rendu, et double en mode strict
}
Les actions qui modifient le monde extérieur (appeler une API, changer le titre de l'onglet) s'appellent des effets de bord. Elles ont leur place dans les gestionnaires d'événements (chapitre 4) ou, plus rarement, dans un effet (chapitre 6), jamais directement dans le corps du composant.
Exercices
Exercice 1 sur 4 Corriger le JSX Réussi À revoir
Ce composant contient six erreurs. Trouve-les et corrige-les.
function enTete() {
return
<h1 class="titre">AppliFrais</h1>
<img src="logo.svg">
<label for="recherche">Rechercher</label>
}
Voir la réponseMasquer la réponse de l'exercice 1
- Le nom doit commencer par une majuscule :
EnTete. returnsuivi d'un retour à la ligne renvoieundefined: il faut des parenthèses.- Trois éléments frères : il faut un parent ou un fragment.
classdevientclassName.fordevienthtmlFor.<img>doit être fermée :<img … />. Et ajoute unalt, obligatoire pour l'accessibilité (chapitre 14).
function EnTete() {
return (
<>
<h1 className="titre">AppliFrais</h1>
<img src="logo.svg" alt="" />
<label htmlFor="recherche">Rechercher</label>
</>
);
}
Le logo est décoratif (le titre dit déjà « AppliFrais ») : son alt est vide pour que les lecteurs d'écran l'ignorent.
Tu avais trouvé ?
Exercice 2 sur 4 Que s'affiche-t-il ? Réussi À revoir
Pour chaque expression, écris ce qui apparaît à l'écran.
<p>{2 + 3}</p><p>{'2' + 3}</p><p>{null}</p><p>{['Repas', 'Taxi']}</p><p>{true ? 'Validée' : 'Refusée'}</p><p>{0 && 'Des frais'}</p>
Voir la réponseMasquer la réponse de l'exercice 2
523: une chaîne plus un nombre donne une concaténation.- Rien :
nullne s'affiche pas. RepasTaxi: un tableau de chaînes s'affiche élément par élément, sans séparateur.Validée0:0 && …vaut0, et React affiche les nombres. C'est le piège du zéro (chapitre 5).
Tu avais trouvé ?
Exercice 3 sur 4 Découper un écran Réussi À revoir
L'écran de détail d'une fiche affiche : un lien « Retour à mes fiches », le titre avec le mois, le badge d'état, un tableau des frais hors forfait (une ligne par frais, avec un bouton « Supprimer », et une ligne de total), puis un formulaire d'ajout d'un frais avec trois champs et un bouton. Propose un découpage en composants et justifie chaque composant.
Voir la réponseMasquer la réponse de l'exercice 3
Un découpage raisonnable, celui que le cours construit :
PageFiche l'écran, qui charge la fiche et assemble le reste
├── BadgeEtat déjà utilisé dans la liste : on le réutilise
├── ListeFrais le tableau, sa ligne de total et les boutons de suppression
└── FormulaireFrais les champs, la validation, l'envoi
Le lien de retour et le titre restent dans PageFiche : deux lignes, aucune logique. On pourrait extraire une LigneFrais si chaque ligne devenait complexe (édition en place, par exemple) ; ici, le map du tableau suffit.
Tu avais trouvé ?
Exercice 4 sur 4 Pur ou impur ? Réussi À revoir
Lesquels de ces composants sont purs ? Pour les autres, dis où déplacer le code fautif.
function A({ montant }: { montant: number }) {
return <p>{(montant * 1.2).toFixed(2)} €</p>;
}
function B() {
document.title = 'Mes fiches';
return <h1>Mes fiches</h1>;
}
function C({ frais }: { frais: Frais[] }) {
frais.sort((a, b) => a.montant - b.montant);
return <ul>{frais.map((f) => <li key={f.id}>{f.libelle}</li>)}</ul>;
}
function D() {
const date = new Date().toLocaleTimeString('fr-FR');
return <p>Mis à jour à {date}</p>;
}
Voir la réponseMasquer la réponse de l'exercice 4
- A est pur : un calcul à partir des props.
- B est impur : il modifie le titre de l'onglet, un élément extérieur. Le changement va dans un effet, via le hook
useTitrePagedu chapitre 11. - C est impur :
sortmodifie le tableau reçu en props, qui appartient au parent. Il faut trier une copie :frais.toSorted((a, b) => a.montant - b.montant)(ou[...frais].sort(…)). - D n'est pas pur au sens strict : deux appels successifs donnent deux heures différentes. Pour un affichage, c'est souvent acceptable ; si l'heure doit rester figée (l'heure du dernier chargement), on la mémorise dans l'état au moment du chargement.
Tu avais trouvé ?