Traçage d'une fonctionnalité de bout en bout
Voici le moment où tout se connecte. Tu as appris les briques une par une — les composants, les hooks, le store, la base. Maintenant on prend une seule fonctionnalité réelle — l'écran d'entraînement actif — et on la suit de l'écran jusqu'à la base de données. À chaque virage, tu reconnaîtras une notion d'un module précédent. C'est le module qui met tout le guide en mouvement.
Pourquoi tracer ? La vraie compétence du développeur
Quand on débute, on croit qu'être bon développeur, c'est connaître : connaître la
syntaxe de useEffect par cœur, mémoriser tous les hooks, retenir le nom de chaque
fonction. C'est rassurant, parce que ça ressemble à apprendre une langue : assez de vocabulaire,
et on se débrouille. Mais c'est une illusion. Un projet réel comme Halterofit, c'est des
centaines de fichiers. Personne — pas même celui qui l'a écrit — ne tient tout ça en tête.
La connaissance par cœur ne passe pas à l'échelle.
La compétence qui passe à l'échelle, elle, est plus humble et bien plus puissante : savoir suivre une donnée à travers les couches d'un système. Donne-moi un écran qui affiche un nombre, et la vraie question n'est pas « est-ce que je connais React ? », mais « d'où vient ce nombre ? ». Quelle fonction l'a produit ? Quelle couche le lui a passé ? Et avant elle, qui ? On remonte le fil, maillon par maillon, jusqu'à la source. C'est exactement le geste d'un détective qui suit une piste, ou d'un plombier qui suit un tuyau du robinet jusqu'à la rue : on ne « connaît » pas tout le réseau, on suit le seul tuyau qui nous intéresse.
Ce réflexe a un nom : tracer. Et c'est libérateur, parce qu'il transforme un codebase intimidant (« je n'y comprends rien, c'est trop gros ») en une série de petites questions auxquelles on peut toujours répondre (« cette ligne appelle telle fonction — allons voir cette fonction »). Tu n'as jamais besoin de tout comprendre. Tu as juste besoin de comprendre le maillon suivant. C'est ce que ce module va te faire pratiquer sur un cas réel, en reliant explicitement chaque maillon au module du guide qui l'explique.
L'utilisateur est en plein entraînement. L'écran affiche le bon exercice, le temps écoulé depuis le début de la séance, et un bouton « exercice suivant ». Tout a l'air simple à l'écran. Mais derrière, plusieurs couches collaborent. Deux questions vont nous servir de fil rouge : par quel chemin l'app sait-elle quoi afficher, et que se passe-t-il exactement quand on touche « suivant » ? Suivons le fil, sans jamais sauter d'étape.
La carte du trajet
Avant de plonger, dessinons la carte. Souviens-toi du sens de circulation qu'on a posé dans le module sur la structure du projet : les écrans appellent des hooks, les hooks appellent des services, les services parlent à la base. La donnée descend pour être lue, et les changements remontent. Voici les acteurs réels de cette fonctionnalité, avec leur fichier exact :
ÉCRAN app/workout/active.tsx appelle useActiveWorkout()
│
▼
HOOK hooks/workout/useActiveWorkout.ts
│ ├─ lit la SESSION dans le store (module état global Zustand)
│ ├─ charge les données via un SERVICE (module useState/useEffect)
│ └─ prépare la liste affichable (module useContext/useMemo/memo)
▼
SERVICE services/database/operations/workouts/getWorkoutWithDetails()
│
▼
BASE WatermelonDB (local sur le téléphone) ↕ Supabase (synchro)
Garde cette carte sous les yeux. Chaque section qui suit prend une flèche de ce schéma et
l'examine de près. Remarque déjà une chose : le hook useActiveWorkout est au
centre de tout. C'est lui le chef d'orchestre. C'est pour ça qu'on l'a choisi comme
synthèse : un seul fichier qui touche presque toutes les couches.
Tout le code de ce module sort d'un fichier réel :
apps/mobile/src/hooks/workout/useActiveWorkout.ts. On va le lire morceau par
morceau, dans l'ordre où il s'exécute. À la fin, tu auras lu le fichier entier — et tu
comprendras pourquoi chaque ligne est là.
Le décor : l'écran que ce hook nourrit
Avant de descendre dans le hook, levons les yeux vers ce qu'il sert. L'écran
active.tsx est construit avec les primitives de React Native :
des <View> pour la mise en page, du <Text> pour afficher
l'exercice et le chrono, un <Pressable> (ou un bouton) pour passer au
suivant. Souviens-toi : en React Native, on ne manipule pas le DOM d'un navigateur, on décrit
une interface avec ces composants natifs, et le framework la peint à l'écran.
Or cet écran ne fait aucun travail de données. Il ne sait pas ce qu'est une base, ni un store. Tout ce qu'il fait, c'est une seule ligne magique :
// L'écran demande tout ce dont il a besoin en UNE ligne.
// Toute la logique (store, base, filtrage) est cachée derrière le hook.
const { currentExercise, elapsedSeconds, goToNextExercise, isLastExercise } =
useActiveWorkout();
C'est le contrat. L'écran réclame des données (currentExercise,
elapsedSeconds) et des actions (goToNextExercise), et il les
affiche. Notre travail de traçage : comprendre comment le hook remplit ce contrat. On part donc
de l'écran et on descend.
Étape 1 — Le hook lit la session dans le store
Première chose que fait useActiveWorkout : il demande au store global « où en
est-on dans la séance ? ». C'est tout le rôle du module sur l'état global avec
Zustand : la session de workout (quel workout est en cours, à quel exercice on en est,
depuis quand on s'entraîne) est un état partagé qui doit survivre aux changements
d'écran. Le hook le lit avec des sélecteurs précis :
// Chaque ligne = un SÉLECTEUR. On ne prend du store que LA valeur
// dont on a besoin. Le composant ne re-render que si CETTE valeur change.
const currentWorkoutId = useWorkoutStore((s) => s.currentWorkoutId);
const workoutStartTime = useWorkoutStore((s) => s.workoutStartTime);
const currentExerciseIndex = useWorkoutStore((s) => s.currentExerciseIndex);
// On récupère aussi des ACTIONS (des fonctions du store) :
const setExerciseIndex = useWorkoutStore((s) => s.setExerciseIndex);
const nextExercise = useWorkoutStore((s) => s.nextExercise);
const prevExercise = useWorkoutStore((s) => s.prevExercise);
// Une clé stable pour détecter l'ajout/retrait d'un exercice :
// joindre les ids en une chaîne "id1,id2,id3".
const exerciseIdsKey = useWorkoutStore((s) => s.exerciseIds.join(','));
On récupère à la fois des valeurs (l'id du workout, l'index courant) et des
actions (nextExercise) — souviens-toi, dans Zustand les actions vivent
dans le store au même titre que l'état.
Pourquoi un sélecteur par ligne plutôt que de tout prendre d'un coup ? C'est le cœur du module
sur l'état global Zustand : chaque sélecteur crée un abonnement ciblé.
Le composant ne se réveille (ne se ré-exécute) que si la valeur exacte qu'il a sélectionnée a
changé. Si le chrono avance mais que l'index d'exercice ne bouge pas, la ligne
currentExerciseIndex ne provoque aucun réveil. C'est le contraire du gaspillage :
on s'abonne au strict nécessaire.
Regarde de près la dernière ligne, exerciseIdsKey. C'est une astuce élégante.
s.exerciseIds est un tableau d'identifiants. Si on s'abonnait au tableau
lui-même, Zustand le verrait comme « nouveau » à presque chaque écriture du store (un tableau
est une référence, et la référence change facilement) — on rechargerait trop souvent. En le
transformant en une simple chaîne de caractères "id1,id2,id3" avec
.join(','), on obtient une valeur qui ne change que si la composition de
la liste change vraiment. C'est du JavaScript tout simple au service d'une
mécanique React fine.
Comparer deux tableaux pour savoir s'ils sont « les mêmes » est coûteux et piégeux en JavaScript. Comparer deux chaînes, c'est instantané et sans ambiguïté. En réduisant une liste d'ids à une chaîne, on se donne une signature bon marché : tant que la signature est identique, rien n'a bougé ; dès qu'elle diffère, on sait qu'il faut réagir. Cette idée — « résumer une structure en une valeur simple et comparable » — te resservira partout.
Étape 2 — Charger les données via le service
Le hook a l'identité du workout (currentWorkoutId), mais pas encore ses
données (les exercices, les séries, les détails). Pour les obtenir, il doit aller à la
base. Mais — point capital de la structure du projet — un hook ne parle
jamais directement à la base. Il délègue à un service, une fonction
dédiée qui sait, elle, interroger WatermelonDB. Cette séparation des couches est ce qui garde le
code lisible : le hook s'occupe du « quand » et du « comment réagir », le service s'occupe du
« où sont les données ».
Le chargement se fait dans un useEffect — exactement le schéma du module sur
useState et useEffect : un effet pour les actions qui touchent le monde
extérieur (ici, lire la base), avec un tableau de dépendances et un nettoyage.
const [loadedWorkout, setLoadedWorkout] =
useState<WorkoutWithDetails | null>(null);
const [failedWorkoutId, setFailedWorkoutId] =
useState<string | null>(null);
useEffect(() => {
if (!currentWorkoutId) return; // pas de séance → rien à charger
let cancelled = false; // garde anti "réponse en retard"
getWorkoutWithDetails(currentWorkoutId) // ◀── LE SERVICE
.then((data) => {
if (!cancelled) setLoadedWorkout(data); // setter → re-render
})
.catch((err) => {
if (cancelled) return;
setFailedWorkoutId(currentWorkoutId);
handleError(err, 'useActiveWorkout');
});
return () => { // NETTOYAGE : si l'effet rejoue, on ignore
cancelled = true; // la réponse de l'ancien chargement
};
}, [currentWorkoutId, exerciseIdsKey, handleError]); // les dépendances
Le WorkoutWithDetails est un type défini dans le module sur
TypeScript : un workout enrichi de ses exercices et de leurs détails. Le
useState<WorkoutWithDetails | null> dit « cette case contiendra soit un
workout complet, soit rien (null) au départ ».
Trois détails méritent qu'on s'y attarde, parce qu'ils condensent à eux seuls plusieurs modules.
Le .then(...) et l'asynchrone. Lire la base prend du temps :
getWorkoutWithDetails ne renvoie pas les données tout de suite, elle renvoie une
promesse de les fournir plus tard. C'est tout le module sur le
JavaScript asynchrone : on enchaîne .then((data) => ...) pour
dire « quand les données arriveront, fais ceci avec ». Pendant ce temps, l'app ne se fige pas,
l'écran reste fluide.
Le setter qui re-render. Quand les données arrivent, on appelle
setLoadedWorkout(data). Ce n'est pas une simple affectation : appeler un setter de
useState dit à React « cette donnée a changé, recalcule l'écran ». C'est le module
sur le modèle de React en action : on ne « pousse » pas manuellement les
nouvelles données vers l'écran, on change l'état et React re-render tout seul.
La garde cancelled. Imagine que l'utilisateur change de workout
pendant qu'un chargement est en cours. L'ancienne requête pourrait répondre après la
nouvelle et écraser le bon résultat avec des données périmées — un bug classique des courses
asynchrones. Le drapeau cancelled, basculé à true dans la fonction de
nettoyage du useEffect, neutralise toute réponse en retard. C'est précisément le
rôle du nettoyage qu'on a vu dans le module sur useState/useEffect : à chaque
fois que l'effet rejoue (ou que l'écran disparaît), l'ancien effet se range proprement avant que
le nouveau démarre.
getWorkoutWithDetails n'est pas magique. À l'intérieur, elle interroge
WatermelonDB : elle trouve le workout par son id, récupère ses
workout_exercises triés par ordre, puis pour chacun va chercher les détails de
l'exercice et ses séries. Elle assemble tout ça en un seul objet WorkoutWithDetails.
Le hook ne sait rien de tout ce travail — il reçoit juste l'objet fini. C'est ça, une couche
de service : elle cache la complexité de la base derrière une fonction simple.
Étape 3 — Préparer la liste affichable avec useMemo
Le hook a maintenant le workout complet venant de la base. Mais il y a une subtilité métier :
l'utilisateur a pu retirer un exercice de sa séance du jour tout en gardant ses séries
déjà enregistrées. Dans ce cas, la ligne existe encore en base (pour le résumé d'après-séance),
mais l'exercice ne doit plus apparaître dans la vue active. Il faut donc filtrer
la liste venue de la base par les ids encore présents dans le store. Et ce calcul, on le fait
avec useMemo :
// On garde la valeur valide seulement si elle correspond à la séance
// active (évite d'afficher l'ancien workout pendant un rechargement).
const workout = loadedWorkout?.id === currentWorkoutId ? loadedWorkout : null;
// useMemo : ne refait ce filtrage QUE si workout ou exerciseIdsKey
// changent — surtout PAS à chaque tick du chrono.
const exercises = useMemo(() => {
const dbExercises = workout?.exercises ?? []; // ?? : "sinon, tableau vide"
if (dbExercises.length === 0) return dbExercises;
const visibleIds = new Set(
exerciseIdsKey ? exerciseIdsKey.split(',') : []
);
return dbExercises.filter((e) => visibleIds.has(e.id)); // garder les visibles
}, [workout, exerciseIdsKey]);
const currentExercise = exercises[safeIndex]; // l'exo à afficher MAINTENANT
workout?.exercises et ?? [] sont des outils du module sur le
JavaScript : le ?. évite de planter si workout est
null, et le ?? fournit une valeur de repli (un tableau vide) au cas où.
Pourquoi useMemo ? Parce que cet écran a un chronomètre qui avance chaque
seconde. Or, comme on l'a vu dans le module sur le modèle de React, chaque tick
du chrono provoque un re-render du hook. Sans précaution, on referait ce filtrage à chaque
seconde, pour rien. useMemo — au cœur du module sur
useContext, useMemo et memo — mémorise le résultat et ne le recalcule que si
l'une de ses dépendances (workout ou exerciseIdsKey) a vraiment changé.
Le chrono peut tourner : tant que la liste d'exercices ne bouge pas, le filtrage n'est pas
refait. C'est un gain de performance, mais surtout une déclaration d'intention : « ce calcul ne
dépend que de ces deux choses ».
Note aussi les deux lignes const workout = ... et safeIndex autour du
useMemo. La première est une garde de cohérence : on ne considère le workout chargé
comme valide que si son id correspond à la séance active — sinon, on renvoie null
plutôt que de risquer d'afficher les données d'un workout précédent pendant un rechargement.
Le safeIndex protège l'index : si la liste a rétréci (un exercice retiré), on borne
l'index pour ne pas pointer dans le vide. Ce sont des réflexes de programmation défensive : on
anticipe les états transitoires.
Étape 4 — Renvoyer un paquet propre à l'écran
Dernière étape : le hook rassemble tout ce dont l'écran a besoin et le renvoie en un seul objet. Données et actions, côte à côte :
const elapsedSeconds = useElapsedSeconds(workoutStartTime); // le chrono
return {
workout,
loading,
failed,
exercises,
exerciseCount,
currentExercise, // QUOI afficher
currentExerciseIndex: safeIndex,
isFirstExercise: safeIndex === 0,
isLastExercise: exerciseCount > 0 && safeIndex === exerciseCount - 1,
elapsedSeconds, // le chrono prêt à l'emploi
goToExercise: setExerciseIndex, // des ACTIONS du store, ré-exposées
goToNextExercise: nextExercise,
goToPrevExercise: prevExercise,
};
L'écran (active.tsx) n'a plus qu'à écrire
const { currentExercise, goToNextExercise } = useActiveWorkout() et tout afficher
avec les composants de React Native. Toute la complexité — store, base,
filtrage, asynchrone — est cachée derrière ce hook.
Remarque l'élégance de cette frontière. L'écran reçoit un objet bien rangé : des données
déjà prêtes (currentExercise, elapsedSeconds), des indicateurs
pratiques (isLastExercise pour, par exemple, changer le libellé du bouton sur le
dernier exercice), et des actions à brancher sur les boutons (goToNextExercise).
L'écran n'a aucune idée qu'il existe une base de données, un store Zustand ou une promesse. Cette
ignorance est une force : c'est ce qui rend l'écran simple à lire et le hook simple à
tester. Chaque couche ne connaît que son voisin immédiat.
Dans le vrai fichier, juste au-dessus de la fonction, on trouve
export type UseActiveWorkoutReturn = ReturnType<typeof useActiveWorkout>.
C'est du TypeScript astucieux : plutôt que de décrire à la main le type de ce
gros objet retourné, on demande à TypeScript de le déduire automatiquement. Si demain
tu ajoutes un champ au return, le type se met à jour tout seul. Zéro entretien.
L'aller-retour complet : appuyer sur « suivant »
On a tracé le chemin descendant (de l'écran à la base, pour lire). Traçons maintenant le chemin montant : que se passe-t-il, exactement, quand le doigt touche le bouton « suivant » ? C'est ici que la boucle se referme et que tu vois la circulation dans les deux sens.
-
L'utilisateur touche le bouton. Le
<Pressable>de React Native déclenche sononPress, qui appellegoToNextExercise(). -
goToNextExercise, on l'a vu, n'est que l'actionnextExercisedu store, ré-exposée. Elle fait unset(...)qui augmentecurrentExerciseIndex— pur module état global Zustand. -
L'état global change. Tous les composants abonnés (via leur sélecteur) à
currentExerciseIndexse réveillent et re-render — c'est le module sur le modèle de React. -
Le hook se ré-exécute donc avec le nouvel index. Il recalcule
currentExercise = exercises[nouvelIndex]. Et grâce auuseMemodu module useContext/useMemo/memo, il ne refiltre pas la liste : seul l'accès par index change. Rapide. -
L'écran reçoit le nouveau
currentExerciseet l'affiche. Aucun « rafraîchissement » manuel n'a été écrit nulle part : un simple changement d'état a tout déclenché en cascade.
Prends une seconde pour mesurer ce qui vient de se passer. Personne, dans tout ce code, n'a écrit « va mettre à jour l'écran ». Le programmeur a seulement décrit quoi afficher en fonction de l'état, et comment changer l'état. React et Zustand se chargent de relier les deux. C'est tout le pari du modèle de React : tu décris, le framework synchronise. Tracer cet aller-retour, c'est voir ce pari fonctionner pour de vrai.
La photo d'ensemble : un seul fichier, tout le guide
Recule maintenant et regarde le tableau complet. Ce seul fichier,
useActiveWorkout.ts, à peine plus de cent lignes, convoque la majorité des notions
du guide. Fais le décompte :
| Ce que tu vois dans le fichier | Le module qui l'explique |
|---|---|
<View>, <Text>, <Pressable> côté écran | React Native (les primitives de l'écran) |
| Le setter qui déclenche l'affichage | le modèle de React (le re-render) |
useState + useEffect avec nettoyage | useState et useEffect |
useMemo pour le filtrage | useContext, useMemo et memo |
| Hook → service → base, couches séparées | la structure du projet |
| Sélecteurs et actions du store | l'état global avec Zustand |
getWorkoutWithDetails qui interroge la base | WatermelonDB (d'où viennent les données) |
WorkoutWithDetails, useState<... | null> | TypeScript (les types) |
.then(...), ?., ?? | JavaScript et l'asynchrone |
Neuf modules, un fichier. Ce n'est pas un hasard : un hook de fonctionnalité, c'est par nature un point de rencontre. Il se tient à la couture entre l'interface et les données, donc il touche tout. C'est précisément pourquoi savoir lire un fichier comme celui-ci, c'est savoir lire l'app. Tu n'as pas besoin d'avoir tout le projet en tête — tu as besoin de savoir, devant n'importe quelle ligne, à quel concept elle se rattache et où aller voir la suite.
Un seul fichier de hook (useActiveWorkout.ts) réunit l'essentiel du guide : les
types (TypeScript), le re-render (le modèle de React), useState/useEffect,
useMemo, la séparation en couches (la structure du projet), les sélecteurs et
actions (Zustand), la source des données (WatermelonDB) et l'asynchrone (JavaScript). Si tu
sais lire ce fichier ligne à ligne, tu sais lire la grande majorité de Halterofit. Relis-le :
tu reconnaîtras chaque morceau.
Refais-le toi-même : la méthode pour tracer n'importe quoi
La vraie victoire de ce module, ce n'est pas de connaître useActiveWorkout par
cœur — c'est de pouvoir refaire ce traçage seul, sur n'importe quelle autre
fonctionnalité, sans guide. Voici la méthode, en quatre temps. Elle marche pour l'écran de
l'historique, le profil, les statistiques, n'importe quoi.
-
Pars de l'écran. Trouve le fichier de l'écran (souvent dans
app/...). C'est ton point de départ, ton robinet. Repère la donnée qui t'intéresse à l'affichage — un nombre, une liste, un titre — et trouve la variable qui la porte dans le JSX. -
Suis les hooks. D'où vient cette variable ? Presque toujours d'un appel de
hook en haut du composant (
const { ... } = useQuelqueChose()). Ouvre ce hook. C'est ton maillon suivant. À l'intérieur, repère comment la variable est construite. -
Suis les services. Si le hook lit des données « du dehors », il appelle un
service (souvent un nom comme
getXxx,fetchXxx, dansservices/...). Ouvre le service. C'est lui qui parle à la base. Tu approches de la source. -
Suis jusqu'à la base. Le service interroge WatermelonDB
(
database.get(...).query(...)ou.find(...)). Là, tu touches le fond du puits : c'est d'ici que la donnée part. Tu as tracé toute la chaîne.
Et pour le sens inverse (une action de l'utilisateur), même logique à l'envers : pars du
onPress du bouton, suis la fonction qu'il appelle, vois quelle action de store ou
quel service elle déclenche, et observe comment le changement d'état revient jusqu'à l'écran.
C'est toujours le même geste : un maillon à la fois, jamais plus.
La compétence reine d'un développeur n'est pas de connaître chaque hook par cœur, mais de suivre une donnée à travers les couches d'un système. Tu viens de le faire, dans les deux sens. Ce réflexe — « par où passe cette donnée ? » — ne dépend ni de React, ni de Halterofit. Il fonctionne sur n'importe quel projet, dans n'importe quel langage : un site web, un script Python, un jeu. Pars de ce qui s'affiche, remonte jusqu'à la source, un maillon à la fois. C'est la compétence que tu emporteras partout.
Le piège qui paralyse les débutants : vouloir tout comprendre d'un coup. On
ouvre useActiveWorkout, on voit cent lignes, le store, le service, des promesses,
du useMemo, et on se dit « c'est trop, je n'y arriverai jamais ». Erreur. On ne
lit jamais un fichier « d'un coup ». On suit un seul fil : une donnée, un maillon à la
fois. « D'où vient currentExercise ? » → de exercises[safeIndex] →
d'où vient exercises ? → du useMemo → et ainsi de suite. Chaque
question est minuscule et a toujours une réponse. C'est l'accumulation de petits pas tracés qui
fait la compréhension, jamais le grand saut. Quand tu te sens noyé, c'est le signal que tu as
lâché ton fil : reviens à une seule donnée et reprends.
Sans regarder le corrigé, trace toi-même cette fonctionnalité : quand l'utilisateur ajoute un exercice en plein entraînement, quelle est la chaîne d'événements complète qui fait apparaître ce nouvel exercice à l'écran ? Nomme, à chaque étape, le module concerné.
Voir le corrigé
1. L'ajout appelle une action du store qui modifie le tableau
exerciseIds (module état global Zustand).
2. Le sélecteur exerciseIdsKey = s.exerciseIds.join(',') produit
donc une nouvelle valeur de chaîne → le hook re-render (module modèle de
React).
3. exerciseIdsKey est dans les dépendances du
useEffect → l'effet rejoue et recharge le workout via le service
getWorkoutWithDetails (modules useState/useEffect +
structure du projet + WatermelonDB).
4. exerciseIdsKey est aussi une dépendance du
useMemo → la liste exercises est refiltrée et inclut désormais le
nouvel exercice (module useContext/useMemo/memo).
5. L'écran re-render avec la liste à jour et affiche le nouvel onglet
d'exercice, le tout avec les composants de React Native.
Tu viens de tracer une fonctionnalité à travers six modules, dans le bon ordre. C'est très
exactement ce que signifie « lire le code en autonomie ».
1. Pourquoi le hook délègue-t-il à getWorkoutWithDetails au lieu de lire la base lui-même ?
Pour respecter la séparation en couches (module sur la structure du projet) : le hook gère le « quand » et la réaction, le service gère le « où sont les données ». Cette frontière garde le hook lisible et le service réutilisable, et permet à l'écran d'ignorer totalement l'existence de la base.
2. À quoi sert la variable cancelled dans le useEffect ?
À ignorer une réponse asynchrone arrivée « en retard ». Si l'utilisateur change de workout
pendant un chargement, le nettoyage du useEffect bascule cancelled à
true, et l'ancienne réponse est jetée au lieu d'écraser les nouvelles données. C'est
la garde anti-course du module useState/useEffect.
3. Pourquoi s'abonner à s.exerciseIds.join(',') plutôt qu'au tableau s.exerciseIds directement ?
Parce qu'un tableau est une référence qui « change » trop souvent aux yeux de Zustand, ce qui
relancerait des rechargements inutiles. La chaîne "id1,id2,id3" est une signature
stable : elle ne change que si la composition de la liste change vraiment. C'est du
JavaScript simple au service de la finesse de l'état global.
4. Le chrono avance d'une seconde. Le filtrage useMemo est-il refait ?
Non. Le tick du chrono provoque bien un re-render (module modèle de React),
mais le useMemo (module useContext/useMemo/memo) ne recalcule que
si ses dépendances workout ou exerciseIdsKey changent. Le chrono n'en
fait pas partie, donc le filtrage est sauté et le résultat mémorisé est réutilisé.
5. Quelle est la méthode générale pour tracer n'importe quelle fonctionnalité seul ?
Partir de l'écran (la donnée affichée), suivre le hook qui la fournit, suivre le service que le
hook appelle, puis suivre jusqu'à la base (WatermelonDB). Un maillon à la fois, jamais tout d'un
coup. Pour une action, on fait l'inverse : du onPress vers l'action de store, et on
regarde le changement d'état revenir jusqu'à l'écran.