Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 :

tsx
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 :

js
// 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.

tsx
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 :

tsx
<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ègleHTMLJSX
Un seul élément racinePlusieurs éléments côte à côteLes envelopper dans un parent ou un fragment <>…</>
Toutes les balises fermées<br>, <img src="…"><br />, <img src="…" />
Classe CSSclass="badge"className="badge" (class est un mot réservé de JavaScript)
Étiquette de formulaire<label for="libelle"><label htmlFor="libelle">
Attributs en camelCaseonclick, tabindex, maxlengthonClick, tabIndex, maxLength (sauf aria-* et data-*, qui gardent leurs tirets)
Style en lignestyle="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 :

tsx
// 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 :

tsx
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 :

  1. porte un nom qui commence par une majuscule (BadgeEtat, pas badgeEtat) ;
  2. renvoie du JSX (ou null pour ne rien afficher).
tsx
// 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 :

tsx
// 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 :

tsx
// 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 :

text
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 :

tsx
// 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.
tsx
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.

tsx
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
  1. Le nom doit commencer par une majuscule : EnTete.
  2. return suivi d'un retour à la ligne renvoie undefined : il faut des parenthèses.
  3. Trois éléments frères : il faut un parent ou un fragment.
  4. class devient className.
  5. for devient htmlFor.
  6. <img> doit être fermée : <img … />. Et ajoute un alt, obligatoire pour l'accessibilité (chapitre 14).
tsx
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.

  1. <p>{2 + 3}</p>
  2. <p>{'2' + 3}</p>
  3. <p>{null}</p>
  4. <p>{['Repas', 'Taxi']}</p>
  5. <p>{true ? 'Validée' : 'Refusée'}</p>
  6. <p>{0 && 'Des frais'}</p>
Voir la réponseMasquer la réponse de l'exercice 2
  1. 5
  2. 23 : une chaîne plus un nombre donne une concaténation.
  3. Rien : null ne s'affiche pas.
  4. RepasTaxi : un tableau de chaînes s'affiche élément par élément, sans séparateur.
  5. Validée
  6. 0 : 0 && … vaut 0, 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 :

text
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.

tsx
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 useTitrePage du chapitre 11.
  • C est impur : sort modifie 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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Les props : passer des données à un composantRevoir le sommaire du cours