Lire et critiquer du code frontend
C'est la compétence d'entrevue cachée. Beaucoup de candidats savent écrire du code ; très peu savent lire celui des autres, dire ce qui cloche, et le dire avec assez de tact pour que l'auteur ait envie de corriger. Ce module est aussi la synthèse pratique du guide.
Critiquer du code, ce n'est pas avoir un avis. C'est faire une observation vérifiable, en déduire un impact concret, puis proposer une piste. Un avis (« j'aime pas ») ne fait bouger personne ; une observation avec impact (« si l'API renvoie un 500, l'écran reste bloqué sur le spinner ») fait bouger tout le monde. Toute la difficulté tient dans ce déplacement : du goût vers le fait.
Lire avant de juger : les cinq gestes
Dans le guide sur Halterofit, le module La méthode pour lire un fichier inconnu t'a donné une démarche en six temps : imports, exports, signatures, survol des noms, tracer un chemin, nommer le pattern. Elle vaut à l'échelle d'un fichier ; ce module la prolonge à celle d'un projet entier, puis de « comprendre » à « critiquer » — la troisième échelle, celle qu'on te demandera en entrevue.
Un projet, contrairement à un fichier, n'a ni « haut » ni « bas » : tu ne peux pas commencer par les imports, il y a deux cents fichiers. Les cinq gestes qui suivent te donnent un autre point d'entrée. Fais-les dans l'ordre ; à la fin tu peux parler du projet intelligemment, même en n'ayant lu que dix pour cent du code.
Geste 1 — Repérer le point d'entrée et le routeur
Sur le web classique, la porte d'entrée est un index.html et un
main.tsx qui monte React dans une div. En Next.js App Router, c'est le
dossier app/ lui-même : le routeur est le système de fichiers
(module « Routing : segments, layouts, navigation »). Ton premier geste est donc de regarder
l'arborescence — pas le code, l'arborescence.
app/
layout.tsx ← coque commune (en-tête, pied de page, polices)
page.tsx ← la route "/"
services/
page.tsx ← la route "/services"
[slug]/page.tsx ← "/services/nettoyage" — segment dynamique
rendez-vous/
page.tsx ← le formulaire de réservation
api/
rendez-vous/route.ts ← le Route Handler qui reçoit le POST
components/ ← composants réutilisés entre plusieurs pages
lib/ ← code sans interface : validation, accès données
Trente secondes d'arborescence t'apprennent plus qu'une heure de lecture de code : les pages du produit, l'existence d'une API interne, et l'endroit où vit la logique sans interface.
Elle répond à des questions que tu n'as pas eu besoin de poser. Combien d'écrans ? Quatre. Un
backend maison ? Oui. La logique est-elle séparée de l'affichage ? Il y a un
lib/, donc l'auteur a au moins tenté. S'il n'existait pas, ce serait
déjà une observation : « toute la logique vit dans les composants ». L'absence d'un dossier
est une information au même titre que sa présence : apprends à lire les trous.
Geste 2 — Suivre UNE fonctionnalité de bout en bout
L'équivalent, à l'échelle du projet, du « tracer un chemin » du guide Halterofit. Tu choisis une seule fonctionnalité — ici « réserver un rendez-vous » — et tu la suis sans dévier : le bouton → le composant du formulaire → l'état qui garde les valeurs → la fonction de soumission → l'appel réseau → le Route Handler → ce qui revient → ce que l'écran affiche. La tentation, sous stress, est de survoler dix fonctionnalités pour « avoir une vue d'ensemble » : piège, dix demi-compréhensions dont aucune ne permet de dire quoi que ce soit de solide. Une fonctionnalité comprise en entier t'apprend le style de l'équipe — comment ils nomment, gèrent les erreurs, où ils mettent les types — et ce style se répète partout. Tu as lu un chemin ; tu as compris la ville.
Geste 3 — Repérer où vit l'état
Une application, c'est de l'état plus des fonctions qui le transforment. Cherche les
useState, useReducer, contextes, caches de données — et note surtout
à quelle hauteur ils se trouvent. Un useState dans le composant
qui l'utilise seul, c'est sain ; le même remonté tout en haut et redescendu par cinq niveaux
de props, c'est un signal.
Geste 4 — Repérer où vivent les appels réseau
Cherche fetch, les appels de bibliothèque de données, les fonctions
async. La question n'est pas « combien ? » mais
« regroupés ou éparpillés ? ». Un projet où chaque composant fait son propre
fetch ne pourra jamais changer sa façon de parler au serveur : trente endroits à
toucher. Un projet où tout passe par quelques fonctions dans lib/ ajoute un
en-tête d'authentification en une ligne. En Next.js, demande-toi aussi de quel côté
ils s'exécutent — la distinction du module « Server et Client Components », reprise plus bas.
Geste 5 — Lire les tests
Les tests sont la déclaration d'intention de l'équipe : ce qu'elle considère assez important pour être protégé. Un projet qui teste la validation mais rien de l'affichage dit « nos bugs viennent des règles métier, pas du pixel ». Un projet sans aucun test dit autre chose — pas forcément de la négligence. Ne saute pas à la conclusion morale ; note le fait.
Le module « Tester le web » montre comment ils s'écrivent ; ici on s'en sert comme d'une documentation. Le nom d'un test (« affiche une erreur quand le email est invalide ») est souvent la meilleure phrase de spécification du projet. Lis les noms avant les corps — le « survol des noms » du guide Halterofit, appliqué au dossier de tests.
Un nom pareil, tu sais déjà à quoi il ressemble à l'intérieur : c'est Le test d'intégration qui parle, du module Tester le web : Vitest & Testing Library — des étiquettes, un libellé de bouton, un message visible, et pas un seul nom d'état interne. C'est exactement ce qui le rend lisible comme une spécification par quelqu'un qui découvre le projet. À l'inverse, un dossier de tests écrit en noms de fonctions internes (« calls handleChange with the right key ») ne t'apprend rien sur ce que fait l'application : note-le comme un fait, au même titre que l'absence de tests.
Les six temps du guide Halterofit et les cinq gestes d'ici sont la même méthode à deux échelles. « Les imports » devient « l'arborescence » (de quoi le projet dépend-il, comment est-il découpé ?). « Les exports » devient « les routes » (qu'offre le produit à ses utilisateurs ?). « Tracer un chemin » devient « suivre une fonctionnalité de bout en bout ». Et « nommer le pattern » devient « nommer l'architecture » : c'est un projet à rendu serveur, un projet tout-client, un projet mixte. Quand tu changeras de langage ou de framework, la méthode ne changera pas — seulement le nom des cases.
La grille de lecture en sept axes
Une fois que tu as lu, il faut juger — et c'est là que la plupart des juniors se perdent : ils sentent que « quelque chose ne va pas » sans savoir formuler quoi. Le remède est mécanique : passe le code au filtre de sept axes précis, un à la fois. Sept passages rapides valent mieux qu'un seul passage flou. Cette grille est la carte du reste du module : chaque section qui suit détaille un axe.
| Axe | La question qu'il pose | Le module qui l'explique |
|---|---|---|
| 1. Correctness (la justesse) | Est-ce que ça fait ce que ça prétend faire, y compris dans les cas tordus (liste vide, valeur nulle, double clic) ? | transversal |
| 2. État & données | Où vit la vérité, et n'est-elle pas dupliquée ou recopiée ? | React web ↔ React Native, Formulaires 1 |
| 3. Accessibilité | Est-ce utilisable au clavier, au lecteur d'écran, sans distinguer les couleurs ? | HTML sémantique & accessibilité |
| 4. Réseau & erreurs | Que voit l'utilisateur quand le serveur est lent, en panne, ou refuse ? | HTTP & fetch, Formulaires 2 |
| 5. Structure & duplication | Le code est-il découpé selon ses raisons de changer ? | React web ↔ React Native |
| 6. Performance | Y a-t-il un coût mesuré, et où ? | Le navigateur, Rendu & data fetching |
| 7. Tests | Qu'est-ce qui est protégé, et qu'est-ce qui ne l'est pas ? | Tester le web |
L'axe 1, correctness, n'a pas de section à lui : il traverse tous les autres.
« Correct » ne veut pas dire « ça marche quand je clique normalement » mais « ça marche aussi
quand la liste est vide, quand la valeur est null, quand on clique deux fois,
quand la date est le 29 février ». Le réflexe : « quel est le cas où ça casse ? ».
La plupart des bugs réels sont des cas que personne n'a imaginés.
L'ordre des axes n'est pas arbitraire : il va du plus objectif au plus discutable. Un bug de correctness ne se négocie pas, une erreur d'accessibilité non plus — c'est une norme. En revanche, un découpage de composants ou une optimisation dépendent du contexte et des chiffres. Si tu manques de temps, commence par le haut de la grille : c'est là que tes remarques seront les plus incontestables.
Axe « état & données » : où la vérité se dédouble
L'axe le plus rentable en entrevue : les erreurs de juniors y sont les plus prévisibles et les plus faciles à nommer. Le principe qui gouverne tout : chaque information ne doit avoir qu'une seule source de vérité. Dès qu'une même donnée existe à deux endroits, tu as créé une occasion de divergence — et elle surviendra.
Signal 1 — L'état dupliqué
Le cas le plus pur : la même vérité stockée deux fois — la liste et son compteur. Deux
useState à mettre à jour ensemble, éternellement, sans que rien ne te rappelle à
l'ordre.
// ⚠️ La même vérité à deux endroits : la liste ET son nombre
const [slots, setCreneaux] = useState<Creneau[]>([]);
const [nombreCreneaux, setNombreCreneaux] = useState(0);
function ajouterCreneau(c: Creneau) {
setCreneaux([...slots, c]);
setNombreCreneaux(nombreCreneaux + 1); // à ne JAMAIS oublier, partout, pour toujours
}
Le jour où quelqu'un ajoutera une deuxième façon d'insérer un créneau et oubliera la deuxième ligne, le compteur affichera un chiffre faux — et rien ne plantera.
La correction tient en une ligne : slots.length. Le nombre n'est pas de
l'état, c'est une lecture de l'état. Ce qui nous mène directement au signal 2,
dont l'état dupliqué du signal 1 n'est qu'un cas particulier.
Signal 2 — L'état dérivable, stocké au lieu d'être calculé
Une information est dérivable si tu peux la recalculer à partir d'autre chose
que tu possèdes déjà. Le nombre d'éléments dérive de la liste ; la liste filtrée, de la liste
et du filtre ; « le formulaire est-il valide ? », des valeurs des champs. Rien de tout cela ne
mérite un useState.
// ⚠️ Version stockée : deux états à garder synchronisés
const [slots, setCreneaux] = useState<Creneau[]>([]);
const [creneauxLibres, setCreneauxLibres] = useState<Creneau[]>([]);
useEffect(() => {
setCreneauxLibres(slots.filter((c) => !c.reserve));
}, [slots]); // un effet dont le seul rôle est de recopier une donnée
// ✅ Version dérivée : une seule vérité, recalculée à chaque rendu
const creneauxLibres = slots.filter((c) => !c.reserve);
Un useEffect dont le seul travail est d'appeler un setState à
partir d'un autre état est presque toujours le signe d'un état dérivé mal placé. C'est un
des motifs les plus faciles à repérer d'un coup d'œil.
Trois raisons de préférer la version dérivée, à savoir énoncer à l'oral.
L'impossibilité d'incohérence : creneauxLibres ne peut pas être
en retard sur slots. Le nombre de rendus : deux pour la
version stockée, un seul pour la dérivée. La charge mentale : une variable de
moins. Quant au « coût du recalcul », il est indétectable ici — et s'il devenait détectable,
on le mesurerait avant d'agir.
Signal 3 — L'état placé trop haut, ou trop bas
Un état a une hauteur naturelle : le plus bas nœud commun à tous ceux qui en ont besoin. Plus haut, il fait re-rendre toute une branche et traverse des composants qui n'en ont que faire (le « perçage de props »). Plus bas, il devient impossible à partager et on en crée une copie ailleurs — retour au signal 1. Symptôme de « trop haut » : une prop qui traverse trois composants sans être utilisée. Symptôme de « trop bas » : un composant frère qui reçoit la même information par un autre chemin. Deux odeurs visuelles, repérables dans le JSX avant de comprendre le code.
Signal 4 — La mutation au lieu de la copie
Plus subtil, donc plus impressionnant à repérer. React décide de re-rendre en comparant les
références, pas le contenu. Si tu modifies un tableau en place puis
le repasses au setState, la référence est identique : React conclut « rien n'a
changé ». Le bug est vicieux parce que la donnée a changé en mémoire ; c'est l'écran
qui ment.
// ⚠️ Mutation : on modifie le tableau existant
function annuler(index: number) {
slots[index].reserve = false; // on touche l'objet EN PLACE
setCreneaux(slots); // même référence → React ne re-rend pas
}
// ✅ Copie : un nouveau tableau, avec un nouvel objet pour l'élément touché
function annuler(index: number) {
setCreneaux(slots.map((c, i) =>
i === index ? { ...c, reserve: false } : c
));
}
map produit un nouveau tableau, et l'objet décomposé produit un nouvel objet :
deux références neuves, donc deux comparaisons qui détectent le changement.
La vérité est l'objet values du formulaire, comme dans la formule « Un état
pour tout le formulaire » du module Formulaires 1. Tout le reste en dérive : les erreurs du
validateur pur (module Formulaires 2), l'activation du bouton de « aucune erreur et pas en
cours d'envoi », le résumé des valeurs elles-mêmes. Un
useState('formulaireValide') dans un projet comme celui-là, et tu tiens
ta première remarque de revue.
Voici le haut d'un composant de la page « Mes rendez-vous ». Sans rien réécrire, réponds : combien de ces cinq déclarations méritent vraiment d'être de l'état ? Et pour chacune des autres, d'où dérive-t-elle ?
const [rendezVous, setRendezVous] = useState<RendezVous[]>([]);
const [recherche, setRecherche] = useState('');
const [resultats, setResultats] = useState<RendezVous[]>([]);
const [aDesResultats, setADesResultats] = useState(false);
const [chargement, setChargement] = useState(true);
useEffect(() => {
const filtres = rendezVous.filter((r) => r.service.includes(recherche));
setResultats(filtres);
setADesResultats(filtres.length > 0);
}, [rendezVous, recherche]);
Voir le corrigé
Trois sur cinq sont du vrai état, et encore, à un détail près.
rendezVous vient du serveur, rien ne permet de le recalculer.
recherche vient de la frappe, elle n'est déductible de rien.
chargement aussi : c'est une information sur le déroulement de
l'appel réseau, pas sur les données.
resultats est dérivable : c'est exactement
rendezVous.filter(...) avec recherche. Le stocker oblige à un
useEffect, donc à un rendu de plus, et ouvre une fenêtre d'incohérence —
entre le rendu où recherche change et celui où l'effet tourne, les résultats
affichés correspondent à l'ancienne recherche. aDesResultats est
dérivable du dérivé : resultats.length > 0, le signal 1
dans sa forme la plus pure.
La lecture qui résume tout : ce useEffect ne fait aucun effet de bord — ni
réseau, ni DOM, ni minuteur. Il recopie de l'état vers de l'état.
Un effet sans effet est une odeur.
Tu vois un useEffect qui ne fait qu'un setState à partir d'un autre état. Que soupçonnes-tu ?
Un état dérivé stocké au lieu d'être calculé. Un effet sans effet de bord (pas de réseau, pas de DOM, pas de minuteur) est une odeur : la valeur peut presque toujours devenir une simple const recalculée au rendu. Bénéfice : une incohérence devient impossible, un rendu est économisé, et il y a une variable de moins à suivre.
Axe « accessibilité » : la passe de soixante secondes
L'accessibilité a mauvaise réputation chez les juniors : ça a l'air immense et plein de sigles. C'est pourtant l'axe où l'on trouve le plus de défauts certains en le moins de temps, grâce à une poignée de vérifications binaires. Le module « HTML sémantique & accessibilité » les explique ; voici comment on les repère.
-
Chaque champ a-t-il un label associé ? Pas un texte à côté : un
<label>avec unhtmlForqui correspond à l'iddu champ — la formule « Le champ étiqueté ». Sans ce lien, le lecteur d'écran annonce « zone de texte » et rien d'autre. -
Y a-t-il des
<div>cliquables ? ChercheonClicksur autre chose qu'un<button>ou un<a>. Une div cliquable n'est pas atteignable au clavier, ne réagit ni à Entrée ni à Espace, et n'est annoncée comme rien. C'est le défaut d'accessibilité numéro un du web. -
L'erreur est-elle signalée autrement que par la couleur ? Une bordure
rouge seule est invisible pour une partie non négligeable de la population, et pour
n'importe qui en plein soleil. Il faut un texte, relié au champ par
aria-describedby. -
Les images ont-elles un
altutile ?alt="photo"ne vaut pas mieux que rien. Une image décorative doit avoir unalt=""explicitement vide, pour dire « ignore-moi ». -
L'ordre des titres est-il continu ? Un
h1, puis desh2, puis desh3. Sauter auh4parce qu'il « avait la bonne taille » casse la navigation par titres. La taille se règle en CSS ; le niveau de titre est une information de structure. -
Le focus est-il visible ? Cherche un
outline: nonesans rien pour le remplacer : c'est le geste qui rend une page inutilisable au clavier — on tabule, quelque chose bouge quelque part, mais on ne sait pas où on est.
<div class="champ">
<span>Courriel</span> <!-- pas un label : aucun lien avec le champ -->
<input type="text" class="input--erreur" /> <!-- erreur signalée par la seule couleur -->
</div>
<img src="/clinique.jpg" /> <!-- pas d'alt du tout -->
<div class="bouton" onclick="reserver()"> <!-- div cliquable : mort au clavier -->
Réserver
</div>
Quatre défauts en huit lignes, tous repérables sans comprendre une seule ligne de logique métier. C'est ce qui rend cet axe si rentable quand on a peu de temps.
Nuance importante pour ta crédibilité : ces six points sont des défauts, pas des préférences. On débat du découpage d'un composant ; on ne débat pas du fait qu'un champ sans label est inutilisable au lecteur d'écran. Tu peux donc être plus affirmatif ici — le ton reste doux, le classement est ferme.
Ce champ a l'air correctement câblé : il y a un label, un id, un aria-describedby. Trois liens devraient tenir. Combien tiennent vraiment ?
<div className="field">
<label>Courriel</label>
<input id="email" type="email" aria-describedby="email-error" />
{error && <span className="field__error">{error}</span>}
</div>
Aucun. Et c'est ce qui rend le cas instructif : chaque pièce est là, aucune n'est branchée.
Le <label> n'a ni htmlFor ni imbrication du champ : il ne nomme rien, le champ reste anonyme pour un lecteur d'écran. L'aria-describedby pointe vers email-error, un id qui n'existe nulle part — l'attribut est là, la description est dans le vide. Et le message d'erreur, quand il apparaît, est un <span> muet : sans role="alert", rien ne signale son apparition à qui ne regarde pas l'écran.
La leçon de lecture : sur cet axe, ne te contente jamais de voir l'attribut, suis le lien. htmlFor doit valoir un id qui existe, aria-describedby aussi. Le correctif tient en trois ajouts : htmlFor="email", id="email-error" sur le message, et role="alert" dessus.
Axe « réseau & erreurs » : ce que voit l'utilisateur quand ça casse
Le grand angle mort du développement local : sur ta machine, l'API répond en trois millisecondes et ne tombe jamais en panne, donc le code ne décrit que le chemin heureux. En revue, tu cherches les chemins malheureux et tu demandes ce que l'utilisateur voit dans chacun.
Signal 1 — res.ok non vérifié
Le module « HTTP & fetch » t'a donné la formule « Le fetch honnête » et
sa ligne du milieu, if (!res.ok) throw .... fetch ne rejette que si
la requête n'a pas pu partir : un « 500 Internal Server Error » est, de son point de vue, une
réussite parfaite. Sans la vérification, le code enchaîne sur res.json() qui
tente de lire une page d'erreur HTML comme du JSON, et le crash apparaît trois fonctions plus
loin. En lecture : cherche fetch( ; si la ligne suivante contient
.json(), tu as trouvé.
Signal 2 — L'erreur avalée
Le catch qui fait un console.error et rien d'autre. Techniquement
l'erreur est « gérée » : le programme ne plante pas. Humainement elle est
avalée : l'utilisateur a cliqué sur « Réserver », il ne se passe rien, et il
ne saura jamais si son rendez-vous existe. La console, il n'a même pas de raison de savoir
qu'elle existe.
async function reserver() {
try {
const res = await fetch('/api/appointments', { method: 'POST', body });
const data = await res.json(); // ⚠️ res.ok jamais vérifié
setConfirmation(data);
} catch (e) {
console.error(e); // ⚠️ l'utilisateur ne voit STRICTEMENT rien
}
}
Deux défauts imbriqués : le premier fait que les erreurs HTTP n'arrivent même pas dans le
catch ; le second fait que celles qui y arrivent n'en ressortent jamais.
Signal 3 — Pas d'état de chargement
Entre le clic et la réponse il se passe du temps — peu chez toi, beaucoup sur un téléphone en
3G. Si l'interface ne change pas, l'utilisateur conclut que son clic n'a pas marché et
reclique. En lecture : cherche un état chargement/envoiEnCours
autour de chaque appel. S'il n'y en a pas, l'écran est muet pendant la latence.
Signal 4 — La double soumission possible
Conséquence directe du signal 3, et le plus grave de la liste : si le bouton n'est pas
désactivé pendant l'envoi, deux clics créent deux rendez-vous. Pas un défaut
d'ergonomie : une donnée fausse en base. Le remède tient en une ligne —
disabled={envoiEnCours} — ce qui rend son absence d'autant plus notable. C'est
pourquoi, dans Halterofit, les gestionnaires de navigation sont enveloppés dans un garde-fou
anti double tap : rendre l'action impossible pendant qu'elle est en cours.
Signal 5 — La validation seulement côté client
Le plus important à savoir formuler : il fait la différence entre « je connais React » et
« je comprends une application ». La validation dans le navigateur existe pour le
confort. Elle n'a aucune valeur de sécurité, parce que le
navigateur appartient à l'utilisateur : n'importe qui peut envoyer une requête directement à
/api/appointments, et ton validateur ne sera jamais exécuté. « Route Handlers
& Server Actions » montre où rejouer la vérification — et si la validation est un
validateur pur, la rejouer côté serveur coûte une ligne d'import. C'est l'argument
principal en faveur de ce patron.
Les cinq signaux forment une seule question : « que voit le patient si le serveur de la
clinique est en panne au moment où il clique ? ». Un bon code répond : un message
clair, un bouton réactivé, ses valeurs toujours dans les champs pour réessayer. Un code
moyen répond : un spinner éternel. Cette question remplace une checklist de dix points, et
elle sonne juste à l'oral parce qu'elle parle d'un humain, pas d'un try/catch.
Axe « structure & duplication » : la règle de trois
La règle de trois : une fois, c'est un cas ; deux fois, une coïncidence ; trois fois, un patron. Elle ne dit pas « ne te répète jamais » mais « attends la troisième occurrence avant de factoriser ». Règle de patience : à deux occurrences, tu ignores lesquelles de leurs différences sont accidentelles ; la troisième te montre l'axe de variation réel. Abstraire à deux, c'est deviner ; à trois, c'est constater.
La question à se poser avant d'extraire
Ressembler ne suffit pas. La seule question qui compte : « ces deux morceaux changeront-ils pour la MÊME raison ? ». Deux cartes qui se ressemblent — l'une pour un service, l'autre pour un membre de l'équipe — évolueront différemment : le marketing changera l'affichage des prix, les RH ajouteront les spécialités. Les fusionner crée un composant tiraillé entre deux maîtres, qui accumulera les props booléennes jusqu'à devenir illisible. À l'inverse, deux boutons de soumission changeront toujours ensemble — là, l'extraction est juste. La ressemblance visuelle est un indice ; la ressemblance de destin est la preuve.
Le composant qui fait cinq choses
L'autre grande odeur, repérable sans lire le code : la longueur du composant et le
nombre de sujets qu'il touche. Un PageRendezVous qui contient l'appel
réseau, l'état du formulaire, la validation, la mise en forme des dates et le rendu de trois
sections fait cinq métiers : aucun n'est réutilisable, aucun n'est testable isolément, et
chaque modification risque d'en casser un autre.
Le découpage utile ne se fait pas « par taille » mais par raison de changer :
la mise en forme des dates part dans lib/ parce qu'elle change quand le format
change ; la validation, quand les règles changent. Reste dans le composant ce qui change quand
l'écran change — et ce critère te donne la phrase à dire en revue, bien plus
convaincante que « c'est trop long ».
L'abstraction prématurée est le piège inverse, et plus coûteux que la duplication qu'il prétend éviter. Un composant générique créé « au cas où » sur la base de deux occurrences finit paramétré par six props, dont trois booléennes qui s'excluent mutuellement ; personne n'ose y toucher.
Du code dupliqué se supprime facilement : tu vois les deux copies, tu choisis. Une mauvaise abstraction se propage — chaque nouvel usage la rend plus difficile à défaire. Retiens la formule : duplicate is far cheaper than the wrong abstraction. En revue, un composant avec cinq props booléennes est une abstraction prématurée qui a mal vieilli.
Cette règle n'a rien de spécifiquement web. Dans Halterofit, c'est exactement le critère que tu appliques à tes composants React Native : une carte d'exercice affichée dans l'historique, dans le résumé de séance et dans les statistiques — trois occurrences, donc un patron, donc on extrait. Deux occurrences seulement, on attend. Et la question « changeront-elles pour la même raison ? » s'y pose identiquement : une carte d'exercice et une carte de plan se ressemblent à l'écran, mais l'une bougera quand tu changeras l'affichage des séries, l'autre quand tu changeras le catalogue. Même règle, mêmes arbitrages, deux plateformes. C'est un point que tu peux mentionner en entrevue : tu n'as pas appris ça « pour le web », tu l'as appris comme principe.
Tu ouvres ce composant partagé, utilisé à quatre endroits. Qu'est-ce qui s'est passé, et à quel moment ?
type CardProps = {
title: string;
subtitle?: string;
isService?: boolean;
isStaff?: boolean;
isCompact?: boolean;
onPress?: () => void;
};
Une abstraction prématurée, décidée à la deuxième occurrence. Regarde les trois booléennes : isService et isStaff s'excluent — une carte n'est pas les deux — mais le type autorise les huit combinaisons, dont { isService: true, isStaff: true } et « ni l'un ni l'autre ». TypeScript ne dira rien : on a demandé trois drapeaux indépendants, il vérifie trois drapeaux indépendants. Les états illégaux sont représentables, donc quelqu'un finira par les écrire.
Ce qui s'est passé : deux cartes se ressemblaient, on a fusionné, et comme elles changeaient pour des raisons différentes — le catalogue d'un côté, les fiches d'équipe de l'autre — chaque divergence a été absorbée par une props de plus. Le composant est maintenant tiraillé entre deux maîtres, et plus personne n'ose y toucher.
La question qui aurait tranché avant de fusionner : ces deux morceaux changeront-ils pour la même raison ? Non. Donc deux composants, quitte à dupliquer le balisage. Le remède aujourd'hui est le même à l'envers : séparer, et n'extraire que ce qui bouge vraiment ensemble.
Axe « performance », honnêtement
Une règle absolue avant tout : on ne parle jamais de performance sans mesure. Pas « ça doit être lent », pas « il faudrait mémoïser ». Un chiffre avant, un chiffre après, ou rien. C'est ce qui sépare l'ingénieur du superstitieux, et ce qu'un évaluateur cherche à entendre — la moitié des candidats récitent des optimisations sans avoir jamais ouvert un profileur.
La raison est arithmétique : les ordres de grandeur du navigateur ne sont pas intuitifs.
Filtrer un tableau de mille éléments prend une fraction de milliseconde ; charger une image de
trois mégaoctets prend deux secondes. Le junior optimise le filter et laisse
l'image. Le coût réel est presque toujours ailleurs qu'où on le croit.
Le réflexe : d'abord mesurer
Les outils de développement du navigateur (module « Le navigateur : DOM, événements, rendu ») ont trois onglets pour trois questions. Network : « qu'est-ce qui est téléchargé, et combien ça pèse ? ». Performance : « où le fil d'exécution passe-t-il son temps ? ». L'audit intégré : « comment ça se comporte sur un appareil modeste ? ».
Les vrais coupables, par fréquence
-
Les images non dimensionnées. Le premier coupable : sans
widthniheight, une image n'occupe aucune place tant qu'elle n'est pas chargée, puis pousse le contenu vers le bas — le décalage de mise en page, où l'utilisateur clique sur un bouton qui vient de bouger. Et une photo de trois mégaoctets dans un emplacement de trois cents pixels gaspille des secondes de réseau. - Les listes énormes non virtualisées. Cinq mille lignes rendues d'un coup créent cinq mille nœuds DOM que le navigateur calcule, dispose et peint : le défilement devient saccadé. La virtualisation ne rend que ce qui est visible — le raisonnement des listes performantes de React Native que tu utilises déjà dans Halterofit : ne pas construire ce que personne ne regarde.
-
Le travail lourd dans un gestionnaire d'événement. Le JavaScript du
navigateur tourne sur un seul fil. Une boucle de cinquante millisecondes dans un
onChangegèle l'interface : pas d'animation, pas de frappe, pas de clic. C'est le mécanisme du module « Le navigateur » — un seul fil, donc tout ce qui est long bloque tout le reste.
<!-- ⚠️ Aucune dimension : la page saute quand l'image arrive -->
<img src="/salle-attente.jpg" alt="Salle d'attente de la clinique">
<!-- ✅ Dimensions connues : le navigateur réserve la place à l'avance -->
<img src="/salle-attente.jpg" alt="Salle d'attente de la clinique"
width="800" height="450">
Le rapport coût/bénéfice de cette correction est imbattable : deux attributs contre un défaut visible par tous les visiteurs. C'est le genre de remarque qui passe toujours bien.
Sans profileur en entrevue, formule-la comme une hypothèse à vérifier, pas comme un verdict : « cette liste peut contenir plusieurs milliers de rendez-vous ; je regarderais le profil de défilement avant de conclure, mais c'est le premier endroit où je mesurerais ». Tu montres que tu connais le coupable probable et que tu refuses de conclure sans chiffre.
Les odeurs propres à Next.js
Les six axes précédents valent pour n'importe quel frontend. Next.js ajoute sa propre famille de défauts, liés à sa particularité fondamentale : le code peut s'exécuter à deux endroits, serveur ou navigateur, et la plupart des erreurs viennent d'une confusion sur lequel des deux. Voici les cinq plus fréquentes.
Odeur 1 — 'use client' posé trop haut
Le module « Server et Client Components » établit la règle : cette directive n'est pas un
interrupteur local, c'est une frontière — tout ce qui est importé dessous
devient client. La poser en tête de page parce qu'un petit bouton a besoin d'un
useState bascule la page entière dans le navigateur : on perd le rendu serveur,
on grossit le paquet JavaScript, on annule l'essentiel de ce que Next.js apporte.
// ⚠️ 'use client' en tête de page : TOUTE la page part au navigateur
'use client';
import { useState } from 'react';
export default function PageServices({ services }) {
const [ouvert, setOuvert] = useState(false); // le seul besoin d'interactivité
return (
<section>
{/* ... 200 lignes de contenu parfaitement statique ... */}
<button onClick={() => setOuvert(!ouvert)}>Détails</button>
</section>
);
}
Le remède tient en une phrase : extraire le bouton dans son propre composant client, et laisser la page en Server Component. On pousse la frontière vers le bas, aussi bas que possible.
Odeur 2 — Données chargées côté client sans raison
Un useEffect qui fait un fetch vers sa propre API interne, pour des
données disponibles au moment du rendu. Le module « Rendu & data fetching » montre le
détour : le serveur rend une page vide, le navigateur télécharge le JavaScript, l'exécute,
puis redemande au même serveur les données qu'il avait déjà sous la main. Trois allers-retours
au lieu de zéro, un écran de chargement inutile, un contenu invisible pour les moteurs de
recherche.
Le signal est net : un useEffect à dépendances vides dont le seul rôle est de
charger des données, dans un projet App Router. En Server Component, ça s'écrit
const services = await getServices(). Mais ne crie pas trop vite : si les données
dépendent d'une interaction (un filtre, une recherche), le chargement client est légitime. La
question reste : « ces données sont-elles connues au moment du rendu ? ».
Odeur 3 — Un secret exposé par NEXT_PUBLIC_
Le préfixe NEXT_PUBLIC_ dit « inline cette valeur dans le paquet JavaScript
envoyé au navigateur » — n'importe qui peut donc la lire en trois clics. Une clé publique
d'analytique : normal. Une clé d'API secrète, un jeton de service : c'est une
fuite, le défaut le plus grave qu'on puisse trouver dans un test technique.
NEXT_PUBLIC_SITE_URL=https://rendezvous.example.com ← normal, c'est public
NEXT_PUBLIC_API_SECRET=sk_live_9f3a... ← ⚠️ FUITE : lisible par tous
DATABASE_URL=postgres://... ← correct : reste sur le serveur
La règle de lecture : cherche NEXT_PUBLIC_ et demande-toi, pour chaque
variable, « est-ce que ça me dérangerait de l'afficher sur une affiche dans la rue ? ».
Odeur 4 — params non attendu
Dans les versions récentes de l'App Router, les params et
searchParams d'une page sont des promesses : il faut les await. Le
code qui écrit params.id directement fonctionne parfois par accident et casse
ailleurs, avec un message obscur. Le module « Routing : segments, layouts, navigation » pose
le détail ; en lecture, retiens la forme correcte.
// ✅ Les params sont une promesse : on les attend avant de les lire
export default async function PageService({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // ◀ le await est obligatoire
const service = await getService(slug);
return <ServiceDetail service={service} />;
}
Odeur 5 — Une Server Action sans vérification des droits
La plus dangereuse, et celle qui fait la meilleure impression quand tu la repères. Une Server Action ressemble à une fonction locale, mais comme l'explique le module « Route Handlers & Server Actions », c'est un point d'entrée HTTP public : Next.js lui crée une URL, et n'importe qui peut l'appeler sans passer par ton interface.
Donc cacher le bouton « Annuler ce rendez-vous » aux visiteurs non connectés ne protège rien : ça enlève le bouton, pas la porte. Toute action qui écrit ou supprime doit vérifier dans son propre corps l'authentification (« qui es-tu ? ») et l'autorisation (« ce rendez-vous est-il le tien ? »). Beaucoup de code fait la première et oublie la seconde, ce qui permet d'annuler le rendez-vous d'un autre en changeant un identifiant.
'use server';
export async function cancelAppointment(id: string) {
const session = await getSession();
if (!session) throw new Error('Non authentifié'); // qui es-tu ?
const rdv = await db.rendezVous.findById(id);
if (rdv.patientId !== session.userId) { // est-ce le TIEN ?
throw new Error('Non autorisé');
}
await db.rendezVous.delete(id);
}
Les deux gardes en tête de fonction sont exactement le pattern « clauses de garde » du guide Halterofit, appliqué à la sécurité. Leur absence est un défaut bloquant, jamais une suggestion.
Voici une page complète de RendezVous, telle qu'on pourrait te la tendre en entrevue avec la question « qu'en penses-tu ? ». Elle contient quatre défauts. Trouve-les, puis — c'est le vrai exercice — classe-les par gravité.
'use client';
import { useState, useEffect } from 'react';
export default function PageRendezVous() {
const [slots, setCreneaux] = useState([]);
const [creneauxLibres, setCreneauxLibres] = useState([]);
const [email, setCourriel] = useState('');
useEffect(() => {
async function charger() {
const res = await fetch('/api/slots');
const data = await res.json();
setCreneaux(data);
}
charger();
}, []);
useEffect(() => {
setCreneauxLibres(slots.filter((c) => !c.reserve));
}, [slots]);
return (
<form>
<span>Votre email</span>
<input value={email} onChange={(e) => setCourriel(e.target.value)} />
<ul>
{creneauxLibres.map((c) => <li key={c.id}>{c.heure}</li>)}
</ul>
<button>Réserver</button>
</form>
);
}
Voir le corrigé
Les quatre défauts, classés du plus grave au moins grave :
1. (Bloquant) res.ok n'est jamais vérifié. Sur un 500,
res.json() tente de lire une page d'erreur HTML et lève une exception
attrapée nulle part — le composant plante ou reste vide, sans message. Il manque aussi un
état de chargement et un état d'erreur. Formule « Le fetch honnête ».
2. (Bloquant) Le champ n'a pas de label associé. Un
<span> n'est pas un <label> : au lecteur d'écran, le
champ est anonyme. Il manque un id, un htmlFor et un
type="email". Défaut certain, pas une préférence — d'où le bloquant.
3. (Important) 'use client' est en tête de la page. Toute la
page bascule dans le navigateur alors que les créneaux pourraient être chargés sur le
serveur — le useEffect de chargement n'existe que parce que la page est
cliente ; en Server Component, ce serait await getCreneaux(). « Important »
et non « bloquant » parce que la page fonctionne, juste plus lentement.
4. (Suggestion) creneauxLibres est un état dérivé stocké.
Le second useEffect recopie de l'état vers de l'état ;
const creneauxLibres = slots.filter(...) le remplace et supprime un rendu.
Lisibilité, pas un bug visible — d'où la suggestion.
Le vrai enseignement n'est pas la liste, c'est le classement. Beaucoup de candidats trouvent les quatre et les récitent à plat, dans l'ordre du fichier. Celui qui dit « deux cassent l'expérience de vrais utilisateurs, deux coûtent en qualité ; je commence par les deux premières » montre qu'il sait prioriser — la compétence qu'on évalue réellement.
Cette Server Action vérifie bien la session. Quel scénario reste possible, et comment le classes-tu ?
'use server';
export async function cancelAppointment(id: string) {
const session = await getSession();
if (!session) throw new Error('Non authentifié');
await db.appointments.delete(id);
}
N'importe quel utilisateur connecté peut annuler le rendez-vous de n'importe qui d'autre, en changeant l'id. Le code répond à « qui es-tu ? » et jamais à « est-ce le tien ? ».
C'est la moitié manquante la plus fréquente : on écrit la garde d'authentification, qui est visible et qu'on sent obligatoire, et on oublie l'autorisation, qui dépend de la donnée. Et rien ne la rend visible — l'action a l'air d'une fonction locale appelée par un bouton que seul le propriétaire voit ; sauf qu'elle a une URL, et que l'id arrive du client.
Bloquant, sans discussion : ce n'est pas de la qualité de code, c'est une donnée d'autrui supprimable par un tiers. Le correctif est de deux lignes — relire le rendez-vous, comparer son propriétaire à session.userId, refuser sinon — et l'observation se formule sans agressivité : « je vois qu'on vérifie l'authentification ; est-ce que l'appartenance du rendez-vous est vérifiée ailleurs ? sinon un identifiant modifié suffirait à annuler celui d'un autre. »
Formuler une critique qui passe
Reste le plus difficile, et c'est un problème humain : le dire de façon à ce que l'auteur ait envie de corriger. Une critique juste mais mal formulée ne produit rien — ou pire, une posture défensive, et le défaut reste. En entrevue, on juge autant ta façon de dire que ta trouvaille. Heureusement, un patron de phrase mécanique rend presque n'importe quelle critique recevable. Trois temps, toujours les mêmes.
La critique en trois temps
Observation : « Ici, la validation n'existe que côté client. »
Impact : « Une requête envoyée directement à l'API créerait un
rendez-vous invalide. »
Proposition : « Est-ce qu'on pourrait rejouer le validateur dans le
Route Handler ? »
Le même patron sert en revue de code, en entrevue et pour se relire soi-même : un fait vérifiable, sa conséquence concrète, puis une piste posée en question plutôt qu'en verdict. Il décrit un code, jamais une personne — c'est précisément ce qui le rend recevable.
Sauter un seul des trois temps suffit à casser l'effet. L'observation doit être un fait vérifiable, pas une appréciation : « la validation n'existe que côté client » se constate en dix secondes, alors que « ce code est mal sécurisé » n'est vérifiable par personne, donc discutable par personne. Un fait crée un terrain commun ; un jugement crée deux camps.
L'impact est le temps que les juniors sautent, et le plus important. Sans lui, ta remarque ressemble à de la récitation — « il faut valider côté serveur » — et l'auteur n'a aucune raison de la prioriser. Avec lui, il comprend ce qui va lui arriver s'il ne fait rien : un utilisateur qui voit quelque chose de faux, une donnée corrompue en base, une heure perdue au prochain changement. Pas « c'est moins propre ».
La proposition en question laisse une porte ouverte, et pas par politesse : dans neuf cas sur dix tu ne connais pas tout le contexte — peut-être que la validation serveur existe dans un middleware que tu n'as pas lu. La forme interrogative te protège d'avoir l'air péremptoire à tort et transforme la revue en conversation.
Trois exemples, avant et après
❌ « C'est mal fait. »
✅ « Je vois que la validation n'existe que côté client ; est-ce que le
serveur revalide ? Sinon une requête directe pourrait créer un
rendez-vous invalide. »
❌ « Ton composant est trop gros. »
✅ « Ce composant fait le chargement, la validation et le rendu — donc il
change pour trois raisons différentes. On gagnerait à sortir le
validateur dans lib/ ; il deviendrait testable seul. Qu'en penses-tu ? »
❌ « Il faut mettre un useMemo là. »
✅ « Cette liste peut monter à plusieurs milliers d'éléments. Je n'ai pas
mesuré : est-ce qu'on a déjà regardé le profil de défilement ? Si c'est
lent, la virtualisation serait le premier levier. »
Remarque que les versions « après » sont plus longues. C'est normal et c'est le prix : une critique utile coûte deux phrases de plus qu'une critique inutile.
La hiérarchie : bloquant, important, suggestion
Dix remarques présentées au même niveau, c'est ingérable : l'auteur ne sait pas par où commencer et traite la plus facile plutôt que la plus grave. Étiqueter résout ça en un mot.
| Niveau | Ce que ça veut dire | Exemples typiques |
|---|---|---|
| Bloquant | Ne doit pas partir en production tel quel. Données fausses, faille, écran cassé, inutilisable pour une partie des utilisateurs. | Server Action sans contrôle des droits ; secret dans NEXT_PUBLIC_ ; double soumission qui crée deux rendez-vous ; champ sans label. |
| Important | À corriger, mais ça peut faire l'objet d'un suivi. Coûte cher à terme sans casser aujourd'hui. | Erreur avalée sans message ; 'use client' trop haut ; absence d'état de chargement. |
| Suggestion | À prendre ou à laisser. Lisibilité, cohérence, confort du prochain lecteur. | État dérivé stocké ; nommage ; extraction d'un composant à la troisième occurrence. |
Cet étiquetage change la nature de ta revue : tu ne déverses plus une liste, tu proposes un plan. Et il te protège — marquer une remarque comme « suggestion » autorise l'auteur à la refuser sans conflit. Beaucoup de tensions en revue viennent d'une ambiguïté sur le niveau d'exigence, pas du contenu.
Séparer le goût du défaut
C'est ce qui distingue un développeur mûr de quelqu'un qui vient d'apprendre les règles. Une bonne partie de ce qu'on est tenté de critiquer relève du goût personnel : fonctions fléchées ou déclarations, ordre des props, longueur idéale d'un fichier. Ce sont des conventions d'équipe, pas des vérités, et les asséner comme des défauts coûte cher : ton interlocuteur comprend que tu ne fais pas la différence, et doute alors de tes remarques sérieuses.
Le test pour trancher : « puis-je nommer un impact concret ? ». Si oui, c'est un défaut, dis-le. Si tu ne trouves qu'un « c'est plus habituel », c'est du goût : tais-toi ou annonce-le comme tel (« pure préférence, à ignorer : … »). Cette honnêteté fait un effet considérable en entrevue parce qu'elle est rare.
Deux collègues commentent la même ligne d'une demande de fusion sur RendezVous. Laquelle des deux fera bouger l'auteur, et pourquoi exactement ? Va plus loin que « la deuxième est plus gentille » : identifie ce qui manque à la première.
Commentaire A
─────────────
« Tu ne devrais jamais faire de fetch dans un useEffect avec Next.js.
C'est une mauvaise pratique. À corriger. »
Commentaire B
─────────────
« Ces créneaux sont connus au moment du rendu — ils ne dépendent d'aucune
interaction. En les chargeant côté client, on ajoute un aller-retour et
un écran de chargement que le visiteur pourrait ne pas avoir du tout.
Est-ce qu'il y a une raison de rester en Client Component ici, ou on
pourrait faire l'appel dans le Server Component ? »
Voir le corrigé
Le commentaire B. Mais la raison n'est pas le ton : c'est la structure. Passons les trois temps.
Observation. A n'en a pas — il énonce une règle générale (« jamais », « mauvaise pratique »), pas une observation sur ce code. B commence par un fait vérifiable : « ces créneaux sont connus au moment du rendu ».
Impact. A n'en donne aucun ; il demande de faire confiance à une autorité, ce qui n'aide pas à prioriser. B le chiffre en langage humain : un aller-retour de plus, un écran de chargement évitable. L'auteur peut décider si ça vaut le travail.
Proposition. A dit « à corriger » — un ordre sans direction. B propose une piste précise et laisse une porte ouverte : peut-être que l'auteur a une raison légitime que le relecteur ignore.
Le défaut caché de A : son « jamais » est factuellement faux. Charger des
données dans un useEffect est légitime quand elles dépendent d'une
interaction. En absolutisant une règle, A se met en position d'avoir tort — et le jour où
l'auteur trouvera le contre-exemple, plus aucune de ses remarques ne sera prise au
sérieux. C'est le piège de la « best practice » récitée : elle a l'air solide, elle est
fragile.
Asséner « best practice » sans expliquer l'impact concret. « Il faut valider côté serveur », « il faut mémoïser » — ces phrases sonnent récitées, et un évaluateur l'entend immédiatement : tu as retenu la conclusion sans le raisonnement. Le remède est mécanique : chaque fois que tu es sur le point de dire « il faut », ajoute « parce que sinon, concrètement… » et complète. Si tu n'y arrives pas, tu viens de découvrir que tu ne comprenais pas la règle.
Le piège jumeau : confondre préférence de style et vrai défaut. Traiter l'ordre des imports au même niveau qu'une faille d'autorisation dilue tes remarques graves dans le bruit. Une seule remarque juste et hiérarchisée vaut mieux que dix à plat.
Tu as trois remarques sur le même fichier : (a) les imports ne sont pas rangés dans le même ordre que dans les autres fichiers ; (b) le bouton d'envoi n'est pas désactivé pendant la requête ; (c) l'auteur écrit des fonctions fléchées là où tu écrirais des déclarations. Lesquelles écris-tu, et sous quelle étiquette ?
(b) seule, en bloquant. Applique le test : puis-je nommer un impact concret ? Pour (b), oui, et il est chiffrable — deux clics impatients créent deux rendez-vous en base, donc une donnée fausse et un patient qui recevra deux confirmations. Pour (c), non : aucune conséquence nommable, c'est du goût. Pour (a), non plus, sauf s'il existe une règle de lint ou une convention écrite dans le dépôt — auquel cas ce n'est plus ton goût, c'est la convention de l'équipe, et la remarque devient « le linter n'est pas passé », ce qui est vérifiable.
Le coût de les écrire toutes les trois à plat n'est pas la longueur : c'est que la seule qui compte se retrouve noyée au milieu de deux préférences, et que ton interlocuteur en conclut que tu ne fais pas la différence. À partir de là, il doutera aussi de tes remarques sérieuses. Si (c) te démange vraiment, annonce-la pour ce qu'elle est : « pure préférence, à ignorer : … ».
Se relire soi-même avant de rendre un test technique
Tout ce module s'applique au code des autres. Il s'applique encore mieux au tien, à un moment précis : les dix dernières minutes avant de rendre un test technique. Elles valent souvent plus que les trois heures précédentes, parce qu'elles corrigent exactement les défauts que l'évaluateur cherchera en premier.
Pourquoi ça marche ? Parce que l'évaluateur ne lit pas ton code comme toi : tu le lis avec ton intention en tête, lui n'a que la trace — le renversement décrit dans le module La méthode pour lire un fichier inconnu du guide Halterofit. Se relire, c'est simuler ce regard extérieur avec la grille en sept axes, comme si le code était celui d'un inconnu.
Les neuf points, en dix minutes
Ces points portent sur le code : les sept axes réduits à des vérifications binaires.
Tout ce qui relève de la conduite d'un test technique — ordre de travail, gestion du
temps, README, ce qu'on choisit de ne pas faire — appartient aux deux modules
Étude de cas : l'énoncé et les données et
Étude de cas : la solution disséquée.
-
Chaque
fetchvérifie-t-ilres.ok? Cherchefetch(, regarde la ligne suivante — le défaut le plus souvent relevé. - Chaque appel a-t-il un état de chargement et un état d'erreur affichés ? Pas seulement stockés : réellement rendus à l'écran.
- Chaque bouton de soumission est-il désactivé pendant l'envoi ? Une ligne, et la double soumission disparaît.
-
Chaque champ a-t-il un
<label htmlFor>? Et le bontype(email,tel,date), qui change le clavier sur mobile. -
Y a-t-il un
onClicksur autre chose qu'un bouton ou un lien ? Si oui, convertis-le en<button>. -
Chaque
useStateest-il vraiment de l'état ? « Puis-je le recalculer à partir des autres ? » Si oui, supprime-le. -
Y a-t-il un
useEffectsans effet de bord ? Un effet qui ne fait quesetStatedepuis un autre état est presque toujours à supprimer. -
La validation est-elle rejouée côté serveur ? À défaut de temps, un
// TODO: revalider côté serveur: un évaluateur préfère un manque assumé à un oubli. -
Les images ont-elles
alt,widthetheight? Trois attributs, un décalage de mise en page en moins.
Les deux modules suivants, Étude de cas : l'énoncé et
les données et Étude de cas : la solution
disséquée, font cet exercice de bout en
bout sur RendezVous : un énoncé réaliste, la construction, puis cette passe de relecture
avant de « rendre », README compris. Garde cette liste en tête en les lisant, et
le module L'entrevue frontend te montrera comment en parler
à l'oral.
La revue de code et la relecture de son propre test technique sont le même geste, avec le même outil. Dans les deux cas tu prends la grille en sept axes, tu la passes sur du code, et tu produis des remarques en trois temps. La seule différence est l'auteur — et c'est précisément là que se cache la difficulté : sur ton propre code, tu lis ce que tu voulais écrire, pas ce que tu as écrit. D'où deux astuces concrètes : laisse passer une pause avant de relire (même dix minutes), et relis dans un ordre différent de celui où tu as écrit — commence par le dernier fichier. Tu perds l'anesthésie de l'habitude, et les défauts redeviennent visibles. C'est aussi la raison pour laquelle relire le code des autres te rend meilleur sur le tien : tu entraînes le regard extérieur.
« Voilà un composant. Qu'est-ce que tu en penses ? »
« Si je lis bien, ça affiche les créneaux disponibles et ça permet d'en réserver un — dis-moi si je me trompe avant que je commente. »
« Je vais regarder ça sous quelques angles : la justesse, l'état, l'accessibilité, la gestion d'erreur. »
« Sur l'état : la liste des créneaux est copiée dans un useState alors
qu'elle arrive déjà en props — deux sources pour la même vérité, elles finiront par
diverger. Ça, c'est bloquant selon moi. Sur l'accessibilité : le message d'erreur n'est
rattaché à aucun champ, donc un lecteur d'écran ne l'annoncera jamais — important, mais
pas bloquant. Le nom data2, c'est une suggestion, rien de plus. »
« Et ce qui est bien : le découpage entre le formulaire et la liste est bien vu, je n'y toucherais pas. »
Et si tu ne trouves rien : « Rien ne me saute aux yeux. Qu'est-ce qui se passe si la liste est vide, ou si le serveur renvoie une erreur ? » — deux fois sur trois, tu viens de trouver le défaut qu'on avait planté.
Quatre mouvements, dans cet ordre : reformuler avant de juger (et se faire corriger tout de suite plutôt que de critiquer à côté), annoncer sa grille (une inspection au hasard devient une démarche), étiqueter la gravité de chaque remarque, et terminer par ce qui est bien. Ce dernier point n'est pas de la politesse : reconnaître ce qui fonctionne prouve qu'on sait distinguer, et c'est un signal fort sur la façon dont on collaborera. La question est volontairement ouverte — on veut voir une méthode, pas une liste de règles.
« Comment fais-tu une revue de code ? »
Réponds par une méthode, pas par une liste de choses que tu détestes. Quelque chose comme : « Je lis d'abord la description de la demande pour savoir ce que le code est censé faire — sans ça je ne peux pas juger si c'est correct. Ensuite je suis la fonctionnalité de bout en bout plutôt que de lire les fichiers dans l'ordre du diff. Puis je passe quelques axes : est-ce que ça fait ce que ça dit, y compris dans les cas tordus ; où vit l'état ; l'accessibilité des champs et des boutons ; ce que voit l'utilisateur quand le réseau échoue. »
Puis ajoute la partie humaine, qui est celle qu'on écoute vraiment : « je formule chaque remarque en trois temps — ce que j'observe, l'impact concret, une proposition en question. Et je marque le niveau : bloquant, important, ou simple suggestion. Je n'écris pas de remarque de goût sans dire que c'en est une. » Cette dernière phrase rassure énormément un futur collègue.
Termine sur l'humilité utile : « et j'approuve. Une revue qui ne se termine jamais bloque l'équipe ; si les points restants sont des suggestions, je les écris et je laisse passer. »
« Quel est le plus gros défaut de ton propre code ? »
Attention à ne pas la confondre avec sa cousine « qu'est-ce qui ne va pas dans ce code que tu viens de rendre ? », qui appelle une revue du livrable et que le module « L'entrevue frontend » traite en détail. Ici, on te demande une habitude de travail, pas un audit de fichier.
Question piège classique, dont les deux mauvaises réponses sont symétriques : le faux défaut déguisé en qualité (« je suis trop perfectionniste ») et l'aveu vague sans suite (« je fais des bugs »). Ce qu'on cherche, c'est : es-tu capable de te regarder lucidement et d'agir en conséquence ?
La structure gagnante est en trois parties : un défaut réel et précis, comment tu l'as découvert, ce que tu as mis en place. Par exemple : « Je code le chemin heureux d'abord, et je pense aux cas d'erreur trop tard. Je m'en suis rendu compte sur mon app de suivi d'entraînement : tout marchait chez moi, et le premier utilisateur sans réseau a vu un écran vide sans explication. Depuis, j'ai un réflexe fixe — dès que j'écris un appel réseau, j'écris l'état de chargement et l'état d'erreur dans la foulée, avant de passer à la suite. Ce n'est pas guéri, mais c'est cadré. »
Pourquoi ça marche : c'est vérifiable, c'est un vrai défaut de junior (donc crédible), et la partie « ce que j'ai mis en place » montre que tu apprends de tes erreurs sans qu'on ait à te le dire. Prépare une version de cette réponse tirée de ton propre projet — l'anecdote concrète est ce qui la rend mémorable.
Lis avant de juger : point d'entrée et routeur, une fonctionnalité de bout en bout, où vit l'état, où vivent les appels réseau, ce que disent les tests. Juge par axes, un à la fois : correctness, état & données, accessibilité, réseau & erreurs, structure & duplication, performance, tests — du plus objectif au plus discutable. Formule en trois temps : observation, impact, proposition en question, et étiquette la gravité (bloquant, important, suggestion). Ne parle jamais de performance sans mesure, et n'appelle « défaut » que ce dont tu peux nommer l'impact. Enfin, applique la même grille à ton propre code dans les dix minutes qui précèdent la remise.
Et ailleurs : presque rien ici n'est spécifique au web. « Lire avant de juger » marche sur n'importe quelle base de code ; la règle de trois, la question « changeront-ils pour la même raison ? », le refus d'optimiser sans mesure, la séparation entre goût et défaut sont des principes d'ingénierie, pas des techniques React. Et la critique en trois temps sort carrément de l'informatique : décrire un fait plutôt qu'une personne, chiffrer la conséquence, poser une question plutôt qu'un verdict fonctionne pour commenter un document, un plan, une décision d'équipe.
- Ce que signale un
useEffectsans effet de bord - un état dérivé stocké au lieu d'être calculé. Ni réseau, ni DOM, ni
minuteur, juste un
setState: la valeur devient uneconstrecalculée au rendu - Ce qu'il faut vérifier sur un
htmlForou unaria-describedby - que l'
idvisé existe. Ne te contente jamais de voir l'attribut, suis le lien — un attribut qui pointe dans le vide se lit exactement comme un attribut branché - Ce qu'on regarde en cherchant où vit l'état
- sa hauteur. Un
useStatedans le composant qui l'utilise seul est sain ; le même remonté puis redescendu par cinq niveaux de props est un signal - La question à poser sur les appels réseau
- non pas « combien ? » mais « regroupés ou
éparpillés ? » — trente
fetchdispersés, c'est trente endroits à toucher pour ajouter un en-tête - Ce que React compare pour décider de re-rendre
- des références, pas du contenu. Un tableau modifié en place puis
repassé au
setStatea la même référence : React conclut que rien n'a changé - Ce que dit le nombre de modules simulés dans un fichier de tests
- douze
vi.mockdisent un composant très couplé ; un fichier qui ne simule que le réseau dit une architecture saine. Chaque doublure marque une frontière - Le premier coupable de performance, par fréquence
- les images non dimensionnées — avant les rendus superflus, avant tout le reste. C'est aussi le moins glorieux à trouver, donc le plus souvent manqué
- Ce que
'use client'fait quand il est posé trop haut - c'est une frontière, pas un interrupteur local : tout ce qui est importé en dessous bascule côté client avec lui