Jour 23 Day 23 · mercredi 2 septembre 2026 Wednesday 2 September 2026 Web Intermédiaire

React : Virtual DOM, hooks & reconciliation React: Virtual DOM, hooks & reconciliation

UI = f(state), diffing, keys, useState/useEffect et leurs pièges : le cœur de React tel qu'on vous le demandera en entretien front — avec les réponses modèles. UI = f(state), diffing, keys, useState/useEffect and their traps: the core of React exactly as you'll be asked about it in a front-end interview — with model answers.

L’essentiel

React repose sur une idée simple : l’UI est une fonction de l’état — UI = f(state). Vous ne manipulez jamais le DOM à la main (« ajoute ce <li>, change cette classe ») : vous décrivez à quoi l’interface doit ressembler pour un état donné, et React fait converger le DOM réel vers cette description. C’est le passage de l’impératif (jQuery) au déclaratif.

Pour y arriver, chaque composant retourne du JSX — du sucre syntaxique pour des appels React.createElement qui produisent des objets JavaScript légers : le Virtual DOM. À chaque changement d’état, React recalcule cet arbre d’objets, le compare au précédent (diffing) et n’applique au DOM réel que les mutations strictement nécessaires. Créer et comparer des objets JS est quasi gratuit ; manipuler le DOM réel est lent.

Les hooks (depuis React 16.8) sont l’API qui donne aux composants fonction un état (useState), des effets de bord (useEffect) et de la mémoïsation (useMemo, useCallback) — sans classes.

Comment ça marche

Une mise à jour se déroule en deux phases distinctes :

  1. Render : React appelle vos fonctions composants et obtient un nouveau Virtual DOM. Cette phase doit être pure (aucun effet de bord) : React peut l’interrompre, la rejouer ou la jeter — c’est pour détecter les impuretés que le StrictMode double les renders en dev.
  2. Commit : React compare l’ancien et le nouvel arbre (reconciliation) et applique la liste minimale de mutations au DOM réel, de façon synchrone. Puis le navigateur paint, puis les useEffect s’exécutent.
setState() ──▶ RENDER (pur, interruptible)
               composants() → nouveau Virtual DOM
                    │
                    ▼  diff ancien vs nouveau
               (reconciliation) → mutations minimales
                    │
                    ▼
               COMMIT (synchrone) → DOM réel à jour
                    │
                    ▼
               paint navigateur → useEffect

Le diff exact de deux arbres coûterait O(n³) ; React le ramène à O(n) avec deux heuristiques :

  • Type différent au même endroit (<div> → <span>, ou CompA → CompB) : React démonte tout le sous-arbre (état local perdu) et le reconstruit.
  • Même type : React garde le nœud, ne met à jour que les props qui changent, puis descend récursivement.

Pour les listes, ces heuristiques ne suffisent pas : sans repère, React compare position par position. Les keys donnent une identité stable à chaque élément : React détecte qu’un élément a été déplacé, inséré ou supprimé, et déplace le nœud DOM au lieu de le réécrire — en conservant son état (valeur d’input, focus, animation).

🎤 En entretien — « explique le Virtual DOM » est LA question React. Réponse en trois temps : (1) une représentation de l’UI en objets JS, peu coûteuse à recréer ; (2) à chaque changement d’état, React diffe le nouvel arbre contre l’ancien ; (3) seules les différences sont appliquées au DOM réel, la partie lente. Concluez par la nuance qui fait la différence : le Virtual DOM n’est pas « plus rapide que le DOM », c’est un modèle déclaratif à coût acceptable.

Concepts clés à maîtriser

HookRôlePiège classique
useStateÉtat local, déclenche le re-renderMuter l’objet au lieu d’en créer un nouveau
useEffectEffet de bord après le commitDeps manquantes (valeurs périmées) ou absentes (boucle infinie)
useMemoMémoïser un calcul coûteuxLe mettre partout « par réflexe » : la mémoïsation a un coût
useCallbackRéférence de fonction stableInutile si l’enfant n’est pas React.memo
useRefValeur mutable sans re-renderLire/écrire .current pendant le render
useContextLire une valeur partagéeTous les consommateurs re-rendent à chaque changement

Règles des hooks : appelés uniquement au niveau supérieur du composant (jamais dans un if, une boucle ou une fonction imbriquée), et uniquement depuis un composant React ou un hook custom. Raison technique : React associe chaque useState/useEffect à sa valeur par ordre d’appel. Un hook conditionnel décale la liste et tout se mélange.

useEffect synchronise le composant avec un système extérieur (fetch, WebSocket, timer, DOM manuel). L’anatomie complète :

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // s'exécute APRÈS le commit, quand `query` change
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then((r) => r.json())
      .then(setResults)
      .catch((e) => {
        if (e.name !== "AbortError") console.error(e);
      });

    // cleanup : exécuté AVANT la prochaine exécution
    // de l'effet, et au démontage du composant
    return () => controller.abort(); // annule la requête obsolète
  }, [query]); // deps : ne relance l'effet que si query change

  return (
    <ul>
      {results.map((r) => (
        <li key={r.id}>{r.name}</li> // key stable : l'id, pas l'index
      ))}
    </ul>
  );
}

useMemo/useCallback sont utiles dans deux cas : (1) un calcul réellement coûteux, (2) la stabilité de référence conditionne autre chose — un enfant React.memo, les deps d’un autre hook. Ailleurs, c’est du bruit : mesurer avant d’optimiser (React DevTools Profiler).

Où placer l’état ? L’échelle à monter dans l’ordre :

  • Lifting state up : deux frères partagent un état → on le remonte dans le parent commun, passé en props. Couvre 80 % des cas.
  • Context : une valeur vraiment globale et peu changeante (thème, utilisateur connecté, langue) → évite le prop drilling. Attention : chaque changement re-rend tous les consommateurs.
  • State managers (Redux Toolkit, Zustand) ou état serveur (TanStack Query) : quand l’état global devient complexe ou vient majoritairement du serveur. En entretien de stage, savoir situer ces outils suffit.

💡 Réflexe à montrer — avant d’ajouter un useEffect, demandez-vous s’il est nécessaire : une valeur dérivée de l’état se calcule pendant le render (au besoin dans un useMemo), pas dans un effet qui fait un setState. La page « You Might Not Need an Effect » de la doc officielle est un excellent filtre.

En entretien

« Explique-moi le Virtual DOM. » — Une représentation légère de l’UI en objets JavaScript. À chaque changement d’état, React reconstruit cet arbre (peu coûteux), le compare à la version précédente (diffing) et n’applique au DOM réel — la partie lente — que les différences. Bénéfice : un modèle déclaratif UI = f(state) sans payer un re-render complet du DOM. Nuance à placer : ce n’est pas intrinsèquement plus rapide que du DOM manuel optimisé, c’est un compromis productivité/performance.

« Comment React décide-t-il quoi mettre à jour ? » — Par la reconciliation, un diff en O(n) basé sur deux heuristiques : type différent à une position donnée → sous-arbre démonté et recréé (état local perdu) ; même type → mise à jour des props modifiées puis descente récursive. Dans les listes, les keys donnent l’identité stable qui permet de détecter déplacements, insertions et suppressions.

« Pourquoi ne pas utiliser l’index comme key ? » — Parce que l’index identifie la position, pas l’élément. Insérez un élément en tête : tous les index se décalent, React croit que chaque élément a changé de contenu et associe l’état local et DOM (valeur d’input, focus, case cochée) au mauvais élément. Key correcte : un id stable issu des données. L’index n’est tolérable que pour une liste statique, jamais triée, filtrée ni modifiée.

« À quoi sert le tableau de dépendances de useEffect ? » — Il dit à React quand relancer l’effet : à chaque render si absent, au montage seulement si [], quand une valeur listée change sinon. Toute valeur réactive lue dans l’effet doit y figurer, sous peine de stale closure (valeurs périmées capturées). Le cleanup retourné s’exécute avant chaque relance et au démontage — c’est là qu’on annule fetchs, timers et subscriptions.

« Quand useMemo et useCallback sont-ils utiles ? » — useMemo mémorise le résultat d’un calcul, useCallback la référence d’une fonction. Utiles si le calcul est coûteux ou si la stabilité de référence conditionne un enfant React.memo ou les deps d’un autre hook. Par défaut, ne pas en mettre : chaque mémoïsation coûte de la mémoire et une comparaison de deps, et c’est au Profiler de justifier l’optimisation.

Pièges & idées reçues

⚠️ key=index, le piège n°1 — sur une liste triable, filtrable ou éditable, key={index} mélange l’état des lignes : la valeur tapée dans l’input de la ligne 2 se retrouve dans la ligne 3 après une suppression, parce que React accroche l’état à la key, pas au contenu. C’est LE piège que les recruteurs font expliquer, souvent sur un bout de code à corriger.

  • Muter le state : items.push(x); setItems(items) ne re-rend pas — même référence, et React compare avec Object.is. Toujours créer un nouvel objet/tableau : setItems([...items, x]).
  • useEffect sans deps qui fait un setState → l’effet se relance à chaque render, le setState re-rend, boucle infinie. Symptôme classique : l’API appelée en rafale.
  • setState n’est pas synchrone : la variable garde l’ancienne valeur jusqu’au prochain render, et les mises à jour sont batchées. Pour dépendre de la valeur précédente : setCount(c => c + 1).
  • « Le Virtual DOM rend React plus rapide que le DOM » — non, c’est une couche en plus. Il rend le déclaratif viable en évitant les réécritures inutiles ; du DOM manuel finement optimisé sera toujours plus rapide, mais inmaintenable à l’échelle.

Pour aller plus loin

The essentials

React rests on one simple idea: the UI is a function of state — UI = f(state). You never manipulate the DOM by hand (“add this <li>, change this class”): you describe what the interface should look like for a given state, and React makes the real DOM converge to that description. It’s the shift from imperative (jQuery) to declarative.

To get there, every component returns JSX — syntactic sugar for React.createElement calls that produce lightweight JavaScript objects: the Virtual DOM. On every state change, React recomputes this object tree, compares it to the previous one (diffing) and applies to the real DOM only the strictly necessary mutations. Creating and comparing JS objects is nearly free; manipulating the real DOM is slow.

Hooks (since React 16.8) are the API that gives function components state (useState), side effects (useEffect) and memoization (useMemo, useCallback) — without classes.

How it works

An update happens in two distinct phases:

  1. Render: React calls your component functions and gets a new Virtual DOM. This phase must be pure (no side effects): React may interrupt it, replay it or throw it away — detecting impurities is why StrictMode double-renders in dev.
  2. Commit: React compares the old and new trees (reconciliation) and applies the minimal list of mutations to the real DOM, synchronously. Then the browser paints, then the useEffects run.
setState() ──▶ RENDER (pure, interruptible)
               components() → new Virtual DOM
                    │
                    ▼  diff old vs new
               (reconciliation) → minimal mutations
                    │
                    ▼
               COMMIT (synchronous) → real DOM updated
                    │
                    ▼
               browser paint → useEffect

An exact diff of two trees would cost O(n³); React brings it down to O(n) with two heuristics:

  • Different type at the same position (<div> → <span>, or CompA → CompB): React unmounts the whole subtree (local state lost) and rebuilds it.
  • Same type: React keeps the node, updates only the changed props, then recurses down.

For lists, these heuristics aren’t enough: without a marker, React compares position by position. Keys give each element a stable identity: React detects that an element was moved, inserted or removed, and moves the DOM node instead of rewriting it — preserving its state (input value, focus, animation).

🎤 In an interview — “explain the Virtual DOM” is THE React question. Answer in three steps: (1) a representation of the UI as JS objects, cheap to recreate; (2) on every state change, React diffs the new tree against the old one; (3) only the differences are applied to the real DOM, the slow part. Close with the nuance that sets you apart: the Virtual DOM isn’t “faster than the DOM” — it’s a declarative model at an acceptable cost.

Key concepts to master

HookRoleClassic trap
useStateLocal state, triggers the re-renderMutating the object instead of creating a new one
useEffectSide effect after commitMissing deps (stale values) or no deps array (infinite loop)
useMemoMemoize an expensive computationSprinkling it everywhere “by reflex”: memoization has a cost
useCallbackStable function referenceUseless if the child isn’t React.memo
useRefMutable value without re-renderReading/writing .current during render
useContextRead a shared valueEvery consumer re-renders on each change

Rules of hooks: called only at the top level of the component (never in an if, a loop or a nested function), and only from a React component or a custom hook. Technical reason: React matches each useState/useEffect to its value by call order. A conditional hook shifts the list and everything gets mixed up.

useEffect synchronizes the component with an external system (fetch, WebSocket, timer, manual DOM). The full anatomy:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // runs AFTER the commit, whenever `query` changes
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then((r) => r.json())
      .then(setResults)
      .catch((e) => {
        if (e.name !== "AbortError") console.error(e);
      });

    // cleanup: executed BEFORE the next run of the
    // effect, and when the component unmounts
    return () => controller.abort(); // cancels the stale request
  }, [query]); // deps: only re-run the effect if query changes

  return (
    <ul>
      {results.map((r) => (
        <li key={r.id}>{r.name}</li> // stable key: the id, not the index
      ))}
    </ul>
  );
}

useMemo/useCallback are useful in two cases: (1) a genuinely expensive computation, (2) reference stability gates something else — a React.memo child, another hook’s deps. Anywhere else it’s noise: measure before optimizing (React DevTools Profiler).

Where should state live? Climb the ladder in order:

  • Lifting state up: two siblings share a piece of state → move it into the common parent, passed down as props. Covers 80% of cases.
  • Context: a truly global, rarely changing value (theme, logged-in user, language) → avoids prop drilling. Careful: every change re-renders all consumers.
  • State managers (Redux Toolkit, Zustand) or server state (TanStack Query): when global state gets complex or mostly comes from the server. For an internship interview, knowing where these tools fit is enough.

💡 Reflex to show — before adding a useEffect, ask whether it’s needed at all: a value derived from state is computed during render (in a useMemo if needed), not in an effect that calls setState. The official docs page “You Might Not Need an Effect” is an excellent filter.

In an interview

“Explain the Virtual DOM to me.” — A lightweight representation of the UI as JavaScript objects. On every state change, React rebuilds this tree (cheap), compares it to the previous version (diffing) and applies to the real DOM — the slow part — only the differences. Benefit: a declarative UI = f(state) model without paying for a full DOM re-render. Nuance to land: it isn’t inherently faster than hand-optimized DOM code, it’s a productivity/performance trade-off.

“How does React decide what to update?” — Through reconciliation, an O(n) diff based on two heuristics: different type at a given position → subtree unmounted and recreated (local state lost); same type → changed props updated, then recurse down. In lists, keys provide the stable identity that lets React detect moves, insertions and deletions.

“Why shouldn’t you use the index as a key?” — Because the index identifies the position, not the element. Insert an element at the top: every index shifts, React thinks each element changed content and attaches local and DOM state (input value, focus, checked box) to the wrong element. Correct key: a stable id from the data. The index is only tolerable for a static list — never sorted, filtered or modified.

“What is useEffect’s dependency array for?” — It tells React when to re-run the effect: on every render if absent, only on mount if [], whenever a listed value changes otherwise. Every reactive value read inside the effect must be listed, or you get a stale closure (captured outdated values). The returned cleanup runs before each re-run and on unmount — that’s where you cancel fetches, timers and subscriptions.

“When are useMemo and useCallback useful?” — useMemo memoizes a computation’s result, useCallback a function’s reference. Useful when the computation is expensive or when reference stability gates a React.memo child or another hook’s deps. Default to not using them: each memoization costs memory and a deps comparison, and the Profiler should justify the optimization.

Pitfalls & misconceptions

⚠️ key=index, trap #1 — on a sortable, filterable or editable list, key={index} mixes up row state: the value typed in row 2’s input ends up in row 3 after a deletion, because React attaches state to the key, not the content. This is THE trap interviewers make you explain, often on a code snippet to fix.

  • Mutating state: items.push(x); setItems(items) doesn’t re-render — same reference, and React compares with Object.is. Always create a new object/array: setItems([...items, x]).
  • useEffect without a deps array calling setState → the effect re-runs on every render, the setState re-renders, infinite loop. Classic symptom: the API hammered with requests.
  • setState is not synchronous: the variable keeps its old value until the next render, and updates are batched. To depend on the previous value: setCount(c => c + 1).
  • “The Virtual DOM makes React faster than the DOM” — no, it’s an extra layer. It makes the declarative model viable by avoiding useless rewrites; finely hand-optimized DOM code will always be faster, but unmaintainable at scale.

Going further

S'entraîner sur ce sujet → Practice this topic →