Créer un modèle mental pour React : réconciliation, état et hooks
Apprenez les raisons qui sous-tendent les concepts fondamentaux de React — la réconciliation, les composants, les props, l’état et les hooks — afin de développer une intuition plutôt que de mémoriser des API.
Si vous savez déjà écrire du code mais que vous n’avez pas vraiment exploré React — ou que vous n’y avez jeté qu’un bref coup d’œil — cet article est conçu pour vous.
Lorsque vous commencez à apprendre React, il est tentant de se lancer directement dans useState, useEffect, les props, les hooks et une longue liste d’autres API. Vous pouvez maîtriser la syntaxe, créer quelque chose qui fonctionne, sans pour autant vraiment comprendre pourquoi React se comporte de cette manière.
Cet article vise à combler ce manque.
Au lieu de considérer React comme un ensemble d’API à mémoriser, nous allons explorer les raisons qui sous-tendent sa conception et la manière dont ses éléments s’assemblent. La syntaxe est importante, mais elle devient bien plus facile à assimiler une fois que l’on comprend ce qui se passe en dessous. Ainsi, plutôt que de commencer par comment écrire du code React, nous développerons d’abord comment raisonner avec React.
Nous aborderons les composants, les props, l’état, le rendu, la réconciliation, les hooks, le prop drilling, le Context et le routage, en reliant chaque concept au problème qu’il est censé résoudre. L’objectif n’est pas de lister toutes les API offertes par React, mais de vous fournir un modèle mental qui vous permettra de comprendre rapidement ces API dès que vous les rencontrerez.
Cet algorithme s’appelle la réconciliation, et c’est un bon point de départ.
Réconciliation — Un algorithme qui a rendu React possible
Détecter les différences entre deux arbres DOM à partir de zéro et calculer l’ensemble minimal des modifications nécessaires est un problème qui peut prendre environ O(n³) de temps pour être résolu.
Mais supposons que le processus de réconciliation fasse quelques hypothèses sensées, et que vous lui donniez quelques indications sur l’endroit où les changements sont susceptibles d’apparaître ?
Voilà ! La complexité diminue à environ O(n).
Cela signifie qu’une approche qui nécessiterait naïvement environ 31 ans pour être terminée est réduite à quelque chose de proche de 16 minutes — simplement en s’appuyant sur des hypothèses et de légères indications de la part du développeur.
Attendez… indications ? Quelles sont exactement ces indications, et comment les fournir ?
Inutile de s’inquiéter. C’est plus simple qu’il n’y paraît, et nous l’expliquerons bientôt.
Pour l’instant, laissons la réconciliation faire son travail en arrière-plan et tournons-nous vers la question qui nous concerne vraiment en tant que développeurs.
Mais pourquoi se donner la peine d’utiliser React ?
Pourquoi React ?
Peu importe ce que l’on vous dit, il y a un véritable ajustement mental lorsque l’on passe de fichiers HTML, CSS et JavaScript classiques à des concepts tels que les composants, les hooks, les props et l’état.
Comparé à des frameworks comme FastAPI ou Go, React peut vraiment sembler exiger un apprentissage plus difficile.
Mais voilà le problème : cette difficulté est concentrée au début.
Dès que vous commencez à comprendre les mécanismes de React, vous constaterez que beaucoup de ces concepts qui semblent intimidants reposent en réalité sur des fondements étonnamment simples.
Vous n’êtes pas obligé de mémoriser toute votre application en même temps.
Au lieu de cela, vous pouvez vous concentrer sur un seul composant : les données qu’il reçoit, ce dont il doit tenir compte, et la manière dont son affichage doit être mis à jour.
Cette façon de penser compartmentalisée est ce qui rend la création et la maintenance d’interfaces complexes gérables. Et au final, c’est le résultat qui intéresse réellement les développeurs.
Rédiger du code qui est plus facile à créer, comprendre, modifier et maintenir. L’objectif ici est de vous aider à y parvenir.
Les quatre piliers de React :—
Composants
Un composant, au fond, est une fonction JavaScript qui prend en entrée un seul objet et renvoie une interface utilisateur écrite en JSX.
JSX est une extension de syntaxe pour JavaScript qui vous permet d’insérer des marques-pages similaires à HTML directement dans votre code JavaScript. Vous pouvez styliser les éléments en utilisant du CSS classique ou une bibliothèque de composants comme Tailwind CSS.
Toute application React suffisamment complexe n’est, en fin de compte, qu’un réseau de composants qui échangent des données entre eux.
Même si vous n’avez jamais travaillé avec React, vous pouvez quand même comprendre ce qu’un composant fait.
const data = {
question: "What is React?",
answer: "A JavaScript library for building user interfaces"
};
function FlashCard(data) {
return (
<div className="border rounded-lg bg-gray-50 p-4">
<h2>{data.question}</h2>
<p>{data.answer}</p>
</div>
);
}
En ignorant quelques particularités syntaxiques propres à React, voilà essentiellement toute l’idée de React.
C’est pas si mal, n’est-ce pas ?
Fondamentalement, nous fournissons des données à une fonction pour obtenir en retour une partie de l’interface utilisateur.
Ainsi, en tant que développeur, votre véritable tâche consiste à concevoir des composants propres en gardant trois questions à l’esprit :
- Quelles informations il reçoit-il ?
- Quelles informations il conserve-t-il ?
- Comment son apparence doit-elle être mise à jour ?
Qu’y a-t-il de compliqué dans quelques paramètres et des variables ? Ne pourrions-nous pas simplement passer les données dont nous avons besoin et stocker ce que nous voulons dans des variables locales ?
En quelque sorte — mais pas tout à fait.
Et que voulons-nous dire par mise à jour de l’interface utilisateur ? Imaginez deux vendeurs qui tentent de vendre la même voiture, mais que le manager n’en informe qu’un d’eux au sujet d’une modification de prix.
Que se passe-t-il pour l’autre vendeur ? Il continue de citer le prix ancien, sans s’en rendre compte.
Imaginons maintenant que le manager publie cette mise à jour dans un chat de groupe auquel tout le monde participe. En modifiant le prix une seule fois, toute l’équipe le voit immédiatement.
C’est essentiellement le problème que React a été conçu pour résoudre.
Dès qu’une information qui affecte ce qui est affiché à l’écran change, tout ce qui en dépend — les personnes dans notre analogie, les composants dans React — a besoin d’un moyen fiable pour découvrir ce changement et y réagir. C’est ce mécanisme que React fournit.
Props
Sont-function User(name, age, city) et function User(name, city) interchangeables ? Et function User(city, name) ?
Imaginons maintenant que vous ayez créé un composant affichant le nom et l’âge d’un utilisateur, qui est actuellement utilisé en 67 endroits différents dans votre codebase. Votre manager vous demande alors d’afficher également la ville de l’utilisateur. Mettre à jour chacune de ces utilisations prendrait des heures de travail.
Mais que se passerait-il si la fonction était conçue de telle manière que les appels anciens continuaient de fonctionner sans modification, tandis que les nouveaux appels pourraient optionnellement transmettre des données supplémentaires ?
Outil nécessaire ici : un seul objet représentant la liste des paramètres. React appelle cela props.
Les props sont simplement le moyen par lequel React regroupe toutes les données transmises à un composant en un seul objet.
/** So a component like this one **/
function User({ name = "Guest", age = 18, city = "Unknown" }) {
return (
<div>
<h2>{name}</h2>
<p>{age} years old</p>
<p>{city}</p>
</div>
);
}
/** Can be used in ways like **/
<User /> /** Guest, 18, Unknown **/
<User name="Pritam" /> /** Pritam, 18, Unknown **/
<User name="Rahul" age={21} /> /** Rahul, 21, Unknown **/
<User name="Priya" age={22} city="Delhi" /> /** Priya, 22, Delhi **/
États
Le gérant du concessionnaire aurait-il dû garder le nouveau prix pour lui ? Le dire uniquement à un vendeur ? Aux deux ? Ou l’annoncer à tout le monde au sein du concessionnaire, y compris les gardes de sécurité et le personnel d’entretien ?
Imaginez une boîte de rangement que vous remplissez de vos affaires. En regardant simplement la boîte de l’extérieur, pouvez-vous deviner si quelque chose à l’intérieur a changé ? Que se passe-t-il si la boîte est déplacée sur une étagère différente ? Au minimum, vous remarquerez que sa position a changé et en déduirez qu’il s’est produit quelque chose.
Imaginons maintenant que cette boîte soit le mécanisme utilisé par React pour conserver les valeurs que vous définissez.
Cela signifie qu’il doit exister un moyen de transmettre les valeurs mises à jour à ceux qui en dépendent, ainsi qu’un moyen pour ces derniers de savoir quand leur valeur a changé.
const [count, setCount] = useState(initialCount);
Avez-vous déjà rencontré cette appel à useState ?
Elle transmet au composant deux éléments :
- count — la valeur actuelle de l’état.
- setCount — une fonction que vous appelez pour demander la mise à jour de cette valeur d’état.
Alors, quel est le problème à écrire simplement value = 5 ?
React n’a aucun moyen de savoir que quelque chose à l’intérieur du tableau a changé !
En d’autres termes, vous avez besoin d’un mécanisme pour dire à React : « Hé, j’ai mis à jour cette valeur, vous devriez peut-être actualiser l’interface utilisateur. »
Est-ce là la piste mentionnée précédemment ? En quelque sorte, oui et non.
useState vous permet de stocker une valeur, de demander des mises à jour et, en même temps, d’informer React que le changement a eu lieu. Mais pourquoi faut-il informer explicitement React ?
Parce que sinon, vous finiriez par agir comme ce gestionnaire de concession automobile négligent.
Vous vous souvenez de l’erreur réelle ? Le gestionnaire a modifié le prix, mis à jour sa propre copie du tableau, et a oublié d’en informer les autres. Vous êtes plus malin que ça — vous n’aurez qu’à appeler setCount().
Alors, que se passe-t-il exactement une fois que vous appelez setCount() ?
React prend l’état mis à jour, rérendu à nouveau le composant pour déterminer à quoi doit ressembler l’interface utilisateur maintenant, puis s’appuie sur la réconciliation pour savoir ce qui doit vraiment changer à l’écran.
quelque chose a changé. C’est la réconciliation qui détermine quoi a changé et ce qui doit être mis à jour en conséquence.
Hooks
useState() — une fonction intégrée qui permet à un composant de conserver une partie de son état et vous offre un moyen de demander des mises à jour.
Lorsque cet état est mis à jour, deux choses peuvent se produire :
- La nouvelle valeur n’a aucun effet sur ce qui est rendu — React peut encore rérendre, mais rien ne change visiblement.
Les fonctions comme celle-ci, qui possèdent des capacités spéciales de React, sont appelées hooks. Il en existe quelques autres qu’il vaut la peine de connaître.
useRef() — utile lorsque vous avez besoin d’un conteneur pour une valeur qui n’a absolument rien à voir avec l’interface utilisateur.
const count = useRef(0);
count.current++;
Donc, la règle générale est : si la valeur qui change doit également modifier l’interface utilisateur, utilisez useState(). Si seule la valeur elle-même doit changer, sans aucune conséquence sur l’affichage, utilisez useRef().
Celui-ci a également un autre usage courant — faire référence à des éléments DOM réels — mais pour l’instant, ce modèle mental suffit.
useEffect() — à utiliser lorsque vous avez besoin que quelque chose s’exécute après que React ait terminé le rendu de l’interface utilisateur.
useEffect(() => {
console.log("Runs after every render");
});
useEffect(() => {
console.log("Runs once after the initial render");
}, []);
useEffect(() => {
console.log("After the initial render and whenever count changes");
}, [count]); /** Dependencies go here **/
useContext() — imaginez un arbre de composants où une donnée comme name doit traverser plusieurs niveaux de composants pour atteindre un composant UserName profondément imbriqué.
Élargissons maintenant cet arbre en y ajoutant plusieurs données qui doivent passer à travers des composants qui ne les utilisent même pas directement.
Cette approche est connue sous le nom de prop drilling.
L’API Context de React propose une solution : elle permet de rendre des données accessibles n’importe où dans l’arbre, sans avoir à les transmettre manuellement à travers chaque composant intermédiaire.
const ParentContext = createContext(null)
<ParentContext value={money}>
<ChildComponent />
</ ParentContext>
function ChildComponent() {
const money = useContext(ParentContext);
return <p>Money: {money}</p>;
}
Il existe bien d’autres points d’ancrage en dehors de ceux-ci, et il vaut la peine de les expérimenter par soi-même.
Jusqu’à présent, la discussion portait sur la manière dont React organise les composants et comment les données circulent entre eux. Mais une application réelle doit également décider quelles parties de cette interface doivent apparaître sous quels URLs.
React Router
React vous permet de créer des interfaces composées de composants qui se mettent à jour automatiquement, sans forcer le navigateur à recharger toute la page. Mais une application réelle a généralement besoin de plus d’une page, ce qui soulève une nouvelle question.
Que se passe-t-il lorsque votre application a besoin de plusieurs vues distinctes ?
Vous pourriez vouloir quelque chose comme ceci :
/home → Accueil
/dashboard → Tableau de bord
/profile → Profil
Si vous les connectez à l’aide de simples balises HTML, un clic sur elles déclenche une navigation complète du navigateur. Toute la page est alors effacée et l’application redémarre à zéro à la nouvelle adresse.
Ce que vous souhaitez vraiment, c’est que l’URL se mette à jour tandis que React détermine discrètement quels composants doivent être remplacés, sans jeter tout le reste.
C’est là que React Router intervient pour résoudre ce problème.
Pensez-y comme à une couche qui associe les URLs aux composants.
Par exemple :
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
React Router examine l’URL actuelle et affiche le composant qui lui est associé. BrowserRouter s’appuie sur l’API History du navigateur pour gérer la navigation côté client, et Routes sélectionne le Route qui correspond le mieux au chemin actuel.
Néanmoins, il y a un autre aspect à prendre en compte.
Que se passe-t-il si vous ne souhaitez pas que toute l’écran soit re-renderisé ?
Imaginez un layout comme celui-ci :
┌─────────────────────────────┐
│ Header │
├──────────┬──────────────────┤
│ │ │
│ Menu │ Page content │
│ │ │
├──────────┴──────────────────┤
│ Footer │
└─────────────────────────────┘
Le passage de /home à /dashboard ne devrait pas faire disparaître et réapparaître l’en-tête, la barre latérale ou le pied de page. Seule la zone de contenu principale doit changer.
C’est précisément le scénario pour lequel les routes imbriquées et <Outlet /> ont été conçues.
function Layout() {
return (
<>
<Header />
<Menu />
<Outlet />
<Footer />
</>
);
}
Les routes elles-mêmes peuvent alors être imbriquées les unes dans les autres :
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
</Route>
</Routes>
Avec cette configuration, Layout reste monté, et React Router insère la route enfant correspondante à l’intérieur de <Outlet />. Selon la documentation officielle de React Router, <Outlet /> indique l’endroit où la route enfant correspondante sera rendue.
La route index correspond à <Home /> avec le chemin /, ce qui en fait la vue par défaut affichée dans l’outil lorsque aucun chemin plus spécifique n’est actif.
Ainsi, passer de /home à /dashboard ne signifie pas vraiment remplacer toute la page. C’est plutôt comme si l’on disait :
">Laisser cette partie de l’interface telle quelle et remplacer uniquement cette section par le composant correspondant à la nouvelle route."
Que faire ensuite ?
Lorsque vous maîtrisez bien les problèmes que React vise à résoudre ainsi que les mécanismes qu’il utilise pour y parvenir, il est utile de consacrer du temps à la lecture de vrais projets de code afin de voir comment les équipes expérimentées structurent leurs applications.
Recherchez un dépôt qui montre comment une application React en production peut être organisée et conçue à grande échelle.
Au lieu de tenter d’absorber l’ensemble du codebase d’un seul coup, choisissez une seule fonctionnalité et suivez son développement à travers les différentes couches de l’application. Observez comment les composants sont regroupés, d’où proviennent les données, comment l’état est géré, et comment les différentes parties de l’application communiquent entre elles.
Il est également utile de trouver un exemple montrant comment React peut être associé à Redux dans une application fonctionnelle.
Si le projet que vous trouvez est plus ancien, ne considérez pas ses méthodes comme la norme actuelle pour React. Utilisez-le plutôt pour étudier comment une grande application peut être divisée en parties et comment Redux s’intègre dans cette structure.
Lorsque vous serez à l’aise pour naviguer dans la structure typique d’un fichier React et que vous serez capable de créer vos propres composants simples, l’étape suivante consistera à apprendre à développer des applications capables de fonctionner à grande échelle.
L’échelle d’une application React soulève ses propres défis, notamment :
- SSR et composants serveur — comment le comportement change lorsque des parties de l’application s’exécutent sur le serveur plutôt que complètement dans le navigateur.
- Gestion de l’état — que faire lorsque l’état de l’application devient trop volumineux ou trop partagé pour être géré confortablement avec l’état local et Context. Redux est une des options parmi plusieurs autres.
- Récupération et mise en cache des données — comment les applications en production gèrent les indicateurs de chargement, la gestion des erreurs, la mise en cache et le synchronisation des données clients avec le serveur.
- Performance — reconnaître quand le rendu devient réellement un goulot d’étranglement, et l’optimiser en conséquence plutôt que d’optimiser tout dès le départ.
Vous n’avez pas besoin de maîtriser chacun de ces sujets avant de commencer à développer.
Une approche plus pratique consiste à commencer à développer, rencontrer un problème spécifique, puis apprendre le concept ou l’outil qui permet de résoudre ce problème précis.
Finalement, l’objectif d’apprendre React n’a jamais été de mémoriser son interface API. Il s’agissait plutôt de comprendre pourquoi ces APIs existent et comment raisonner sur les problèmes qu’elles sont conçues pour résoudre.
Lectures complémentaires
- Comprendre les hooks personnalisés React : réutiliser la logique sans état partagé — Découvrez ce qu sont les hooks personnalisés React, comment ils extraient et partagent la logique gérant l’état entre les composants, ainsi que les erreurs fréquentes à éviter lors de leur création.
- Patterns de conception React : du programmation orientée objets classique aux hooks modernes — Explique comment les patterns logiciels classiques tels que Singleton, Factory et Observer s’appliquent dans React, ainsi que les patterns spécifiques à React comme les HOC, les hooks et les composants composés.