Module 0 · Lire du code

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.

💡 Le concept

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.

arborescence du projet RendezVous
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.

🔗 Pont — du fichier au projet

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.

AxeLa question qu'il poseLe 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.

🧭 Bon à savoir

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.

📅 Dans RendezVous

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.

✍️ Exercice de lecture

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.

🧠 Quiz éclair

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.

  1. Chaque champ a-t-il un label associé ? Pas un texte à côté : un <label> avec un htmlFor qui correspond à l'id du champ — la formule « Le champ étiqueté ». Sans ce lien, le lecteur d'écran annonce « zone de texte » et rien d'autre.
  2. Y a-t-il des <div> cliquables ? Cherche onClick sur 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.
  3. 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.
  4. Les images ont-elles un alt utile ? alt="photo" ne vaut pas mieux que rien. Une image décorative doit avoir un alt="" explicitement vide, pour dire « ignore-moi ».
  5. L'ordre des titres est-il continu ? Un h1, puis des h2, puis des h3. Sauter au h4 parce 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.
  6. Le focus est-il visible ? Cherche un outline: none sans 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.
un extrait qui échoue à quatre des six points
<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.

🧠 Quiz éclair

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.

📅 Dans RendezVous

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 ».

⚠️ Piège fréquent

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.

🔗 Pont — la règle de trois, côté mobile

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.

🧠 Quiz éclair

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 width ni height, 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 onChange gè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.

✍️ Exercice de lecture

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é.

app/rendez-vous/page.tsx
'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.

🧠 Quiz éclair

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 formule

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.

NiveauCe que ça veut direExemples 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.

✍️ Exercice de lecture

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.

⚠️ Piège fréquent

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.

🧠 Quiz éclair

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.

  1. Chaque fetch vérifie-t-il res.ok ? Cherche fetch(, regarde la ligne suivante — le défaut le plus souvent relevé.
  2. 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.
  3. Chaque bouton de soumission est-il désactivé pendant l'envoi ? Une ligne, et la double soumission disparaît.
  4. Chaque champ a-t-il un <label htmlFor> ? Et le bon type (email, tel, date), qui change le clavier sur mobile.
  5. Y a-t-il un onClick sur autre chose qu'un bouton ou un lien ? Si oui, convertis-le en <button>.
  6. Chaque useState est-il vraiment de l'état ? « Puis-je le recalculer à partir des autres ? » Si oui, supprime-le.
  7. Y a-t-il un useEffect sans effet de bord ? Un effet qui ne fait que setState depuis un autre état est presque toujours à supprimer.
  8. 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.
  9. Les images ont-elles alt, width et height ? Trois attributs, un décalage de mise en page en moins.
📅 Dans RendezVous

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.

🔗 Pont — revoir les autres, se revoir soi-même

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.

🎤 En entrevue

« 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é.

🧭 Pourquoi cette réponse marche

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.

À retenir

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.

🗂️ L'aide-mémoire
Ce que signale un useEffect sans 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 une const recalculée au rendu
Ce qu'il faut vérifier sur un htmlFor ou un aria-describedby
que l'id visé 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 useState dans 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 fetch dispersé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 setState a 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.mock disent 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