Accueil / Articles

De JSX aux objets simples : comment fonctionnent les éléments, composants et props de React

Découvrez ce à quoi se compile JSX, pourquoi les composants ne sont rien d’autre que des fonctions, et comment les props, les enfants, les valeurs par défaut, la propagation et les slots découlent de ce modèle mental unique.

Éditeur : REACTAPP.TOP9 min de lecture1983 mots

Sources: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

De nombreux bugs dans React proviennent d’une compréhension floue de ce qu’est réellement JSX, les composants et les props. Une fois que l’on sait que JSX se transforme en une appel de fonction, que cet appel produit un objet ordinaire, et qu’un composant est une fonction qui reçoit un seul objet d’arguments, des règles qui semblaient arbitraires commencent à prendre du sens. Cet article construit ce modèle en partant des résultats du compilateur, puis l’applique aux props, aux children, aux valeurs par défaut, au mécanisme de propagation des valeurs, à l’immutabilité et aux patterns de composition que vous utiliserez tous les jours.

JSX, ce sont des appels de fonction, pas du HTML

JSX est souvent qualifié d’« HTML à l’intérieur de JavaScript », ce qui est incomplet. Plus précisément, JSX est un sucre syntaxique que le compilateur transforme en appels de fonction ordinaires.

Prenons un titre simple :

const element = <h1 className="title">Hello World</h1>;

React.createElement, en transmettant le nom de la balise, un objet contenant les attributs et le contenu :

const element = React.createElement("h1", { className: "title" }, "Hello World");

C’est la transformation classique. Depuis React 17, la plupart des outils utilisent le runtime JSX automatique, qui émet plutôt des appels à une fonction jsx() importée de react/jsx-runtime ; il n’est donc plus nécessaire d’avoir React dans le champ de vision. L’idée reste identique : le markup devient un appel de fonction.

Cet appel renvoie un objet JavaScript pur appelé élément React :

{
  type: "h1",
  props: {
    className: "title",
    children: "Hello World"
  }
}

Ce que le modèle explique

Comprendre que « JSX devient une appel de fonction qui renvoie un objet » éclaire plusieurs problèmes courants :

  • Aucune instruction if à l’intérieur de JSX. On ne peut pas placer une instruction à l’intérieur d’un argument de fonction, il faut donc utiliser une expression ternaire ou &&, ou bien déplacer la logique avant le return.
  • className au lieu de class. JSX est JavaScript, et class y est un mot réservé.
  • Un seul élément racine. Une fonction renvoie une seule valeur, donc un composant doit renvoyer un seul élément (qui peut contenir de nombreux enfants).

Un composant est une fonction qui renvoie des éléments

Les composants sont des fonctions dont la valeur de retour est un élément React, ou null pour ne rien afficher.

function Greeting() {
  return <h1>Hello, Devendra!</h1>;
}

Il n’y a pas de mécanisme caché et, dans React moderne, il n’est pas nécessaire d’utiliser des classes. React appelle la fonction chaque fois qu’il doit savoir ce qui doit être affiché. Lorsque les données changent, React l’appelle à nouveau, reçoit un nouvel arbre d’éléments, le compare avec l’ancien et met à jour uniquement les parties du DOM qui diffèrent. Cette comparaison est appelée réconciliation.

Deux règles que chaque composant doit respecter

Les noms doivent commencer par une lettre majuscule

JSX utilise la capitalisation pour déterminer la signification d’une balise. Une balise en majuscules fait référence à un composant, ce qui pousse React à appeler votre fonction :

// ✅ React treats this as a component
<Greeting />

Une balise en minuscules est considérée comme un élément DOM intégré, donc React tenterait de créer un élément littéral <greeting> au lieu d’appeler une fonction quelconque :

// ❌ React treats this as a native DOM element (like <div>)
<greeting />

Retournez exactement une racine

Il est impossible de retourner deux éléments frères côte à côte, car cela revient à renvoyer deux valeurs depuis une seule fonction :

// ❌ This will throw an error
function Card() {
  return (
    <h2>Title</h2>
    <p>Description</p>
  );
}

Il existe deux solutions. Envelopper les éléments frères dans un élément parent, ou utiliser un Fragment, écrit <>...</>, qui les regroupe sans ajouter de nœud supplémentaire au DOM. Le fragment ci-dessous montre les deux versions côte à côte.

// ✅ Wrap in a parent
function Card() {
  return (
    <div>
      <h2>Title</h2>
      <p>Description</p>
    </div>
  );
}// ✅ Or use a Fragment (renders nothing extra in the DOM)
function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Préférez le Fragment sauf si l’enveloppe est nécessaire pour le stylage ou la mise en page.

Les props sont les arguments de la fonction

Si les composants sont des fonctions, les props en sont leurs arguments. Ici, le composant lit une valeur name à partir de l’objet qu’il reçoit :

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

Et ici, il est rendu avec cet attribut défini :

// Usage
<Greeting name="Devendra" />

React rassemble tous les attributs écrits sur la balise JSX dans un objet, props, et le transmet en tant que premier argument. C’est tout le mécanisme.

Déstructuration dans la signature

Revenir constamment sur props. devient rapidement encombrant. En déstructurant l’objet dans la liste des paramètres, on donne un nom à chaque prop utilisé et on documente d’un coup d’œil les entrées de la composante :

function Greeting({ name, role }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Le point d’appel reste inchangé :

<Greeting name="Devendra" role="Senior Engineer" />

Valeurs par défaut avec déstructuration

Lorsqu’un prop est optionnel, assignez-lui une valeur par défaut directement dans le schéma de déstructuration. Si role n’est pas fourni, il reprend automatiquement la valeur "Developer" :

function Greeting({ name, role = "Developer" }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Ainsi, cette utilisation affiche le rôle par défaut :

// role will be "Developer" if not provided
<Greeting name="Devendra" />

La déstructuration des valeurs par défaut ne s’applique que lorsque la valeur est undefined. Fournir explicitement null contourne cette valeur par défaut, ce qui convient de retenir lorsque les propriétés proviennent de données API.

children : composition intégrée à la syntaxe

Le contenu niché entre les balises d’ouverture et de fermeture d’un composant arrive automatiquement en tant que propriété children. Un Card peut simplement afficher tout ce qui lui est donné à l’intérieur de son conteneur :

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

C’est l’appelant qui décide de ce qui sera inséré à l’intérieur :

// Usage
<Card>
  <h2>My Title</h2>
  <p>Some description here.</p>
</Card>

La composition via children est le moyen de créer des composants flexibles et réutilisables. Plutôt que d’anticiper chaque variation avec une autre propriété, le composant fournit la structure tandis que le parent fournit le contenu. Des bibliothèques comme shadcn/ui et Material UI construisent des dialogues, des cartes et des boutons de cette manière.

Toute valeur JavaScript peut être une propriété

Les accolades courbes dans JSX signifient « évaluer ceci en JavaScript », ce qui permet à une propriété d’accepter n’importe quelle valeur : chaînes de caractères, nombres, booléens, tableaux, objets ou fonctions. Ce composant reçoit plusieurs types en même temps, affichant conditionnellement un badge, transformant un tableau en éléments de liste et associant une fonction de rappel à un bouton :

function UserCard({ name, age, isAdmin, hobbies, onDelete }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <ul>
        {hobbies.map(h => <li key={h}>{h}</li>)}
      </ul>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

Le point d’appel montre comment chaque type est transmis :

<UserCard
  name="Devendra"
  age={28}
  isAdmin={true}
  hobbies={["coding", "teaching", "gaming"]}
  onDelete={() => console.log("deleted")}
/>

Points à noter :

  • Les valeurs de chaîne simples peuvent être écrites directement entre guillemets.
  • Tout le reste, y compris les nombres, booléens, tableaux, objets et fonctions, doit être placé entre accolades courbes.
  • Une propriété fonctionnelle telle que onDelete permet à un enfant de signaler des événements à son parent. C’est ce modèle de callback qui permet de gérer l’état de manière centralisée : l’état se trouve dans un parent commun, et les enfants ne le modifient qu’en appelant des fonctions transmises par ce parent.
  • Les props sont en lecture seule

    N’altérez jamais les props. Attribuer une valeur à un champ d’un objet prop modifie silencieusement les données appartenant au parent :

    // ❌ Never do this
    function Greeting({ user }) {
      user.name = "Modified"; // This will cause silent bugs
      return <h1>{user.name}</h1>;
    }
    

    Obtenez plutôt une nouvelle valeur et laissez l’originale intacte :

    // ✅ Create a new value instead
    function Greeting({ user }) {
      const displayName = user.name.toUpperCase();
      return <h1>{displayName}</h1>;
    }
    

    Les données circulent dans une seule direction, du parent vers l’enfant, ce qui garantit la prévisibilité des applications React. Un enfant qui souhaite que le parent modifie quelque chose appelle une fonction fournie par ce dernier.

    Réorientation des props avec rest et spread

    Lorsqu’un composant enrobe un élément DOM, on souhaite souvent accepter de nombreux attributs sans devoir les lister un par un. La syntaxe Rest dans le paramètre permet d’extraire les propriétés que le composant gère lui-même, ici label, et de rassembler le reste dans inputProps, qui est ensuite appliqué à l’élément input:

    function Input({ label, ...inputProps }) {
      return (
        <div>
          <label>{label}</label>
          <input {...inputProps} />
        </div>
      );
    }
    

    Tout attribut supplémentaire, tel que type, placeholder ou onChange, est transmis directement à l’élément sous-jacent :

    // All extra props (type, placeholder, onChange, etc.) flow straight to <input>
    <Input
      label="Email"
      type="email"
      placeholder="you@example.com"
      onChange={handleChange}
    />
    

    Les bibliothèques utilisent constamment ce schéma, mais l’ajout arbitraire de propriétés à un élément DOM peut transmettre des valeurs qu’il ne comprend pas, ce qui déclenche des avertissements d’attribut inconnu. Soyez méticuleux quant à ce que vous ajoutez et où. L’exemple affiche également un label sans le relier à l’élément d’entrée ; dans du code réel, reliez-les avec htmlFor et un id afin que le label soit accessible.

    Trois niveaux de flexibilité des composants

    Les composants ont tendance à évoluer par des étapes reconnaissables au fur et à mesure que les exigences augmentent.

    Niveau 1 : une propriété pour chaque variation

    Le bouton le plus simple accepte une propriété distincte pour chaque élément qu’il pourrait afficher :

    function Button({ text, color, icon }) {
      return <button style={{ color }}>{icon} {text}</button>;
    }
    

    Chaque nouvelle variation nécessite une propriété supplémentaire, ce qui fait que l’API continue de croître sans limite. Ce problème est souvent appelé « explosion des propriétés » ; il diffère de la « forage de propriétés », qui consiste à transmettre des données à travers de nombreux niveaux.

    Niveau 2 : composition des enfants

    Permettre aux appelsants de transmettre du contenu sous la forme de children élimine la plupart de ces propriétés. Le bouton ne conserve qu’une variant pour le stylisme :

    function Button({ children, variant }) {
      return <button className={`btn btn-${variant}`}>{children}</button>;
    }
    

    Désormais, c’est l’appelant qui contrôle ce qui apparaît à l’intérieur, y compris les icônes :

    <Button variant="primary">
      <Icon name="save" /> Save Changes
    </Button>
    

    Niveau 3 : emplacements nommés

    Lorsqu’un composant a besoin de plusieurs zones distinctes, on peut accepter du JSX via des propriétés nommées. Un layout de page peut ainsi inclure un en-tête, une barre latérale et du contenu principal :

    function PageLayout({ header, sidebar, children }) {
      return (
        <div className="layout">
          <header>{header}</header>
          <aside>{sidebar}</aside>
          <main>{children}</main>
        </div>
      );
    }
    

    Chaque zone est remplie au moment de l’appel :

    <PageLayout
      header={<Navbar />}
      sidebar={<FilterPanel />}
    >
      <ProductList />
    </PageLayout>
    

    Les slots sont des propriétés ordinaires contenant du JSX : ceux qui appellent le composant contrôlent ce qui remplit chaque zone, le composant lui-même gère la structure, et l’agencement reste fonctionnel même en cas de changements de besoins. Pour en savoir plus sur cette approche de refacturation, consultez comment résoudre le problème de surcharge des props dans React grâce à la composition et aux slots.

    Le modèle en bref

    • JSX : un équivalent syntaxique pour les appels de fonctions créant des éléments, tels que React.createElement().
    • Élément React : un objet JavaScript simple décrivant l’interface utilisateur.
    • Composant : une fonction qui retourne des éléments React.
    • Props : l’objet contenant les arguments transmis à cette fonction.
    • children : une propriété spéciale qui contient du contenu JSX imbriqué.
  • Mutation des props : jamais autorisée ; considérez les props comme uniquement lus.
  • La pureté est la règle qui permet l’échelle

    Tout cela dépend de la pureté : les mêmes props doivent toujours produire le même résultat. Lorsque le rendu n’a aucun effet secondaire, ne mute jamais les props et ne modifie pas l’état externe ; React peut alors sauter, répéter ou reclasser les opérations en toute sécurité, et vous pouvez analyser chaque composant de manière isolée. Le mode strict rend même les composants deux fois en développement afin de mettre en évidence une logique impure.

    En brisant la pureté, vous invitez des bugs qui apparaissent loin de leur cause réelle. En la respectant, vos composants restent prévisibles, faciles à tester et faciles à maintenir au fur et à mesure que l’application se développe.