Module 15 · Synthèse

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.

🎯 La scène

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.

🏋️ Dans Halterofit

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 :

apps/mobile/src/app/workout/active.tsx
// 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 :

apps/mobile/src/hooks/workout/useActiveWorkout.ts
// 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.

💡 Le concept : pourquoi une clé en chaîne ?

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.

apps/mobile/src/hooks/workout/useActiveWorkout.ts
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.

🏋️ Dans Halterofit : que fait vraiment le service ?

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 :

apps/mobile/src/hooks/workout/useActiveWorkout.ts
// 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 :

apps/mobile/src/hooks/workout/useActiveWorkout.ts
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.

🧭 Bon à savoir : un type qui se maintient seul

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.

🔁 Le cycle, étape par étape
  1. L'utilisateur touche le bouton. Le <Pressable> de React Native déclenche son onPress, qui appelle goToNextExercise().
  2. goToNextExercise, on l'a vu, n'est que l'action nextExercise du store, ré-exposée. Elle fait un set(...) qui augmente currentExerciseIndex — pur module état global Zustand.
  3. L'état global change. Tous les composants abonnés (via leur sélecteur) à currentExerciseIndex se réveillent et re-render — c'est le module sur le modèle de React.
  4. Le hook se ré-exécute donc avec le nouvel index. Il recalcule currentExercise = exercises[nouvelIndex]. Et grâce au useMemo du module useContext/useMemo/memo, il ne refiltre pas la liste : seul l'accès par index change. Rapide.
  5. L'écran reçoit le nouveau currentExercise et 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 fichierLe module qui l'explique
<View>, <Text>, <Pressable> côté écranReact Native (les primitives de l'écran)
Le setter qui déclenche l'affichagele modèle de React (le re-render)
useState + useEffect avec nettoyageuseState et useEffect
useMemo pour le filtrageuseContext, useMemo et memo
Hook → service → base, couches séparéesla structure du projet
Sélecteurs et actions du storel'état global avec Zustand
getWorkoutWithDetails qui interroge la baseWatermelonDB (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.

À retenir

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.

  1. 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.
  2. 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.
  3. Suis les services. Si le hook lit des données « du dehors », il appelle un service (souvent un nom comme getXxx, fetchXxx, dans services/...). Ouvre le service. C'est lui qui parle à la base. Tu approches de la source.
  4. 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.

🔄 Transférable

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.

⚠️ Piège fréquent

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.

✍️ Exercice de synthèse

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

🧠 Quiz éclair

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.