Des scripts DOM à l’interface utilisateur en fonction de l’état : le modèle fondamental de React
Pourquoi React remplace les mises à jour manuelles du DOM par des composants déclaratifs, et comment JSX, les props, l’état, le re-rendering ainsi que le flux de données unidirectionnel s’intègrent dans un même modèle mental.
Un bouton « J’aime » qui met à jour un compteur, change d’icône et affiche une animation est facile à créer avec des appels simples au DOM. Cependant, en avoir cinquante dans un flux en direct, chacun gérant également les commentaires, les partages et l’état enregistré, c’est là que le code DOM manuel commence à devenir insuffisant. React a été conçu pour résoudre ce type de problème : vous décrivez à quoi doit ressembler l’écran pour les données actuelles, et React détermine quels changements au DOM sont nécessaires.
Ce guide présente les concepts qui rendent cela possible, dans l’ordre où ils s’enchaînent : JSX, les composants, les props, l’état, le re-rendering, le style déclaratif, ainsi que la manière dont les données et les événements circulent à travers un arbre de composants. À la fin, vous devriez être capable de prédire quand un composant sera re-renderé, de décider où doit aller une donnée d’état, et d’identifier les erreurs fréquentes des débutants qui perturbent les mises à jour.
Le problème que React a été conçu pour résoudre
Au moment où les bibliothèques de composants n’étaient pas encore la norme, une interface utilisateur interactive nécessitait du code impératif : il fallait trouver les éléments puis effectuer manuellement chaque modification dès qu’un événement se produisait. Pour un simple bouton « J’aime », la configuration ressemblait à ceci :
// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;
et le gestionnaire de clic devait mémoriser tous les détails visuels des deux états :
button.addEventListener("click", function() {
if (!liked) {
liked = true;
count++;
countEl.textContent = count;
button.classList.add("liked");
button.classList.remove("unliked");
icon.src = "/icons/heart-filled.svg";
button.style.color = "#e0245e";
// trigger animation
button.classList.add("animate-pop");
setTimeout(() => button.classList.remove("animate-pop"), 300);
} else {
liked = false;
count--;
countEl.textContent = count;
button.classList.remove("liked");
button.classList.add("unliked");
icon.src = "/icons/heart-empty.svg";
button.style.color = "#6c757d";
}
});
Cela est gérable pour un seul bouton. Imaginez maintenant une page affichant 50 publications, chacune disposant de ses propres commandes pour « J’aime », commenter, partager et enregistrer, toutes susceptibles de changer en raison des clics de l’utilisateur ou de la réception de nouvelles données depuis le serveur. Le script se transforme alors en un réseau complexe d’appels aux éléments, d’écouteurs et de variables qui doivent être mis à jour manuellement. Trois modes de défaillance apparaissent à mesure que l’application grandit :
- Synchronisation. Le DOM affiche une chose tandis que vos variables indiquent autre chose. Si vous mettez à jour l’élément compteur mais oubliez la variable, ou inversement, vous devez alors déboguer deux sources de vérité différentes.
- Réutilisabilité. Le gestionnaire est lié à des identifiants d’éléments spécifiques et à une structure HTML particulière, de sorte que déplacer le bouton vers une autre page signifie devoir le réécrire.
- Complexité. Chaque nouvelle fonctionnalité vous oblige à comprendre tout ce avec quoi elle pourrait interagir. De petits changements peuvent endommager des parties éloignées de la page.
React, open source depuis 2013 par Facebook, répond à ces trois problèmes grâce à une idée simple : cesser d’émettre des commandes DOM et plutôt décrire l’interface utilisateur qui devrait exister pour un état donné. React compare cette description avec ce qui est affiché à l’écran et applique les différences. Vous décrivez ; React met à jour.
JSX : du markup qui se compile en JavaScript
La première chose inhabituelle dans le code React, c’est un markup ressemblant à de l’HTML à l’intérieur d’une fonction :
function Greeting() {
return (
<div className="greeting">
<h1>Hello, Priya!</h1>
<p>Welcome back.</p>
</div>
);
}
Cet markup est JSX, une extension de syntaxe qui vous permet d’écrire des arbres d’éléments dans des fichiers JavaScript. Ce n’est ni de l’HTML ni quelque chose que le navigateur comprend. Une étape de compilation (Babel, le compilateur TypeScript ou un bundler qui les utilise) le réécrit en appels de fonction ordinaires avant même que le code ne s’exécute. Vous écrivez :
// What you write (JSX):
return (
<h1 className="title">Hello!</h1>
);
et le compilateur produit quelque chose d’équivalent à :
// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");
React.createElement ne renvoie qu’un objet simple décrivant l’élément : son type, ses propriétés et ses enfants. React lit ces objets pour déterminer ce que le véritable DOM doit contenir. (Les transformations JSX plus récentes utilisent plutôt un outil différent provenant de react/jsx-runtime, mais le résultat est du même type d’objet de description.) Personne n’écrit ces appels manuellement ; JSX existe parce que le markup imbriqué est bien plus facile à lire.
Où JSX diffère d’HTML
La syntaxe est proche de celle d’HTML mais pas identique. Les différences qui posent le plus de problèmes sont :
// HTML JSX
// ───────────────────────────── ──────────────────────────────────
// class="title" className="title" ← JS reserved word
// for="email" htmlFor="email" ← JS reserved word
// <input> (self-closing optional) <input /> ← must self-close
// onclick="handler()" onClick={handler} ← camelCase, no quotes
// style="color: red" style={{ color: "red" }} ← JS object
class et for sont des mots réservés en JavaScript, c’est pourquoi JSX utilise className et htmlFor. Chaque élément doit être fermé. Les gestionnaires d’événements sont en camelCase et reçoivent une fonction, pas une chaîne de caractères. Les styles en ligne sont des objets. Pour en savoir plus sur les compromis derrière cette syntaxe, consultez pourquoi JSX n’est pas HTML.
Insérer des expressions JavaScript
Les accolades ouvrent une fenêtre vers JavaScript. Tout ce qui peut être évalué pour produire une valeur peut y être placé : variables, appels de fonctions, expressions ternaires, méthodes d’array. Dans cette carte, l’état en ligne est d’abord calculé :
function UserCard({ user }) {
const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;
puis plusieurs expressions façonnent le markup : une bio tronquée, un nom de classe conditionnel, une étiquette conditionnelle et une date formatée :
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
<span className={isOnline ? "badge-green" : "badge-grey"}>
{isOnline ? "Online" : "Offline"}
</span>
<p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
</div>
);
}
La règle est simple : les accolades servent à contenir des expressions, pas des instructions. Vous pouvez utiliser une expression ternaire mais pas un bloc if, ainsi que .map() mais pas une boucle for, directement à l’intérieur du markup.
Composants : fonctions qui retournent de l’UI
// A component is just a function that returns JSX
function Button() {
return (
<button className="btn">
Click me
</button>
);
}
Vous l’utilisez de la même manière qu’une balise HTML, et vous pouvez en placer autant que vous le souhaitez :
function App() {
return (
<div>
<Button />
<Button />
<Button />
</div>
);
}
Cela affiche trois boutons identiques à partir d’une seule définition.
Assemblage de petits composants en plus grands
Les composants deviennent puissants lorsqu’ils sont imbriqués. De petits éléments à fonction unique sont assemblés pour former des composants plus grands, comme des blocs de construction. Un avatar ne sait faire que afficher une image :
function Avatar({ src, alt }) {
return <img className="avatar" src={src} alt={alt} />;
}
Et une chaîne de composants légèrement plus grands s’y ajoute : un bloc de nom, une ligne d’informations utilisateur qui combine l’avatard et le nom, ainsi qu’une carte de message qui associe les informations utilisateur au corps du message :
function UserName({ name, handle }) {
return (
<div>
<strong>{name}</strong>
<span>@{handle}</span>
</div>
);
}function UserInfo({ user }) {
return (
<div className="user-info">
<Avatar src={user.avatar} alt={user.name} />
<UserName name={user.name} handle={user.handle} />
</div>
);
}function PostCard({ post, author }) {
return (
<article className="post-card">
<UserInfo user={author} />
<p>{post.content}</p>
<span>{post.likes} likes</span>
</article>
);
}
Chaque couche a une seule fonction. Avatar affiche une image, UserInfo dispose l’avatard à côté du nom, et PostCard structure un message complet. C’est l’approche que React encourage : diviser l’interface en les plus petits éléments pertinents, attribuer à chacun une responsabilité claire, puis les combiner. Le guide pour créer des composants React réutilisables aborde plus en détail la conception des composants.
Pourquoi les noms de composants sont-ils en majuscules
React distingue les éléments natifs des composants à l’aide de la première lettre. <button> devient un bouton DOM ; <Button> pousse React à rechercher une variable nommée Button dans le contexte et à l’appeler. Un composant nommé avec une lettre minuscule est considéré silencieusement comme une balise HTML inconnue, ce qui est une cause fréquente de la confusion « mon composant ne s’affiche pas ».
Props : configurer un composant depuis l’extérieur
Un bouton qui affiche toujours « Cliquez-moi » n’est pas très utile. Les props (abréviation de properties) permettent à un composant parent d’envoyer des données à un composant enfant, de sorte qu’une même définition peut servir pour de nombreuses situations.
Les props se déplacent dans une seule direction, du composant qui affiche un autre vers celui qui est affiché, et ils arrivent sous forme d’un seul argument objet. Le parent les écrit comme des attributs :
// Parent passes data as props (looks like HTML attributes)
function App() {
return (
<div>
<Button label="Submit" colour="blue" />
<Button label="Cancel" colour="grey" />
<Button label="Delete" colour="red" />
</div>
);
}
Et l’élément enfant déstructure ce dont il a besoin :
// Child receives them as an object
function Button({ label, colour }) {
return (
<button className={`btn btn-${colour}`}>
{label}
</button>
);
}
Une seule définition, trois boutons qui ont des apparences différentes car chacun a reçu des valeurs distinctes.
Les props peuvent contenir n’importe quelle valeur
Les chaînes de caractères ne sont qu’un début. Les nombres, les booléens, les tableaux, les objets et les fonctions sont tous des props valides. Une carte de produit, par exemple, peut recevoir un objet de données, une fonction de rappel et un drapeau :
function ProductCard({ product, onAddToCart, featured }) {
return (
<div className={`card ${featured ? "card-featured" : ""}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<p>{product.rating} ★ ({product.reviewCount} reviews)</p>
<button onClick={onAddToCart}>
Add to Cart
</button>
</div>
);
}
Au point d’appel, ces valeurs sont transmises entre crochets :
// Usage:
<ProductCard
product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
onAddToCart={() => addToCart(product.id)}
featured={true}
/>
Notez que le callback inline onAddToCart fait référence à product.id, mais product ici n’est que le littéral d’objet passé en tant que propriété, et non une variable dans le scope. Dans du code réel, on utiliserait une variable définie dans le composant parent, par exemple onAddToCart={() => addToCart(item.id)}. Le fait de transmettre des fonctions en tant que propriétés est la méthode standard permettant aux composants enfants de signaler des événements, ce qui sera abordé à nouveau plus loin.
Les propriétés sont en lecture seule
Un composant ne doit jamais modifier ses propres propriétés. Pendant toute la durée d’un rendu, celles-ci constituent des entrées fixes. Lorsqu’il est nécessaire de faire une modification, c’est le composant parent qui la procède et rend le composant enfant avec la nouvelle valeur. Réassigner une propriété à l’intérieur du composant enfant est une erreur :
// WRONG — never modify props
function Button({ count }) {
count = count + 1; // ← this is a mistake
return <button>{count}</button>;
}
La réaffectation ne modifie qu’une variable locale ; elle n’atteint jamais le composant parent et disparaît lors du rendu suivant. Lorsqu’un composant a besoin d’une valeur qui peut changer, c’est là que le state entre en jeu :
// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Ici, initialCount ne sert qu’à initialiser le state ; après le premier rendu, c’est le composant lui-même qui gère son compteur.
State : la mémoire propre d’un composant
Les props proviennent de l’extérieur. Le state est des données appartenant au composant et qu’il peut modifier au fil du temps. Lorsque le state change, React rend à nouveau le composant avec la nouvelle valeur, sans que vous ayez besoin de toucher directement au DOM.
Le state est créé à l’aide du hook useState, importé de React :
import { useState } from "react";
Un compteur illustre la forme de base :
function Counter() {
const [count, setCount] = useState(0);
// ↑ current value ↑ function to update it ↑ initial value return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useState(0) renvoie une paire : la valeur actuelle (count, qui commence à 0) et un fonctionnaire de mise à jour (setCount). Appeler ce fonctionnaire avec une nouvelle valeur indique à React de la stocker et de rérender le composant.
Réconstruction du bouton « J’aime » avec l’état
Le bouton « J’aime » impératif initial devient beaucoup plus simple. Commencez par l’import :
import { useState } from "react";
puis décrivez l’apparence du bouton pour toute combinaison de liked et count:
function LikeButton({ initialLikes }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(initialLikes); function handleClick() {
if (liked) {
setLiked(false);
setCount(c => c - 1);
} else {
setLiked(true);
setCount(c => c + 1);
}
} return (
<button
onClick={handleClick}
className={liked ? "btn-liked" : "btn-default"}
>
{liked ? "♥" : "♡"} {count}
</button>
);
}
Il n’y a aucune recherche d’éléments, aucune appel à classList et aucune affectation de textContent. Le gestionnaire met à jour deux valeurs d’état, et le JSX définit l’apparence du bouton en fonction de ces valeurs. React s’assure que le DOM correspond.
Remarquez le formulaire de mise à jour setCount(c => c - 1). En passant une fonction plutôt qu’une valeur, vous obtenez l’état le plus récent, ce qui est plus sûr lorsque plusieurs mises à jour sont en file d’attente dans le même événement.
Chaque instance conserve son propre état
L’état appartient à une instance spécifique d’un composant, et non à la définition du composant. Affichez trois boutons similaires : chacun possède son propre liked et count ; en cliquant sur l’un, les autres restent inchangés :
function Feed() {
return (
<div>
<LikeButton initialLikes={24} /> {/* has its own state */}
<LikeButton initialLikes={7} /> {/* has its own state */}
<LikeButton initialLikes={156} /> {/* has its own state */}
</div>
);
}
Qu’est-ce qui doit figurer dans l’état
Utilisez l’état pour les valeurs qui :
- changent au fil du temps en raison d’interactions ou de données reçues
- doivent mettre à jour l’interface utilisateur lorsqu’elles changent
- appartiennent à cette instance de composant en particulier
Évitez de stocker dans l’état tout ce que vous pouvez calculer à partir des propriétés ou de l’état existants (calculez-le plutôt pendant le rendu), ainsi que les valeurs qui changent sans nécessiter un nouveau rendu, comme l’ID d’un chronomètre, qui conviennent mieux dans un ref.
Comment fonctionne le nouveau rendu
Le nouveau rendu est l’élément moteur de tout ce modèle. Lorsque l’état change, React appelle à nouveau votre fonction de composant, obtient une description fraîche de l’interface utilisateur, la compare à la précédente et met à jour uniquement les parties du DOM qui diffèrent.
Lors du premier rendu, le compteur génère son markup et React crée les nœuds DOM :
Initial render:
count = 0
Component runs → returns <p>Count: 0</p> <button>Increment</button>
React creates DOM nodes
Lorsque l’utilisateur clique, le setter déclenche un nouveau rendu et React compare les deux descriptions :
User clicks Increment:
setCount(1) called
React re-renders the component
count = 1
Component runs again → returns <p>Count: 1</p> <button>Increment</button>
React compares: <p>Count: 0</p> vs <p>Count: 1</p>
React updates only the text node inside <p>
Button is unchanged — React leaves it alone
Seul le texte à l’intérieur du paragraphe change dans le DOM réel. Le bouton reste inchangé. React ne reconstruit pas la page ; il calcule l’ensemble minimal de modifications et n’applique que celles-ci, c’est pourquoi des mises à jour fréquentes sont généralement acceptables.
Qu’est-ce qui provoque la mise à jour d’un composant ?
Un composant se met à nouveau à jour lorsque son état propre change :
1. The component's own state changes (setCount, setUser, etc.)
↓
Component re-renders
Il se met également à jour dans quelques autres situations :
2. The component's props change (parent passes different values)
↓
Component re-renders3. A context the component uses changes
↓
Component re-renders4. The parent re-renders
↓
All children re-render (unless memoized)
En pratique, « props changés » et « parent mis à jour » désignent le même événement vu sous deux angles : un enfant ne reçoit de nouvelles props que parce que son parent s’est à nouveau mis à jour. La conséquence pratique est qu, par défaut, la mise à jour d’un parent se propage à tous ses enfants, que leurs props aient changé ou non, à moins qu’ils ne soient mémorisés.
Cette cascade ne pose que rarement problème. Un rendu n’est rien de plus qu’une appel de fonction qui crée des objets ; la partie coûteuse consiste à modifier le DOM réel, ce que React minimise au maximum. La plupart des problèmes de performance réels proviennent de la manière dont les composants et l’état sont structurés, et non du nombre brut de rendus.
Le DOM virtuel et la réconciliation
React conserve une représentation légère en JavaScript de l’interface utilisateur, souvent appelée DOM virtuel. Chaque rendu génère un nouvel arbre d’objets élémentaires, et React compare cet arbre avec l’arbre précédent dans un processus appelé réconciliation. Le résultat de cette comparaison est la liste des opérations à effectuer sur le DOM réel.
On ne travaille jamais directement avec cette couche ; il s’agit d’un détail d’implémentation. Cela est important car comparer des objets simples en mémoire est peu coûteux, tandis que les opérations réelles sur le DOM sont relativement lentes. En faisant passer chaque mise à jour par cette comparaison, React permet de regrouper et de minimiser les tâches coûteuses.
UI déclarative versus impérative
React est déclaratif, et comprendre ce mot représente le véritable changement de perspective. Comparez deux façons de rendre une liste.
Impérative : énumération des étapes
La version impérative vide d’abord le conteneur :
// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = ""; // clear it
puis crée, configure et ajoute manuellement chaque élément :
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item.name;
li.className = item.active ? "active" : "";
li.addEventListener("click", () => handleClick(item.id));
list.appendChild(li);
});
Le code est une séquence d’instructions : créer ce nœud, définir cette propriété, attacher cet écouteur, l’insérer là. Vous êtes responsable de ce qui se passe avant, après, et à chaque étape intermédiaire.
Déclarative : description du résultat
La version déclarative indique à quoi devrait ressembler la liste pour n’importe quel tableau items :
// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
<li
key={item.id}
className={item.active ? "active" : ""}
onClick={() => onItemClick(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}
Il n’y a pas de procédure « vider la liste, puis la reconstruire ». Vous décrivez l’objectif visé, et React trouve comment passer du DOM actuel à cet objectif. La propriété key indique à React quel élément correspond à quoi entre deux rendus, afin qu’il puisse déplacer, mettre à jour ou supprimer les bons éléments au lieu de les recréer tous.
Pourquoi le style déclaratif convient aux interfaces utilisateur
- Predictibilité. Avec les mêmes propriétés et état, un composant affiche toujours le même résultat. Vous pouvez considérer l’interface comme une fonction pure de vos données :
UI = f(state, props). - Moins à retenir. Vous n’avez pas besoin de suivre l’apparence actuelle du DOM ni les modifications nécessaires. Vous ne faites que décrire la situation actuelle.
L’arbre des composants et le flux de données unidirectionnel
Toute application React est un arbre. Un composant racine, généralement App, affiche ses enfants, qui à leur tour affichent les leurs, reflétant ainsi la structure de l’écran :
App
├── Navbar
│ ├── Logo
│ ├── NavLinks
│ └── UserMenu
│ ├── Avatar
│ └── DropdownMenu
├── Dashboard
│ ├── Sidebar
│ │ ├── SidebarLink (×5)
│ │ └── UserStats
│ └── MainContent
│ ├── StatsRow
│ │ ├── StatCard (×4)
│ └── RecentActivity
│ ├── ActivityItem (×10)
└── Footer
Les données descendent
Les données se déplacent du parent vers l’enfant via des props. Un composant ne peut pas accéder à l’état d’un frère ou d’un parent. C’est ce flux unidirectionnel qui permet de garder les grandes applications compréhensibles :
App (has user, notifications, theme)
│
├── Navbar (receives: user, notifications)
│ │
│ └── UserMenu (receives: user)
│ │
│ └── Avatar (receives: user.avatar, user.name)
│
└── Dashboard (receives: user, theme)
│
└── MainContent (receives: theme)
Avatar ne voit que ce que UserMenu lui transmet, et UserMenu ne voit que ce que Navbar lui transmet. Rien ne s’échappe vers les niveaux supérieurs ou latéraux, donc lorsque une valeur est incorrecte, il faut vérifier la chaîne des composants parents.
Flux des événements vers le haut
// Parent owns the state and passes down both the value and the updater
function App() {
const [searchQuery, setSearchQuery] = useState("");
et il transmet à ses enfants à la fois la valeur et le fonctionnement permettant de la modifier ; la barre de recherche appelle ce mécanisme chaque fois que l’entrée change :
return (
<div>
<SearchBar
query={searchQuery}
onChange={setSearchQuery} {/* passes the setter as a prop */}
/>
<Results query={searchQuery} />
</div>
);
}// Child receives the updater and calls it on user input
function SearchBar({ query, onChange }) {
return (
<input
value={query}
onChange={e => onChange(e.target.value)} {/* calls parent's setter */}
placeholder="Search..."
/>
);
}
La requête n’existe que dans App. SearchBar ne stocke rien ; il signale chaque frappe via la propriété onChange. Results reçoit la requête mise à jour en tant que propriété et se rérendu à nouveau. Les données descendent sous forme de propriétés, tandis que les événements montent sous forme d’appels de fonction. (Les commentaires explicatifs à l’intérieur des balises d’ouverture ne sont destinés qu’à la lecture ; dans un JSX réel, un commentaire entre accolades doit se trouver parmi les enfants, et à l’intérieur d’une balise on écrit un commentaire simple /* */ ou on l’omet.)
Erreurs qui perturbent les mises à jour de React
Mutation directe de l’état
Il est tentant de modifier directement un objet d’état et de le renvoyer :
// WRONG — mutating state directly
const [user, setUser] = useState({ name: "Priya", age: 28 });
Le premier exemple birthday modifie l’objet et transmet au setter la même référence, ce qui empêche la mise à jour de l’interface utilisateur. Le second exemple crée un nouvel objet à l’aide de l’opérateur spread, que React considère comme une modification :
function birthday() {
user.age = 29; // ← directly modifying the object
setUser(user); // ← same object reference — React sees no change
} // UI does NOT update// CORRECT — create a new object
function birthday() {
setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
}
React détermine si l’état a changé en comparant les références à l’aide de Object.is, et non en examinant leur contenu. Si vous modifiez un objet ou un tableau sur place puis que vous renvoyez la même référence, React en conclura qu’il ne s’est rien passé et pourra sauter l’affichage.
Les tableaux suivent la même règle. Tenter d’ajouter des éléments dans le tableau existant échoue :
// WRONG — mutating the array
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // ← modifies in place
setItems(items); // ← same reference — no re-render
alors que le fait de les répartir dans un nouveau tableau fonctionne :
// CORRECT — create a new array
setItems([...items, 4]); // ← spread creates a new array
Mélanger props et état
Un test rapide permet de savoir ce qui relève de quoi. La valeur provient-elle d’un parent ? C’est alors une prop, lisible uniquement. Le composant en est-il le propriétaire et la modifie-t-il au fil du temps ? C’est alors de l’état. Enfermer une valeur qui ne change jamais dans useState est un signe de confusion :
// Wrong: using state for something that should be a prop
function UserCard() {
const [userName] = useState("Priya"); // ← why is this state? It never changes here
return <p>{userName}</p>;
}
Si le parent contrôle les données, acceptez-les en tant que prop :
// Correct: static data the parent controls comes as a prop
function UserCard({ name }) {
return <p>{name}</p>;
}
Stocker des valeurs que l’on pourrait calculer
Tout ce qui change n’a pas besoin d’avoir son propre état. Conserver un compteur à côté de la liste pour laquelle il compte double l’information :
// Wrong: storing derived data in state
const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(0); // ← why is this state?
Désormais, toute modification de items exige une mise à jour correspondante de itemCount, et une seule mise à jour oubliée crée une incohérence entre les deux. Le calcul de ce compteur lors du rendu assure leur synchronisation par construction :
// Every time items changes, you have to remember to also update itemCount
// And if you forget once, they're out of sync// Correct: derive it during render
const [items, setItems] = useState([...]);
const itemCount = items.length; // ← just a variable — always in sync
Composants qui font tout
Un seul composant chargé de rendre une barre latérale, un tableau, un formulaire et plusieurs modaux est difficile à lire, à tester et à réutiliser. Une heuristique utile : si vous ne pouvez pas décrire en une courte phrase ce qu’un composant fait, il en fait trop.
// Hard to maintain — does everything
function UserDashboard() {
// manages user state
// fetches orders
// handles filters
// manages pagination
// controls modal open/close
// renders sidebar
// renders order table
// renders filter controls
// renders pagination
// renders modal
return ( /* 200 lines of JSX */ );
}
Le diviser selon ses responsabilités permet à chaque partie d’avoir un nom et une fonction bien définies :
// Better — clear single responsibilities
function UserDashboard() {
return (
<DashboardLayout>
<OrderFilters />
<OrderTable />
<OrderPagination />
<OrderDetailModal />
</DashboardLayout>
);
}
Concevoir une application à partir de composants
Définissez les limites avant d’écrire du code
Partez du design et identifiez les éléments. Tout ce qui se répète est un composant. Tout élément ayant une fonction bien définie est également un composant. Les éléments qui changent ensemble doivent être regroupés ; ceux qui changent indépendamment doivent être séparés. Pour une page de liste de produits, le design :
Looking at the design:
[ Filter Bar ]
[ Product Card ][ Product Card ][ Product Card ]
[ Product Card ][ Product Card ][ Product Card ]
[ Pagination ]
se décompose en cet arbre :
Components:
ProductPage
├── FilterBar
│ ├── FilterGroup (×3)
│ └── SortDropdown
├── ProductGrid
│ └── ProductCard (×N)
└── Pagination
Mettre l’état dans le parent commun le plus bas
Pour chaque élément d’état, trouvez le composant le plus bas de l’arbre qui en a besoin, et conservez l’état là :
If only ProductCard needs the "expanded" state → put it in ProductCard
If FilterBar and ProductGrid both need filters → put filters in ProductPage
If Pagination and ProductGrid both need currentPage → put it in ProductPage
Lorsque deux éléments frères ont besoin des mêmes données, déplacez l’état vers leur parent commun le plus proche et transmettez-le vers le bas. Cela est appelé « lifting state up ». Conserver l’état au plus bas possible limite également la quantité de contenu de l’arbre qui est réaffiché lorsqu’il change.
Créez d’abord une version statique, puis ajoutez l’état
Commencez par des données enregistrées à l’avance : pas de useState, pas de gestionnaires d’événements, seulement du markup piloté par des props. Une fois la structure définie, identifiez quels valeurs changent réellement avec le temps et introduisez un état uniquement pour celles-ci. La première étape consiste en une carte entièrement statique :
// Step 1: static — no state, hardcoded data
function ProductCard() {
return (
<div className="card">
<img src="/headphones.jpg" alt="Headphones" />
<h3>Wireless Headphones</h3>
<p>₹2,499</p>
<button>Add to Cart</button>
</div>
);
}
La deuxième étape remplace le contenu enregistré à l’avance par un prop product, et la troisième ajoute un petit état pour la confirmation « Ajouté », qui se réinitialise automatiquement après deux secondes :
// Step 2: accept props — still no state
function ProductCard({ product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button>Add to Cart</button>
</div>
);
}// Step 3: add state for interactivity
function ProductCard({ product, onAddToCart }) {
const [added, setAdded] = useState(false); function handleAdd() {
setAdded(true);
onAddToCart(product.id);
setTimeout(() => setAdded(false), 2000);
} return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button onClick={handleAdd} disabled={added}>
{added ? "Added ✓" : "Add to Cart"}
</button>
</div>
);
}
Comme la structure et l’interactivité sont séparées, chaque étape peut être vérifiée facilement indépendamment.
Points clés
Le modèle dans son ensemble, résumé. Pourquoi React existe :
Why React:
Plain JS DOM manipulation is hard to scale and maintain
React: describe the UI, let React handle DOM updates
ainsi que les aspects essentiels de chaque concept abordé ci-dessus, du JSX aux erreurs fréquentes :
JSX:
HTML-like syntax in JavaScript — compiled to React.createElement()
{} embeds any JavaScript expression
Use className (not class), onClick (not onclick)Components:
Functions that return JSX
Capital letter names (Button, not button)
Reusable, composable building blocksProps:
Data passed from parent to child (like function arguments)
Read-only — a component never modifies its own props
Can be strings, numbers, objects, arrays, functionsState:
const [value, setValue] = useState(initialValue)
Data owned by a component that changes over time
Calling the setter triggers a re-render
Each component instance has its own stateRe-rendering:
Happens when state changes, props change, or parent re-renders
React diffs the virtual DOM and updates only what changed
Not expensive — surgical DOM updatesDeclarative:
Describe what the UI should look like
React figures out what changed and how to update the DOM
UI = f(state, props) — predictable, testableData flow:
Props flow down (parent → child)
Events flow up (child calls parent's function)
One-way data flow keeps the application predictableCommon mistakes:
Mutating state directly (use spread / new objects)
Storing derived values in state (compute them during render)
Overloading one component (split by responsibility)
- Les composants sont des fonctions, les props en sont les arguments, et l’état en est la mémoire. L’interface utilisateur représente toujours une projection de l’état actuel.
- La ré-renderisation est conçue pour être peu coûteuse ; c’est le travail sur le DOM que React vous épargne qui constitue la partie coûteuse. Structurez bien l’état avant de vous soucier du nombre de rendus.
- Donnez toujours aux fonctions d’assignation de nouveaux objets et tableaux ; React compare les références, pas le contenu.
- Gardez l’état dans le composant le plus basique qui en a besoin, dérivez tout le reste lors du rendu, et laissez les événements remonter via des props de callback.
Le contexte, les effets, les hooks personnalisés et l’optimisation des performances s’appuient tous sur ces principes. Avec une maîtrise solide des composants, des props, de l’état et de la ré-renderisation, les aspects plus avancés de React deviennent des extensions d’un modèle que vous comprenez déjà, plutôt que de nouvelles règles à mémoriser.
Lectures complémentaires
- Éviter les problèmes de l’état silencieux dus à la mutation des références en JavaScript — Découvrez pourquoi modifier des objets et des tableaux par référence perturbe les mises à jour de React, pourquoi la fonction spread ne crée que des copies superficielles, et comment cloner en profondeur l’état de manière sûre.
- Comprendre les hooks personnalisés de React : réutilisation de la logique sans état partagé — Apprenez ce qu sont les hooks personnalisés de React, comment ils extraient et partagent la logique gérant l’état entre les composants, ainsi quels sont les erreurs fréquentes à éviter lors de leur création.