useState & useEffect
Voici les deux outils que tu vas croiser dans presque chaque fichier de l'app.
useState donne à un composant une mémoire qui survit aux re-renders.
useEffect lui permet de se synchroniser avec le monde extérieur :
minuteurs, abonnements, réseau. Avec le modèle mental vu dans le module sur le modèle mental de
React, ces deux hooks vont passer du statut de « formule magique recopiée » à celui d'outils que
tu comprends vraiment. Ce module est long et bavard exprès : il vaut mieux relire deux fois la
théorie que recoller du code au hasard.
Qu'est-ce qu'un hook, au juste ?
Avant de parler de useState et useEffect en détail, il faut un mot sur la
famille à laquelle ils appartiennent : les hooks. Le mot anglais hook
signifie « crochet », mais le verbe to hook into something veut dire « se brancher sur,
s'accrocher à » quelque chose. C'est exactement l'idée. Un hook est une fonction spéciale qui
se branche sur les fonctionnalités internes de React : la mémoire, le cycle de rendu, le
contexte, les optimisations. Quand tu appelles useState, tu ne fais pas qu'appeler une
fonction utilitaire ordinaire — tu dis à React : « réserve-moi un emplacement de mémoire et gère-le
pour moi entre les rendus ».
Avant les hooks (React, vers 2018), pour qu'un composant ait de la mémoire ou des effets de bord, il
fallait l'écrire sous forme de classe, avec un vocabulaire compliqué
(this.state, this.setState, des méthodes de cycle de vie aux noms à
rallonge). Les hooks ont permis d'obtenir les mêmes super-pouvoirs depuis de simples fonctions, en
ajoutant juste un appel comme useState(0). C'est pour ça que tout le code moderne que tu
liras dans Halterofit est fait de fonctions, jamais de classes : les hooks ont rendu les classes
inutiles pour 99 % des cas.
Tous les hooks commencent par use : useState, useEffect,
useRef, useMemo, useContext… et tu peux même écrire les
tiens : useElapsedSeconds, qu'on lit plus bas, en est un. Ce préfixe n'est pas
décoratif : c'est une convention que les outils respectent. L'analyseur de React
(le linter) se sert du préfixe use pour vérifier que tu respectes les règles qui
suivent. Si tu nommes ta fonction getElapsedSeconds au lieu de
useElapsedSeconds, React n'a aucune idée que c'est un hook, et tu perds tous ces
garde-fous.
Les règles des hooks
Les hooks ne sont puissants que parce qu'ils obéissent à deux règles strictes. Elles paraissent arbitraires au début ; elles deviennent évidentes dès qu'on comprend pourquoi elles existent. Les voici :
-
Appelle les hooks seulement au niveau supérieur de ton composant. Jamais à
l'intérieur d'un
if, d'une boucle, d'untry/catch, ou d'une fonction imbriquée. Les appels de hooks doivent être « visibles » directement, en haut du corps de la fonction. - Appelle les hooks seulement depuis un composant React ou depuis un autre hook. Pas depuis une fonction JavaScript ordinaire, pas depuis un gestionnaire d'événement.
La conséquence la plus visible de la règle 1, c'est que l'ordre des appels de hooks doit
rester identique d'un rendu à l'autre. Si à un rendu tu appelles trois hooks, et qu'au rendu
suivant tu en sautes un parce qu'il était dans un if devenu faux, tout se décale.
Voici le secret que peu de gens expliquent : React n'identifie pas tes états par leur nom,
mais par leur ordre d'appel. Quand tu écris const [email, setEmail] = useState('')
puis const [password, setPassword] = useState(''), React ne « voit » pas les noms
email et password — ce sont juste des variables locales à toi. Lui tient
simplement une liste interne : « premier useState de ce composant », « deuxième
useState », etc. Au rendu suivant, il redonne les valeurs dans le même ordre.
Si tu mets un useState dans un if, l'ordre peut changer entre deux rendus,
et React te rend l'état du voisin : ton mot de passe se retrouve dans ton champ e-mail. D'où la
règle. Retiens cette phrase : React compte, il ne lit pas les noms.
// ❌ INTERDIT : un hook caché dans une condition.
function Mauvais({ connecte }) {
if (connecte) {
const [nom, setNom] = useState(''); // appelé seulement parfois → l'ordre saute
}
// ...
}
// ✅ CORRECT : le hook est toujours appelé, c'est SA VALEUR qu'on conditionne.
function Bon({ connecte }) {
const [nom, setNom] = useState(''); // toujours appelé, toujours en 1er
const affiche = connecte ? nom : 'invité';
// ...
}
La parade est toujours la même : appelle le hook inconditionnellement, puis mets la condition après, sur la valeur ou dans le JSX. Ne déplace jamais l'appel lui-même.
useState : se souvenir d'une valeur entre deux re-renders
Souviens-toi de ce qu'on a posé dans le module sur le modèle mental de React : à chaque re-render,
React rappelle ta fonction depuis le tout début. Une variable normale
(let x = 0) serait donc recréée et remise à zéro à chaque fois — elle ne se
« souviendrait » de rien. useState demande à React de garder la valeur de
côté, dans sa liste interne, et de te la redonner au prochain appel. C'est, littéralement,
la mémoire de ton composant entre deux affichages.
// useState retourne une PAIRE : [valeur actuelle, fonction pour la changer].
// 'Patrick' est la valeur INITIALE (utilisée seulement au tout premier render).
const [name, setName] = useState('Patrick');
// ▲ valeur ▲ "setter"
// Pour lire : on utilise `name`.
// Pour changer : on appelle `setName('Bob')` → React re-render avec name = 'Bob'.
Cette double-écriture [valeur, setValeur] s'appelle une déstructuration de tableau :
useState renvoie un tableau de deux éléments, et on les nomme à la volée. La convention
x / setX est universelle ; respecte-la, tout le monde la lit d'un coup d'œil.
Tu ne modifies jamais name directement (name = 'Bob' ne
marche pas). Tu appelles toujours le setter setName('Bob'). C'est lui qui dit à React
« la valeur a changé, planifie un re-render ». Sans setter, React ne sait pas qu'il doit redessiner
et l'écran reste figé, alors même que ta variable a l'air « bonne » dans le débogueur. L'état, en
React, n'est pas juste une boîte : c'est une boîte plus une notification.
L'état est un instantané figé (et les mises à jour sont groupées)
Voici l'idée la plus profonde de tout le module, et celle qui débloque le plus de bugs une fois comprise. Pendant un rendu donné, la valeur de l'état ne change pas. Elle est figée, comme une photo (un « instantané », un snapshot). Quand tu appelles un setter, tu ne modifies pas la valeur du rendu en cours : tu demandes à React de refaire un nouveau rendu, avec une nouvelle photo où la valeur sera à jour.
Le piège classique qui en découle :
const [count, setCount] = useState(0);
function handlePress() {
setCount(count + 1); // count vaut 0 ici → on demande "passe à 1"
setCount(count + 1); // count vaut TOUJOURS 0 ici → on redemande "passe à 1"
setCount(count + 1); // idem → "passe à 1"
// Résultat : count finit à 1, pas à 3 !
}
Pourquoi ? Parce que pendant tout l'exécution de handlePress, count reste
l'instantané du rendu courant : 0. Les trois lignes calculent donc trois fois
0 + 1. Surprenant la première fois, parfaitement logique une fois qu'on tient l'idée
d'instantané.
Les mises à jour sont asynchrones et groupées (batching)
Deuxième surprise, liée à la première : appeler un setter ne re-render pas immédiatement. React n'interrompt pas ta fonction au milieu pour redessiner. Il note ta demande, finit d'exécuter ton gestionnaire d'événement jusqu'au bout, puis regroupe toutes les demandes accumulées et fait un seul re-render à la fin. Ce regroupement s'appelle le batching (« mise en lot »). C'est une optimisation : si tu changes cinq états dans la même fonction, l'écran ne clignote pas cinq fois, il se redessine une fois avec les cinq nouvelles valeurs.
C'est aussi pour ça que, juste après setCount(5), si tu écris
console.log(count), tu verras encore l'ancienne valeur : le re-render — et donc la
nouvelle photo de count — n'a pas encore eu lieu. Ce n'est pas un bug, c'est le modèle.
Bien mettre à jour : forme fonctionnelle et initialisation paresseuse
La mise à jour fonctionnelle : setX(prev => ...)
Comment alors incrémenter trois fois pour de vrai ? En passant au setter une fonction plutôt qu'une valeur. React appelle cette fonction en lui donnant la valeur la plus à jour qu'il connaît (pas l'instantané figé), et utilise ce qu'elle retourne comme nouvelle valeur. On l'appelle la mise à jour fonctionnelle ou functional update.
function handlePress() {
setCount(prev => prev + 1); // React : "le plus à jour vaut 0" → 1
setCount(prev => prev + 1); // "le plus à jour vaut 1" → 2
setCount(prev => prev + 1); // "le plus à jour vaut 2" → 3
// Résultat : count finit bien à 3 ✅
}
prev n'est pas un mot magique : c'est juste le nom que tu donnes au paramètre. React le
remplit pour toi avec l'état courant de la file d'attente. La règle pratique : dès que ta
nouvelle valeur dépend de l'ancienne, utilise la forme fonctionnelle.
Tu vas voir useElapsedSeconds plus bas. Il appelle setElapsed(...) chaque
seconde. Ici le code passe une valeur recalculée plutôt qu'une forme fonctionnelle, parce que la
nouvelle valeur ne dépend pas de l'ancienne (elle est recalculée depuis l'horloge). C'est un bon
réflexe à entraîner en lisant : se demander « cette mise à jour dépend-elle de l'état précédent ? »
décide à elle seule s'il faut setX(valeur) ou setX(prev => ...).
L'initialisation paresseuse : useState(() => ...)
Dernier raffinement de useState. La valeur initiale n'est utilisée qu'au tout premier
rendu — ensuite, React l'ignore. Pourtant, si tu écris useState(calculLourd()),
calculLourd() est appelé à chaque rendu (la fonction est évaluée avant
d'entrer dans useState), même si le résultat est jeté. Gaspillage. La parade : passer une
fonction à useState. React ne l'appellera qu'une fois, au premier rendu.
// ❌ calculLourd() s'exécute à CHAQUE rendu (et le résultat est ignoré après le 1er)
const [data, setData] = useState(calculLourd());
// ✅ initialisation paresseuse : la fonction n'est appelée qu'au premier rendu
const [data, setData] = useState(() => calculLourd());
Subtil mais utile : lire des données d'un stockage local, parser un gros objet, générer une valeur
par défaut coûteuse… autant de cas où la forme paresseuse useState(() => ...) évite
un calcul répété pour rien.
useState dans ton vrai code : l'écran d'inscription
Passons au concret. Chaque champ du formulaire d'inscription a son propre useState.
C'est le motif le plus courant que tu rencontreras dans toute l'app :
// Un état par champ : la valeur tapée + de quoi la mettre à jour.
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false); // afficher/masquer le mot de passe
const [error, setError] = useState(''); // message d'erreur affiché
const [isLoading, setIsLoading] = useState(false); // requête en cours ?
Sept morceaux de mémoire indépendants, et — point crucial — appelés tous en haut du composant, dans le
même ordre à chaque rendu : la règle des hooks est respectée. Chaque frappe au clavier appelle un
setter (ex. setEmail(...)), ce qui re-render l'écran avec la nouvelle valeur dans le champ.
Note au passage la différence de nature entre ces états. email est une donnée
« contenu » (ce que l'utilisateur tape). isLoading et showPassword sont des
états « d'interface » : des booléens qui pilotent l'apparence (spinner affiché ? mot de passe en
clair ?). error est un état « message ». Voir l'état d'un écran, c'est voir la liste de
tout ce qui peut y bouger.
Et voici comment ces setters sont orchestrés au moment d'envoyer le formulaire :
const handleSignUp = async () => {
setError(''); // 1. on efface l'ancienne erreur
const nameErr = getDisplayNameError(displayName);
if (nameErr) { setError(nameErr); return; } // (les validateurs vus dans le module TypeScript)
const emailErr = getEmailError(email);
if (emailErr) { setError(emailErr); return; } // email invalide → on affiche et on arrête
// ... (mot de passe, confirmation : même schéma) ...
setIsLoading(true); // on passe en "chargement" (le bouton montre un spinner)
try {
await signUp(email, password, displayName); // appel réseau
router.replace({ pathname: '/(auth)/confirm-account', /* ... */ });
} catch (err) {
setError(/* message lisible */); // en cas d'échec : on affiche l'erreur
} finally {
setIsLoading(false); // dans tous les cas : on quitte le "chargement"
}
};
Remarque comme isLoading pilote l'interface : pendant la requête il vaut
true → le bouton affiche un spinner et les champs passent en editable={!isLoading}
(désactivés) ; à la fin, retour à false. L'état décrit l'écran, et changer l'état
redessine l'écran. Le finally garantit qu'on sort toujours du chargement, même
si l'appel échoue.
useEffect : se synchroniser avec un système extérieur
Beaucoup de débutants pensent useEffect comme « du code qui réagit à un événement ».
C'est la mauvaise image, et elle mène à du code bancal. Le bon modèle, celui de l'équipe React
elle-même, est : useEffect sert à SYNCHRONISER ton composant avec un système extérieur.
Un système extérieur, c'est tout ce qui vit en dehors de React : un minuteur du navigateur, une
connexion réseau, un abonnement à un store, le titre de l'onglet, une API du téléphone… Tu décris
l'état dans lequel ce système doit être en fonction de ton état React, et React se charge
de le garder synchronisé.
La nuance « synchroniser, pas réagir » est subtile mais elle change tout. Exemple : tu veux un
minuteur qui tourne tant que l'entraînement est actif. La mauvaise lecture (« quand
l'entraînement démarre, lance un minuteur ; quand il s'arrête, arrête-le ») te pousse à écrire deux
bouts de code séparés et à oublier des cas. La bonne lecture (« le minuteur doit être synchronisé avec
startEpochMs ») te donne un seul effet, avec son nettoyage, et React
gère démarrage, arrêt et redémarrage tout seul. C'est exactement ce que fait le hook du chrono qu'on
lit plus bas.
Quand tu n'as PAS besoin d'un effet
Le meilleur useEffect est souvent celui que tu n'écris pas. Avant d'en ajouter un,
pose-toi la question : est-ce que je peux calculer cette valeur directement pendant le rendu ?
Si oui, n'utilise pas d'effet — calcule-la, point. Une erreur très répandue consiste à stocker dans un
état une donnée qui pourrait être dérivée d'un autre état, puis à synchroniser les deux avec
un effet. C'est du travail en double et une source de bugs.
// ❌ INUTILE : un état + un effet pour ce qu'on peut juste calculer
const [firstName, setFirstName] = useState('Patrick');
const [lastName, setLastName] = useState('Patenaude');
const [fullName, setFullName] = useState('');
useEffect(() => { setFullName(firstName + ' ' + lastName); }, [firstName, lastName]);
// ✅ DÉRIVÉ pendant le rendu : pas d'état superflu, pas d'effet
const fullName = firstName + ' ' + lastName;
Si le calcul est vraiment lourd et que tu veux éviter de le refaire à chaque rendu, ce n'est
toujours pas un cas pour useEffect : c'est un cas pour useMemo,
qu'on détaille dans le module sur useContext, useMemo, useCallback et memo. La règle : on dérive
pendant le rendu, on mémoïse si c'est coûteux, et on réserve useEffect aux vrais systèmes
extérieurs.
Le timing exact : après le rendu, après le commit
Quand React rend ton composant, il fait deux choses dans l'ordre : (1) il calcule le JSX
(la phase de rendu, pure, sans effet de bord), puis (2) il applique le résultat à l'écran
(la phase de commit). useEffect s'exécute après ces deux phases, une fois
que l'écran est peint. C'est pour ça qu'on dit que les effets ne « ralentissent pas l'affichage » :
l'utilisateur voit d'abord l'écran, l'effet tourne ensuite.
Ce timing explique pourquoi on ne met jamais d'effet de bord directement dans le corps
du composant (pendant la phase de rendu) : le rendu doit rester pur et prévisible, comme on l'a martelé
dans le module sur le modèle mental de React. Lancer un minuteur ou un appel réseau pendant le rendu
casserait cette pureté. useEffect est précisément la porte de sortie propre vers le monde
extérieur, ouverte au bon moment.
L'anatomie en 3 parties
Tout useEffect bien écrit a trois parties. On va les voir sur un hook réel de ton app,
puis disséquer chacune en profondeur :
export function useElapsedSeconds(startEpochMs: number | null): number {
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
// ── PARTIE 1 : l'effet (ce qu'on met en place) ──
if (startEpochMs == null) return; // pas de minuteur si pas de départ
const tick = () =>
setElapsed(Math.max(0, Math.floor((Date.now() - startEpochMs) / 1000)));
const timeout = setTimeout(tick, 0); // une fois, presque tout de suite
const interval = setInterval(tick, 1000); // puis chaque seconde
// ── PARTIE 2 : le nettoyage (renvoyé sous forme de fonction) ──
return () => {
clearTimeout(timeout); // on arrête les minuteurs quand on s'en va
clearInterval(interval); // SINON : fuite mémoire + minuteurs en double
};
}, [startEpochMs]); // ── PARTIE 3 : les dépendances ──
return startEpochMs == null ? 0 : elapsed;
}
Partie 1 — l'effet : ce qu'on met en place
La fonction passée à useEffect est l'effet : le code qui établit la
synchronisation avec le système extérieur. Ici, le système extérieur, ce sont les minuteurs du
moteur JavaScript (setTimeout et setInterval). L'effet commence par
une garde : if (startEpochMs == null) return; — s'il n'y a pas d'heure de départ, il n'y a
rien à synchroniser, donc on ne crée aucun minuteur. Quitter tôt comme ça est parfaitement valide : un
effet a le droit de ne rien faire.
Ensuite, on définit tick : à chaque appel, il recalcule le nombre de secondes
écoulées depuis le départ, à partir de l'horloge réelle Date.now(), et met
elapsed à jour via son setter. On l'arme deux fois : un setTimeout(tick, 0)
pour afficher la bonne valeur immédiatement (utile si on reprend un entraînement déjà en cours), puis un
setInterval(tick, 1000) pour rafraîchir chaque seconde. Le commentaire d'en-tête du fichier
— celui que la méthode du module sur la lecture d'un fichier inconnu t'apprend à lire en premier —
souligne un choix malin : on dérive le temps depuis l'horloge à chaque tick au lieu
d'accumuler un compteur. Résultat : si le téléphone met l'app en arrière-plan et gèle le fil
JavaScript, le chrono restera juste au retour, car il se recale sur l'heure réelle plutôt que de
compter des ticks manqués.
Partie 2 — le nettoyage : ce qu'on défait
Si ton effet renvoie une fonction, React l'appelle la fonction de nettoyage (cleanup). React l'exécute dans deux situations : (a) juste avant de relancer l'effet (parce qu'une dépendance a changé), et (b) quand le composant est démonté (il disparaît de l'écran). Le principe : tout ce que l'effet a « branché » doit être « débranché » par le nettoyage. Tu as ouvert une connexion ? Ferme-la. Tu t'es abonné ? Désabonne-toi. Tu as armé un minuteur ? Désarme-le.
Ici, le nettoyage appelle clearTimeout et clearInterval pour stopper les deux
minuteurs. Sans lui, ce serait la catastrophe décrite dans le commentaire : à chaque changement de
startEpochMs, on créerait un nouvel interval sans arrêter l'ancien. Les
minuteurs s'accumuleraient (deux, trois, dix qui tournent en parallèle), chacun
appelant setElapsed, et le compteur se mettrait à sauter ou clignoter — sans parler de la
fuite mémoire, puisque ces minuteurs zombies retiennent en vie tout ce qu'ils touchent.
Le nettoyage n'est pas une politesse optionnelle ; c'est la moitié de l'effet.
Partie 3 — les dépendances : quand l'effet rejoue
Le deuxième argument de useEffect, ici [startEpochMs], est le
tableau de dépendances. Il répond à une seule question : quand faut-il relancer
l'effet ? React compare, d'un rendu à l'autre, chaque valeur du tableau à la précédente. Si au
moins une a changé, il lance le nettoyage de l'ancien effet, puis exécute l'effet à nouveau. Si rien n'a
changé, il laisse l'effet en place, intact.
| Tableau de dépendances | Comportement |
|---|---|
[startEpochMs] | Relance l'effet uniquement quand startEpochMs change. (Le cas de ton hook.) |
[] (vide) | L'effet ne tourne qu'une fois, après le premier rendu ; le nettoyage à la fin, au démontage. |
| aucun tableau | L'effet tourne à chaque re-render. Presque toujours une erreur. |
L'exhaustivité du tableau (et ce qui arrive si tu triches)
Règle d'or : le tableau doit contenir toutes les valeurs réactives que l'effet utilise
— toute variable, prop ou état lu à l'intérieur de l'effet et susceptible de changer. C'est ce qu'on
appelle l'exhaustivité des dépendances, et le linter de React te le rappelle à coups
d'avertissements. Pourquoi cette rigueur ? Parce qu'un effet doit refléter l'état le plus récent. S'il
lit startEpochMs mais que tu l'omets du tableau, l'effet ne se relancera pas quand
startEpochMs changera : il continuera de calculer avec l'ancienne valeur,
capturée la première fois. C'est le fameux bug de la « valeur périmée » (stale closure).
La tentation, quand un effet se relance « trop souvent », est de retirer des dépendances du tableau pour
le faire taire. Ne fais jamais ça : tu ne corriges pas le problème, tu le caches, et tu
fabriques une valeur périmée. La vraie solution est presque toujours de restructurer : sortir
du code de l'effet, transformer une valeur en mise à jour fonctionnelle (setX(prev => ...),
qui ne dépend plus de l'état lu), ou stabiliser une fonction avec useCallback (voir le
module sur useContext, useMemo, useCallback et memo). Le tableau de dépendances n'est pas un réglage de
confort : c'est une déclaration de vérité sur ce dont dépend ton effet.
L'avertissement « React Hook useEffect has a missing dependency » est ton ami, même quand il t'agace. Dans 95 % des cas, lui obéir révèle un vrai bug latent. Considère-le comme un relecteur attentif, pas comme un pinailleur. Le 5 % restant — où tu sais vraiment ce que tu fais — se gère par une restructuration propre, pas en désactivant la règle.
Les erreurs classiques de useEffect
Presque tous les bugs de useEffect tombent dans l'une de ces trois familles. Les
reconnaître à la lecture te fera gagner des heures.
1. La boucle infinie
Symptôme : l'app rame, chauffe, le compteur de rendus explose. Cause typique : l'effet met à jour un état qui figure dans ses propres dépendances. L'effet change l'état → l'état change → React relance l'effet → l'effet change encore l'état → … à l'infini.
// ❌ BOUCLE INFINIE : l'effet écrit `count`, et `count` est dans les deps.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // change count → relance l'effet → change count → ...
}, [count]);
La parade dépend de l'intention. Souvent, c'est qu'on n'avait pas besoin d'un effet du tout (valeur dérivable pendant le rendu). Parfois, il faut juste retirer la dépendance circulaire et utiliser une mise à jour fonctionnelle. Mais d'abord : pose-toi la question « cet effet a-t-il lieu d'être ? ».
2. Le nettoyage oublié (la fuite)
Symptôme : ralentissements progressifs, comportements en double, avertissements du type « tentative de
mise à jour d'un composant démonté ». Cause : l'effet branche quelque chose (minuteur, abonnement,
écouteur d'événement) mais ne renvoie pas de nettoyage pour le débrancher. À chaque relance ou à chaque
changement d'écran, les ressources s'empilent. C'est exactement la fuite que
useElapsedSeconds évite grâce à ses clearTimeout / clearInterval.
Règle mnémotechnique : tout ce qui s'ouvre dans un effet doit se fermer dans le nettoyage.
3. Les dépendances incomplètes (la valeur périmée)
Symptôme : « ça marchait au début mais ça ne se met plus à jour ». L'effet a capturé une valeur au premier rendu et ne l'a jamais rafraîchie, parce qu'on a oublié (ou retiré) une dépendance. L'effet lit une vieille photo. C'est insidieux parce qu'il n'y a ni plantage ni boucle — juste une valeur qui « se coince ». La parade : déclarer honnêtement toutes les dépendances, et restructurer si l'effet se relance alors trop souvent (voir la section sur l'exhaustivité plus haut).
Quand un useEffect se comporte bizarrement, regarde le tableau de dépendances en
premier, puis le nettoyage. Boucle infinie ? Tu mets sûrement à jour un état
présent dans les deps. Valeur figée ? Il manque une dépendance. Comportement en double ou fuite ? Il
manque un nettoyage. Ces trois questions résolvent la grande majorité des cas.
Un mot sur useRef, pour le contraste
Pour bien cerner useState, il aide d'en voir l'opposé. useRef est aussi une
boîte de mémoire stable qui survit aux re-renders — mais avec une différence capitale :
changer son contenu ne déclenche AUCUN re-render. C'est une boîte « silencieuse ». Tu y
accèdes via sa propriété .current.
// useState : changer la valeur → re-render (l'écran se met à jour)
const [count, setCount] = useState(0);
setCount(1); // → React redessine
// useRef : changer .current → AUCUN re-render (valeur retenue, écran inchangé)
const ref = useRef(0);
ref.current = 1; // → rien ne se redessine
Le critère de choix tient en une phrase : si une valeur doit apparaître à l'écran quand
elle change, c'est useState. Si tu as juste besoin de retenir quelque chose
sans que ça redessine quoi que ce soit, c'est useRef.
L'écran sign-up.tsx utilise justement trois useRef :
emailRef, passwordRef, confirmRef. Ce sont des références vers
les champs de saisie, pour pouvoir déplacer le focus au champ suivant quand on appuie
sur « suivant » du clavier (onSubmitEditing={() => emailRef.current?.focus()}).
Donner le focus n'a pas à redessiner l'écran : useRef est donc l'outil parfait. Voilà le
contraste en vrai dans ton app — useState pour ce qui s'affiche, useRef pour
la plomberie qui n'a pas à redessiner.
Concentre-toi sur la dernière ligne du hook du chrono :
return startEpochMs == null ? 0 : elapsed;
Questions : (1) Pourquoi retourner 0 directement quand
startEpochMs est null, au lieu de simplement retourner elapsed ?
(2) Cette ligne provoque-t-elle un re-render ? (3) Bonus : pourquoi le hook recalcule-t-il le temps
depuis Date.now() à chaque tick plutôt que de faire setElapsed(prev => prev + 1) ?
Voir le corrigé
(1) Sécurité et clarté. Si aucun entraînement n'est en cours
(startEpochMs == null), l'effet ne tourne pas (il sort tôt) et elapsed
pourrait encore contenir une vieille valeur d'une session précédente, car React garde l'état tant
que le composant vit. Forcer 0 garantit qu'« aucun chrono actif » affiche toujours
zéro, sans dépendre de cet état résiduel.
(2) Non. C'est juste la valeur retournée, calculée pendant le rendu
courant — un calcul, pas une mise à jour d'état. Les re-renders sont déclenchés ailleurs : par
setElapsed(...) (chaque seconde) et par un changement de startEpochMs
(qui relance l'effet). Calculer une valeur de retour ne déclenche jamais de re-render ; seuls les
setters le font.
(3) Parce que dériver depuis l'horloge réelle reste juste même si le fil
JavaScript a été gelé (app en arrière-plan) : au réveil, le prochain tick recale la valeur sur le
temps réellement écoulé. Un prev + 1 aurait, lui, « perdu » toutes les secondes
passées en arrière-plan, car il ne compte que les ticks qui ont vraiment eu lieu.
1. Pourquoi ne peut-on pas appeler un hook à l'intérieur d'un if ?
Parce que React identifie ses états par l'ordre d'appel des hooks, pas par leur nom. Un hook dans un if peut être appelé à un rendu et sauté au suivant : l'ordre se décale et React te rend la valeur d'un autre état. D'où la règle « toujours au niveau supérieur, jamais conditionnel ».
2. Pourquoi setCount(count + 1) appelé trois fois de suite n'aboutit-il qu'à +1 ?
Parce que count est un instantané figé du rendu courant : les trois appels lisent la même valeur (ex. 0) et calculent tous 0 + 1. Pour incrémenter trois fois, il faut la forme fonctionnelle setCount(prev => prev + 1), qui reçoit la valeur la plus à jour.
3. Quel est le bon modèle mental de useEffect ?
« Synchroniser le composant avec un système extérieur » (minuteur, réseau, abonnement…), pas « réagir à un événement ». On décrit l'état voulu du système en fonction de l'état React, et React maintient la synchro, y compris l'arrêt et le redémarrage via le nettoyage.
4. À quoi sert la fonction retournée par useEffect, et quand s'exécute-t-elle ?
C'est le nettoyage : défaire ce que l'effet a branché (arrêter un minuteur, se désabonner). React l'appelle avant de relancer l'effet (dépendance changée) et au démontage du composant. L'oublier provoque fuites et comportements en double.
5. Quelle est la différence entre useState et useRef ?
Les deux retiennent une valeur entre les rendus. Mais changer un useState (via son setter) déclenche un re-render ; changer un useRef (via .current) n'en déclenche aucun. useState pour ce qui s'affiche, useRef pour ce qu'on retient sans redessiner.
Un hook est une fonction qui se branche sur React ; elle commence par use
et s'appelle toujours au niveau supérieur, jamais dans un if (React compte l'ordre, il ne
lit pas les noms). useState = mémoire qui survit aux re-renders ; sa valeur est un
instantané figé du rendu, les mises à jour sont groupées (batching), et on utilise
setX(prev => ...) dès qu'on dépend de l'ancienne valeur. useEffect =
synchroniser avec un système extérieur, en 3 parties (effet, nettoyage, dépendances),
avec un tableau de dépendances exhaustif. useRef = boîte stable qui ne
redessine pas.
Mettre dans un état une valeur qu'on pourrait simplement calculer pendant le rendu,
puis « la maintenir à jour » avec un useEffect. C'est du travail en double et la porte
ouverte aux désynchronisations. Réflexe : avant d'écrire un effet, demande-toi si la valeur n'est pas
déjà dérivable de ce que tu as. Si elle l'est, dérive-la (et mémoïse avec useMemo si c'est
coûteux). Garde useEffect pour les vrais systèmes extérieurs.
La distinction « état (ce qui change et redessine) » vs « effet de bord (interaction avec le monde extérieur, à nettoyer après usage) » est universelle en programmation d'interfaces. Tu la retrouveras sous d'autres noms dans Vue, SwiftUI, Flutter ou Jetpack Compose. Et le réflexe « toute ressource ouverte doit être fermée » (le pattern acquisition / libération) dépasse largement React : fichiers, connexions, écouteurs, verrous… c'est l'un des réflexes les plus rentables de toute ta carrière.