Des réponses aux entretiens sur React et JavaScript qui démontrent une véritable profondeur.
Découvrez des réponses plus solides et plus nuancées aux questions fréquentes lors d’entretiens sur React et JavaScript, allant du Virtual DOM à la conception de systèmes, qui illustrent un jugement technique plus approfondi.
Introduction : Pourquoi la plupart des candidats ont tous le même ton
Si vous assistez à suffisamment d’entretiens sur React, vous remarquerez un schéma récurrent : les mêmes phrases toutes faites reviennent sans cesse. « Le Virtual DOM est plus rapide. » « useEffect sert aux effets secondaires. » « JavaScript s’exécute sur un seul thread. » Aucune de ces affirmations n’est fausse, mais ce sont des choses que n’importe qui pourrait réciter après avoir lu quelques articles de blog, et elles ne donnent aucune idée à l’interlocuteur sur la façon dont vous réfléchissez vraiment.
Ce qui distingue un candidat solide de celui qui se voit envoyer un e-mail de refus poli, ce n’est pas s’il connaît la définition théorique — c’est s’il peut aller plus en profondeur. Pouvez-vous relier un concept aux compromis qui le sous-tendent ? Pouvez-vous expliquer pourquoi une décision de conception a été prise, et non seulement ce qu’elle fait ? C’est le type de jugement que les interviewers cherchent réellement à évaluer.
Ce guide aborde les questions qui se posent réellement lors des entretiens pour des postes de niveau intermédiaire et avancé en développement frontend, accompagnées de réponses suffisamment détaillées pour inciter l’entreteneur à cesser de parcourir ses notes et à écouter vraiment.
Section 1 : Concepts fondamentaux de React
Question 1 : « Expliquez le Virtual DOM. Comment fonctionne-t-il ? »
La réponse banale : « Le Virtual DOM est une copie légère du vrai DOM. React compare les deux et ne met à jour que ce qui a changé, ce qui le rend plus rapide. »
La réponse plus approfondie : le Virtual DOM est une abstraction, mais le décrire uniquement comme « plus rapide » manque l’essentiel. L’avantage réel en termes de performance ne provient pas du Virtual DOM lui-même, mais de la logique de regroupement des mises à jour et de conciliation qui lui est associée.
React gère en interne deux arbres : celui qui est actuellement affiché à l’écran, et un arbre en cours de création représentant ce qui va être rendu. Lorsque l’état change, React ne se précipite pas pour modifier directement le DOM réel. Au lieu de cela, il construit le nouvel arbre Virtual DOM, effectue une comparaison pour déterminer l’ensemble minimal de modifications nécessaires, puis applique toutes ces modifications au DOM réel en une seule opération groupée. C’est précisément cette étape de regroupement qui évite des recalculs répétés du layout, souvent appelés problèmes de performance liés au layout.
Il y a aussi une nuance à mentionner : le Virtual DOM n’est pas gratuit. Son algorithme de comparaison est délibérément maintenu à une complexité O(n) en s’appuyant sur des heuristiques — par exemple, en supposant que des éléments de types différents produiront des sous-arbres entièrement distincts — plutôt qu’en effectuant une comparaison d’arbres complètement générale de complexité O(n³). Ce compromis permet de conserver des performances rapides dans les cas courants, mais cela signifie que certains scénarios, comme une liste très longue où un seul élément change, peuvent encore être coûteux en ressources. C’est précisément pour cette raison qu’il existe des outils tels que React.memo, useMemo et des bibliothèques de virtualisation de listes.
Il convient également de reconnaître que le Virtual DOM perd progressivement de son attrait en tant que point fort unique. Des compilateurs comme celui de Svelte évitent complètement l’étape du Virtual DOM, et React lui-même se dirige vers des fonctionnalités de rendu concurrentiel qui modifient le fonctionnement réel du processus de conciliation. Le Virtual DOM a résolu un problème spécifique vers 2013 ; comprendre pourquoi il a été introduit est plus important que de pouvoir réciter ses mécanismes.
Répondre de cette manière montre une conscience historique, une reconnaissance honnête des compromis à faire, ainsi qu’une connaissance de l’évolution du paysage frontend en général — vous ne répondez pas seulement à la question littérale, mais vous démontrez également que vous comprenez où cette idée s’inscrit dans le tableau global.
Question 2 : « Quelle est la différence entre useEffect, useLayoutEffect, et dans quels cas les utiliser ? »
La réponse oubliable : « useEffect s’exécute après le rendu. useLayoutEffect s’exécute avant que le navigateur ne dessine l’écran. Utilisez useLayoutEffect lorsque vous devez mesurer le DOM. »
La réponse plus solide : la distinction liée au moment d’exécution est de notoriété publique, mais c’est l’explication du pourquoi de cette différence qui distingue une réponse pertinente. useEffect s’exécute de manière asynchrone, après que le navigateur a déjà dessiné l’écran. useLayoutEffect, en revanche, s’exécute de manière synchrone, juste après que React a terminé le calcul des modifications du DOM, mais avant que le navigateur n’ait eu le temps de dessiner.
Cette différence signifie que useLayoutEffect bloque en réalité la mise à jour visuelle. Si vous y placez des calculs coûteux, l’utilisateur percevra un écran figé. C’est précisément pour cette raison que la documentation React recommande d’utiliser par défaut useEffect — bloquer inutilement le dessin est un piège de performance courant.
Néanmoins, il existe des raisons légitimes d’utiliser useLayoutEffect au-delà de la simple « mesure du DOM ». Un bon cas d’usage est d’éviter des clignotements visibles. Imaginez qu’il s’agisse de rendre une boîte à outils dont la position dépend des dimensions d’un élément cible : effectuer ce calcul de positionnement à l’intérieur de useEffect provoque un éclairage visible, où la boîte à outils apparaît brièvement au mauvais endroit avant de se placer correctement. Effectuer ce même calcul à l’intérieur de useLayoutEffect évite complètement cet éclairage, car il a lieu avant que le navigateur ne dessine quoi que ce soit.
Il existe un troisième hook dans cette famille que de nombreux développeurs négligent : useInsertionEffect. Son but est de permettre aux outils CSS-in-JS d’insérer des règles de style dans le document avant que les effets de mise en page ne lisent par erreur des informations de style obsolètes depuis le DOM. La plupart des développeurs n’auront jamais besoin de l’utiliser directement, mais le simple fait de savoir qu’il fait partie du cycle de vie des effets de React indique une meilleure compréhension de la manière dont React 18 gère globalement les effets.
Un intervieweur pourrait insister et demander ce qui se passe si l’on utilise useLayoutEffect lors du rendu côté serveur. La réponse est que React vous avertira, car il n’y a pas de DOM disponible pour effectuer des mesures sur le serveur. Ce hook ne s’exécute tout simplement pas pendant le SSR, donc toute logique dépendante du DOM doit soit être protégée par une condition réservée au client, soit être déplacée dans useEffect.
Question 3 : « Expliquez le comportement de rendu de React. Quand un composant est-il rérendu ? »
La réponse banale : « Un composant est rérendu chaque fois que son état ou ses props changent. »
La réponse plus précise : ce n’est que la surface des choses. La question intéressante est de savoir ce qui compte réellement comme un « changement » et ce que fait React une fois qu’il en détecte un.
Un composant est rérendu dans trois conditions :
- Son état local change, généralement via un setteur d’état
- Son composant parent est rérendu, que les props transmis aient effectivement changé ou non
- Une valeur de contexte qu’il consomme change
La réflexion clé concerne le deuxième point : React ne compare pas les props avant de décider s’il faut rérender un composant enfant. Par conception, si un parent est rendu, ses enfants le sont également. Comparer les props n’est pas sans coût en termes de calcul, et dans la plupart des cas réels, le composant enfant doit de toute façon être mis à jour ; donc omettre cette comparaison par défaut constitue un compromis raisonnable.
C’est précisément là que les développeurs recourent à React.memo, souvent de manière erronée. La mémorisation a elle-même un coût — React doit toujours effectuer une comparaison des props lors de chaque rendu. Si ces props sont des objets complexes, ou si le composant concerné est déjà peu gourmand en ressources lors de son rendu, l’envelopper dans React.memo peut en fait nuire aux performances plutôt que de les améliorer.
La véritable compétence réside dans la capacité à savoir quand l’optimisation est vraiment justifiée. Une règle pratique consiste à éviter de mettre en mémoire les résultats avant d’avoir mesuré un problème concret. Utilisez le Profilleur des outils de développement React pour identifier d’abord les véritables goulots d’étranglement, puis appliquez React.memo, useMemo ou useCallback de manière ciblée. Optimiser prématurément dans React signifie généralement travailler à l’encontre de la conception du framework plutôt qu’avec elle.
Le rendu concurrentiel introduit par React 18 ajoute une couche supplémentaire à ce processus. Les rendus peuvent désormais être interrompus, priorisés ou même annulés en cours d’exécution. Comprendre qu’un rendu ne se traduit pas toujours directement par une mise à jour synchrone du DOM est essentiel pour écrire du code qui fonctionne correctement dans un environnement de rendu concurrentiel.
Question 4 : Comment gérer la gestion de l’état dans une grande application React ?
useState pour tout ce qui est local.
- État UI local : valeurs de formulaire, interrupteurs, état d’ouverture d’un modal.
useStatesuffit dans ce cas. - État serveur : données récupérées depuis un backend. React Query ou SWR sont conçus pour cela, car ils gèrent déjà le cache, la déduplication, la récupération en arrière-plan et les mises à jour optimistes — des fonctionnalités pour lesquelles Redux n’a jamais été conçu.
Section 2 : Approfondissements sur JavaScript
Question 5 : Expliquez les closures en JavaScript. Donnez un exemple concret.
Une réponse de surface décrit une fermeture comme étant simplement une fonction qui conserve les variables de son scope d’encadrement.
Une réponse plus approfondie relie cela au scope lexical : lorsqu’une fonction est créée, elle capture des références aux variables qui l’entourent à ce moment-là, et elle conserve un accès à celles-ci même après que l’exécution soit passée en dehors de ce scope initial.
Ce qui distingue un bon candidat, c’est sa capacité à expliquer pourquoi ce comportement est important spécifiquement dans React. Prenons un schéma qui provoque fréquemment des erreurs :
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Always logs 0
setCount(count + 1); // Resets to 1 every time
}, 1000);
}, []); // Empty deps = closure over initial count
}
La variable count référencée à l’intérieur de setInterval reste bloquée sur la valeur qu’elle avait lors du rendu initial. Comme l’effet s’exécute une seule fois, grâce au tableau de dépendances vide, cette clôture n’est jamais mise à jour avec de nouvelles valeurs. Ajouter simplement count à la liste des dépendances n’est pas non plus la solution adéquate, car cela entraînerait la destruction et la reconstruction de l’intervalle à chaque mise à jour. La solution appropriée consiste à utiliser la forme d’actualisation fonctionnelle, setCount(c => c + 1), qui évite complètement de dépendre de cette clôture obsolète.
Les clôtures provoquent également des conflits avec les écouteurs d’événements à l’intérieur des hooks personnalisés. Chaque fois qu’un écouteur est attaché à l’intérieur de useEffect et qu’il lit des données d’état, une clôture est impliquée. Une technique courante pour un hook de type useEventListener consiste à conserver la fonction de traitement dans un ref, afin que l’écouteur puisse toujours accéder à la version la plus récente sans avoir besoin d’être réattaché.
Rien de tout cela ne fait des clôtures quelque chose à éviter — ce sont un mécanisme fondamental qu’il convient de maîtriser. Les modèles de modules, les variables privées, les fonctions de fabrication et le currying dépendent tous d’elles. La compétence essentielle consiste à reconnaître précisément quand une clôture est créée et à s’assurer qu’elle capture bien la valeur prévue.
Question 6 : Qu’est-ce que le cycle d’événements ? Expliquez les microtâches et les macrotâches.
Une réponse sommaire indique que le cycle d’événements gère les tâches asynchrones et que les microtâches s’exécutent avant les macrotâches.
Une réponse plus détaillée explique que JavaScript s’exécute sur un seul thread, et que le navigateur simule la concurrence grâce au cycle d’événements. Le code synchrone s’exécute sur la pile d’appels ; lorsqu’une opération asynchrone est rencontrée, elle est transmise à une API Web — setTimeout, fetch, événements DOM — et une fois ce travail terminé, sa fonction de rappel est placée dans une file d’attente.
La subtilité à souligner est qu’il n’y a pas une seule file d’attente. Les macro-tâches — setTimeout, setInterval, les opérations I/O — sont placées dans une file, tandis que les micro-tâches — Promise.then, queueMicrotask, MutationObserver — le sont dans une autre. Une fois la pile d’appels vide, le cycle d’événements vide toute la file des micro-tâches avant même de traiter une seule macro-tâche.
Cela crée un risque réel : les micro-tâches peuvent priver le reste du programme de ressources. Si de nouvelles micro-tâches continuent d’être enregistrées de manière récursive, les callbacks de setTimeout en attente n’auront jamais leur tour. Ce type de situation peut paralyser l’interface utilisateur lorsque des Promises sont enchaînées en boucle sans jamais restituer le contrôle au navigateur.
Cela est directement lié à la manière dont React regroupe les mises à jour d’état. Dans React 18, les mises à jour d’état sont regroupées automatiquement, quel que soit leur point de départ : à l’intérieur de setTimeout, dans une Promise ou dans un gestionnaire d’événement natif. Ce n’était pas le cas avant React 18, où les mises à jour déclenchées à l’intérieur de setTimeout étaient appliquées une par une au lieu d’être regroupées. Comprendre le cycle d’événements permet de voir pourquoi le regroupement automatique de React 18 est important : il s’articule avec la file d’attente des microtâches afin que toutes les mises à jour en attente soient exécutées ensemble avant le prochain rendu.
On vous demandera peut-être également de prédire la sortie d’un court extrait comme celui-ci :
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
La réponse attendue est : 1, 4, 3, 2 — les instructions synchrones s’exécutent en premier, suivies des microtâches telles que la fonction de rappel de Promise, et ce n’est qu’ensuite que s’exécute la macrotâche enregistrée par setTimeout.
Question 7 : « Expliquez this en JavaScript. En quoi diffère-t-il des autres langages ? »
La réponse sommaire : « this fait référence à l’objet qui a appelé la fonction. »
La réponse approfondie : « En JavaScript, this suit un encadrement dynamique plutôt qu’un encadrement lexical. La plupart des langages définissent self ou this au moment où la fonction est définie. JavaScript, quant à lui, le résout au moment de l’appel, en fonction de la manière dont la fonction est invoquée et non de son emplacement dans le code source. »
« Il existe un ordre de priorité pour quatre règles de liaison :
- Liaison par création :
new Foo()définitthissur l’instance nouvellement créée - Liaison explicite :
foo.call(obj),foo.apply(obj)oufoo.bind(obj)forcentthisà être l’objet que vous passez en paramètre
obj.foo() fait en sorte que this soit égal à objfoo() laisse this égal à undefined en mode strict, ou recourt à globalThis dans le cas contraire"">Les fonctions flèche brisent délibérément ce schéma — elles héritent this de manière lexique du contexte qui les entoure. C’est précisément pour cette raison que les développeurs utilisaient des fonctions flèche à l’intérieur des composants React basés sur des classes avant l’apparition des hooks : cela leur permettait d’éviter de devoir appeler .bind(this) à l’intérieur du constructeur."
« Le code React basé sur les hooks ne touche que rarement directement à this, puisque les composants ne sont plus des classes. Néanmoins, ce concept réapparaît lorsque l’on maintient des composants de classe anciens, qu’on intègre des bibliothèques tierces ou face à un intervieweur qui souhaite vérifier sa maîtrise des fondamentaux de JavaScript. Un piège courant dans la pratique est de passer une méthode d’objet en tant que callback — par exemple, de transmettre obj.handleClick à un écouteur d’événement — ce qui supprime son liaison implicite et fait en sorte que this pointe vers une destination inattendue. »
Question 8 : « Qu’est-ce que les Promises en JavaScript ? Expliquez async/await. »
La réponse sommaire : « Les Promises gèrent le travail asynchrone, et async/await n’est rien d’autre qu’un sucre syntaxique ajouté par-dessus elles. »
La réponse clé : « Une Promise représente une valeur qui n’existe pas encore mais qui sera finalement déterminée. Elle remplace les chaînes complexes de callbacks par une interface chainable et un moyen cohérent d’indiquer le succès ou l’échec. »
« Ce qui rend les Promises véritablement utiles, ce n’est pas leur syntaxe mais les garanties qu’elles offrent. Une fois qu’une Promise est résolue – que ce soit en état satisfait ou rejeté – ce résultat est fixé et ne peut jamais changer. C’est cette immuabilité qui les rend composables et faciles à comprendre. »
« Dire que async/await n’est « qu’un sucre ajouté » en sous-estime son importance : il redéfinit fondamentalement la manière d’écrire de la logique asynchrone, lui permettant de ressembler à du code synchrone et facilitant grandement sa lecture. Cela dit, il introduit quelques pièges auxquels il convient de faire attention : »
// This runs sequentially - 6 seconds total
async function sequential() {
const a = await fetch('/a'); // 3s
const b = await fetch('/b'); // 3s
}
// This runs in parallel - 3 seconds total
async function parallel() {
const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
}
« Une erreur fréquente chez les développeurs moins expérimentés consiste à placer await à l’intérieur d’une boucle, ce qui force involontairement les opérations à s’exécuter une après l’autre plutôt que en parallèle. La solution consiste à utiliser Promise.all chaque fois que les opérations ne dépendent pas les unes des autres, et à réserver une boucle for...of avec await aux cas où une séquence stricte est effectivement requise. »
« La gestion des erreurs est un autre domaine où des problèmes surviennent. En enveloppant une appel à await dans try/catch, on peut capturer son échec, mais omettre cet enveloppement peut entraîner l’effondrement d’un processus Node.js en cas d’échec non géré. Au frontend, en enveloppant les appels asynchrones dans des bornes d’erreur ou en utilisant une bibliothèque comme React Query qui gère les états d’erreur de manière déclarative, on évite ce mode de défaillance. »
Section 3 : Conception du système et architecture
Question 9 : « Concevez un éditeur de documents collaboratif en temps réel comme Google Docs. »
Cette question change complètement l’orientation de l’interview. Le recruteur ne cherche plus à évaluer vos connaissances basiques sur React — il veut voir comment vous raisonnez en matière d’architecture de système.
Une approche solide serait la suivante :
« Avant d’écrire ne serait-ce qu’une ligne de code, je définirais clairement les exigences :
- Combien de personnes vont éditer en même temps ? La conception pour 10 utilisateurs simultanés est très différente de celle prévue pour 10 000.
- Quelle latence est acceptable — un temps réel absolu, ou plutôt quelque chose de proche du quasi-temps réel ?
- L’application doit-elle fonctionner hors ligne ?
- Quelle stratégie de résolution des conflits allons-nous adopter ? »
« Du côté frontend : »
- Gestion de l’état : chaque client conserve sa propre copie locale du document. Les modifications sont d’abord appliquées de manière optimiste sur le client, puis envoyées au serveur, qui les transmet à tous les autres clients connectés.
- Transformation opérationnelle ou CRDT : c’est le mécanisme permettant de résoudre les modifications conflictuelles. La transformation opérationnelle était la technique originale de Google, mais elle dépend d’un serveur central pour arbitrer l’ordre des modifications. Les CRDT (Conflict-free Replicated Data Types) peuvent fonctionner en pair-à-pair, et des outils tels que Yjs en ont accru l’utilisation.
requestAnimationFrame, et débouncing des synchronisations réseau afin de ne pas envoyer de requête à chaque frappe."Au niveau de la synchronisation :
- WebSocket gère le transport en temps réel
- Server-Sent Events ou long-polling servent de solution de secours si la connexion WebSocket ne peut pas être établie
- Les données de présence — qui est en ligne et où se trouve son curseur — sont transmises via un canal léger distinct du contenu du document"
"La partie véritablement difficile de ce problème n’a rien à voir avec le rendu React — c’est le modèle de cohérence qui se trouve en dessous. Lorsque deux personnes tapent à exactement la même position du curseur au même instant, que se passe-t-il ? La réponse dépend entièrement du choix entre OT et CRDTs, et cette seule décision influence presque toutes les autres choix architecturaux qui suivent."
Question 10 : "Comment optimiser une application React qui charge et interagit lentement ?"
La réponse décevante : une liste de tactiques hétéroclites — mémoïsation, chargement différé, segmentation des bundles — sans aucun cadre conceptuel.
La réponse qui se distingue : « Je commencerais par mesurer plutôt que de deviner. Le Profiler des React DevTools et le panneau Performance des Chrome DevTools vous indiquent si le goulot d’étranglement réside dans le temps de chargement, le temps de rendu ou les deux — et il est inutile d’optimiser à l’aveugle. »
En ce qui concerne le chargement :
- Décomposition du code : La décomposition basée sur les routes via React.lazy et Suspense constitue la base, mais cela ne devrait pas s’arrêter là. Les composants lourds qui ne sont pas immédiatement visibles — modaux, contenu en dessous de la zone visible — méritent également leurs propres points de décomposition.
- Préchargement : Utilisez
<link rel="preload">pour les ressources essentielles, et associez React.lazy à des indications de préchargement pour les routes que l’utilisateur est susceptible de visiter ensuite.
import lodash from 'lodash' au lieu de import debounce from 'lodash/debounce' peut faire une différence de 100 KB dans votre bundle final.En ce qui concerne les interactions :
- Virtualisation : dès qu’une liste dépasse environ 50 éléments, optez pour react-window ou react-virtualized. Afficher 10 000 nœuds DOM en même temps ne sera jamais rapide, quel que soit le degré d’efficacité du reste de votre code.
- Une stratégie de mémorisation disciplinée : Évaluez d’abord, puis agissez. Enveloppez les calculs coûteux dans
useMemo, les fonctions de rappel coûteuses dansuseCallback, et les composants qui se rérendent inutilement dansReact.memo. Mémoriser tout par défaut sans évaluer l’impact réel ajoute généralement des surcoûts plutôt que de les réduire. - Colocalisation des états : Gardez les états aussi proches que possible du composant qui les utilise réellement. Lever les états vers un ancêtre commun simplement parce que cela semble plus ordonné entraîne des rérendus supplémentaires chaque fois que ces états changent.
- Séparation des contextes : Lorsqu’un seul contexte mélange des mises à jour fréquentes — comme la position de la souris — avec des mises à jour moins fréquentes — comme l’état d’authentification — divisez-le en deux. Sinon, chaque mouvement de la souris force un rérendu de tous les composants qui en dépendent, y compris ceux qui ne s’intéressent qu’à l’authentification.
Prestations perçues :
- Les écrans squelette plutôt que les indicateurs de chargement donnent l’impression qu’une interface est plus rapide, car le contenu semble s’afficher progressivement plutôt que d’apparaître d’un coup.
- L’hydratation progressive grâce aux limites
Suspensede React 18 permet à l’ contenu essentiel d’être chargé en premier, tandis que les sections secondaires le sont ensuite. - L’indicateur Interaction to Next Paint, un nouveau critère de Core Web Vital de Google, mérite d’être suivi. Il remplace First Input Delay car il mesure la réactivité tout au long du cycle de vie de la page et non seulement lors de la première interaction. L’objectif est que les gestionnaires d’événements s’exécutent en moins de 200 millisecondes.
Lectures complémentaires
- Défis de l’architecture backend qui compliquent le travail des équipes React priorisant l’interface — Explique cinq défauts de conception backend courants dans les projets basés sur React, allant de l’utilisation erronée du paradigme API aux déploiements fragiles, ainsi que les solutions architecturales pour assurer une fiabilité de niveau production.
- Gérer les états UI du monde réel avec le rendering conditionnel de React — Apprenez à créer des interfaces d’authentification, de rôle, de permission, d’état de chargement, d’erreur et d’état vide dans React en utilisant des modèles de rendering conditionnel pratiques.