frontend / react
Internals : réconciliation et hydration niveau expert
Explication
Ce que vous allez apprendre
- Expliquer ce qu'est réellement le Virtual DOM et à quoi sert la réconciliation
- Comprendre la règle de diffing sur le type des éléments et ses conséquences sur le state
- Décrire le rôle de l'architecture Fiber dans le rendu interruptible
- Utiliser
startTransitionpour marquer un rendu comme non urgent - Diagnostiquer et corriger un mismatch d'hydration entre serveur et client
Dans quel contexte ?
Après une migration vers le rendu serveur, la console du navigateur affiche systématiquement un avertissement "Hydration failed because the initial UI does not match what was rendered on the server" sur la page src/pages/DashboardPage.jsx. En creusant, le composant affiche new Date().toLocaleTimeString() directement dans le rendu : le serveur produit une heure, le navigateur en produit une autre quelques millisecondes plus tard. Comprendre le mécanisme d'hydration explique précisément pourquoi ce genre de valeur non déterministe doit être différé après le montage.
Étape 1 : le rendu traité comme une boîte noire jusqu'ici
Jusqu'ici, ce cours a traité le rendu comme une boîte noire : on modifie le state, l'interface se met à jour. Cette leçon ouvre cette boîte noire.
Étape 2 : qu'est-ce que le Virtual DOM
Le "Virtual DOM" n'est rien d'autre qu'une représentation en mémoire, sous forme d'objets JavaScript légers, de ce que l'interface doit afficher.
Étape 3 : la réconciliation, comparer avant d'agir
À chaque rendu, React compare ce nouvel arbre au précédent — c'est la réconciliation — pour calculer le minimum de changements réels à appliquer au DOM.
Étape 4 : la règle qui structure cette comparaison
Deux éléments de type différent au même endroit provoquent un démontage complet suivi d'un remontage, perdant tout état interne. Deux éléments de même type sont simplement mis à jour sur place.
Étape 5 : l'architecture Fiber
Depuis React 16, cette comparaison se fait via une structure de données appelée Fiber, organisée pour découper le travail de rendu en petites unités interruptibles.
Étape 6 : ce que Fiber rend possible
C'est cette architecture qui rend possible le "rendu concurrent" : React peut mettre en pause un rendu coûteux, laisser le navigateur traiter une interaction urgente, puis reprendre.
Le batching, depuis React 18
Depuis React 18, plusieurs appels à setState dans un même "tick" sont automatiquement regroupés en un seul re-rendu, quel que soit le contexte.
Et pour finir, l'hydration
L'hydration est le mécanisme par lequel React "réattache" ses composants à du HTML déjà généré côté serveur, sans le recréer entièrement.
Piège fréquent
Rendre une valeur non déterministe (new Date(), Math.random(), window.innerWidth) directement dans le JSX provoque un mismatch d'hydration : le HTML généré côté serveur diffère de celui que produirait le client. Il faut différer ce genre de valeur après le montage, via useEffect, en affichant un placeholder identique des deux côtés en attendant.
| Concept | Rôle |
|---|---|
| Virtual DOM | Représentation en mémoire de l'UI, comparée d'un rendu à l'autre |
| Réconciliation | Calcule le minimum de changements réels à appliquer au DOM |
| Fiber | Structure qui rend le rendu interruptible et priorisable |
| Hydration | Réattache React à un DOM déjà généré côté serveur |
Et ensuite ?
Une fois ces internals compris, la prochaine étape explore les nouveautés de React 19 : Actions, useActionState et useOptimistic.
Commandes & code
Internals : réconciliation et hydration
Ce qui se passe réellement entre un appel à setState et la mise à jour des pixels à l'écran.
// Le Virtual DOM n'est qu'une représentation en mémoire : un arbre d'objets JS légers
// { type: "button", props: { onClick, children: "Envoyer" } }
// React compare (diff) le nouvel arbre au précédent -- c'est la RÉCONCILIATION
// Règle n°1 du diffing : deux éléments de TYPE différent produisent des arbres différents
function Exemple({ chargement }) {
// si "chargement" change, React DÉMONTE <Spinner> et MONTE <Contenu> --
// aucun état interne n'est conservé entre les deux (types différents)
return chargement ? <Spinner /> : <Contenu />;
}
// Règle n°2 : même type au même endroit -> React RÉUTILISE le noeud DOM et met juste à jour les props
function Compteur({ valeur }) {
// re-rendu avec une nouvelle "valeur" : le même <p> DOM est conservé,
// seul son textContent est mis à jour -- pas de démontage/remontage
return <p>{valeur}</p>;
}// Fiber : depuis React 16, l'arbre de réconciliation n'est plus une simple récursion synchrone,
// mais une structure de données (linked list de "fibers") permettant de DÉCOUPER le travail
// en unités interruptibles -- base du rendu concurrent (Concurrent Mode / React 18+)
// Chaque Fiber contient : le type, les props, un pointeur vers l'enfant/le frère/le parent,
// et le travail "en attente" -- ce qui permet à React de mettre en pause le rendu,
// laisser le navigateur traiter un événement urgent (frappe clavier), puis reprendre
// startTransition : signale explicitement un rendu comme NON urgent,
// interruptible par une interaction plus prioritaire
import { startTransition, useState } from "react";
function Recherche() {
const [terme, setTerme] = useState("");
const [resultats, setResultats] = useState([]);
function gererChangement(e) {
setTerme(e.target.value); // urgent : le texte tapé doit s'afficher immédiatement
startTransition(() => {
// non urgent : le calcul/rendu des résultats peut être interrompu
// si l'utilisateur tape encore avant qu'il ne se termine
setResultats(filtrerResultatsLourds(e.target.value));
});
}
return <input value={terme} onChange={gererChangement} />;
}// Batching automatique (React 18+) : TOUS les setState d'un même "tick" sont regroupés
// en UN SEUL re-rendu, même dans un setTimeout, une Promise ou un handler natif
// (avant React 18, seul le batching dans les handlers React était automatique)
function Composant() {
function gererClic() {
setTimeout(() => {
setA(1); // avant React 18 : 2 rendus séparés dans un setTimeout
setB(2); // depuis React 18 : 1 SEUL rendu, les deux mises à jour sont fusionnées
}, 0);
}
}// Hydration : côté SSR, le serveur envoie du HTML déjà rendu -- React doit ensuite
// "réattacher" son arbre de fibers à CE DOM existant, sans tout re-créer
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App />);
// hydrateRoot RÉUTILISE le DOM existant et y attache les event listeners --
// contrairement à createRoot qui construirait le DOM depuis zéro
// Piège classique : mismatch d'hydration -- le HTML serveur ne correspond pas
// à ce que le rendu client produirait
function Horodatage() {
return <p>{new Date().toLocaleTimeString()}</p>;
// le serveur rend une heure, le client une autre (quelques ms/s plus tard) -> WARNING de mismatch
}
// correction : rendre la valeur dynamique APRÈS le montage, via useEffect
function HorodatageCorrige() {
const [heure, setHeure] = useState(null);
useEffect(() => setHeure(new Date().toLocaleTimeString()), []);
return <p>{heure ?? "--:--:--"}</p>; // le serveur ET le premier rendu client affichent la même chose
}// Selective hydration (React 18+) : plusieurs <Suspense> peuvent s'hydrater INDÉPENDAMMENT
// et dans le désordre -- React priorise l'hydration de la zone avec laquelle l'utilisateur interagit
function Page() {
return (
<>
<Suspense fallback={<SqueletteEntete />}>
<Entete />
</Suspense>
<Suspense fallback={<SqueletteCommentaires />}>
<Commentaires /> {/* si l'utilisateur clique ici pendant l'hydration de l'Entête,
React interrompt et priorise CETTE zone */}
</Suspense>
</>
);
}Résumé
- La réconciliation compare le type des éléments avant leurs props : un changement de type démonte/remonte, sans conserver l'état interne.
- L'architecture Fiber rend le rendu interruptible, base du rendu concurrent et de
startTransition. - Depuis React 18, le batching des
setStateest automatique dans tous les contextes, pas uniquement dans les handlers React. hydrateRootréattache React à un DOM déjà rendu côté serveur : un mismatch entre HTML serveur et rendu client déclenche un warning à corriger.
Exercices pratiques
Mission : expliquer pourquoi le compteur du Spinner perd toujours son état
Objectif : Diagnostiquer une perte d'état liée à la règle de diffing sur le type, puis corriger un mismatch d'hydration causé par une valeur non déterministe.
Contexte
Sur DashboardPage.jsx, un compteur de tentatives de reconnexion (useState local) est défini À L'INTÉRIEUR du composant Spinner affiché pendant chargement. Un développeur constate que ce compteur revient systématiquement à zéro chaque fois que chargement repasse à true après avoir été false, alors que rien dans le code ne réinitialise explicitement ce state. Par ailleurs, la console affiche toujours l'avertissement de mismatch d'hydration à cause de new Date().toLocaleTimeString() rendu directement dans le JSX.