Retour au cours

frontend / react

Internals : réconciliation et hydration niveau expert

Leçon 241 exercice

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 startTransition pour 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.

ConceptRôle
Virtual DOMReprésentation en mémoire de l'UI, comparée d'un rendu à l'autre
RéconciliationCalcule le minimum de changements réels à appliquer au DOM
FiberStructure qui rend le rendu interruptible et priorisable
HydrationRé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.

jsx
// 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>;
}
jsx
// 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} />;
}
jsx
// 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);
    }
}
jsx
// 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
}
jsx
// 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 setState est automatique dans tous les contextes, pas uniquement dans les handlers React.
  • hydrateRoot ré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

1 disponible
1

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.

Résoudre l’exercice →