Triage du 95_a_capitaliser.md (~75 propositions) : - 60 entrées intégrées dans knowledge/ (backend, frontend, workflow) - 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md, frontend/patterns/general.md, workflow/patterns/general.md - 6 doublons rejetés - Mise à jour des READMEs index pour refléter les nouvelles entrées - 95_a_capitaliser.md restauré à sa structure initiale - 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant - 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI, prisma migrate diffs cosmétiques Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 KiB
Frontend — Risques & vigilance : State
Extrait de la base de connaissance Lead_tech. Voir
knowledge/frontend/risques/README.mdpour l'index complet.
Erreurs silencieuses / écrans blancs
Risques
- Exceptions non gérées → app inutilisable
- États async mal gérés → UI incohérente (loading infini, vide incompris)
Symptômes
- Écran blanc après une action
- Toast générique "Une erreur est survenue" sans corrélation
- Pas de moyen de reproduire / diagnostiquer
Bonnes pratiques / mitigations
- Pattern "états UI explicites" (loading/empty/error)
- Boundary d'erreur UI + fallback
- Logging minimal côté client avec requestId/traceId quand possible
Mélange server state / client state
Risques
- Cache pollué par des états UI (onglets, filtres)
- UI qui reflète une donnée périmée sans le savoir
- Re-renders et bugs de synchronisation
Symptômes
- "Ça revient tout seul" après refresh
- Données affichées ≠ données du backend
- Debug très long car état implicite
Bonnes pratiques / mitigations
- Séparer explicitement server state vs client state
- Invalidation/reload explicite du server state
- État UI local réinitialisable
Appels API gérés en state local d'écran (refactor coûteux)
Risques
- Server state non partageable entre écrans (liste/detail, wizard, tabs) → duplication et incohérences
- Pas de cache / invalidation standard → bugs subtils et re-fetchs inutiles
- Refactor tardif quand l'epic s'étend (mutations, cache, offline, pagination)
Symptômes
- Même appel API recopié dans plusieurs écrans
- Un écran "A" modifie une ressource mais l'écran "B" n'est jamais rafraîchi
- Code review qui force un refactor vers un store/cache au milieu d'un epic
Bonnes pratiques / mitigations
- Par défaut : créer un store de domaine (ex : Zustand) ou un cache de server state pour tout domaine susceptible d'être réutilisé
- Centraliser
isLoading/error/dataet la stratégie de refresh/invalidation - Exception acceptable : état purement UI, local et jetable (ex : input de recherche, filtres temporaires non persistés)
Catch silencieux — erreur inconnue sans feedback utilisateur
Risques
- Un
catchqui ne traite que les cas connus laisse l'utilisateur face à un spinner qui disparaît sans message - L'état d'erreur reste implicite → impossible de diagnostiquer ou de reproduire
Symptômes
- Bouton spinner qui s'arrête, rien ne se passe
- Pas de toast / message d'erreur affiché
- Erreur "avalée" silencieusement dans les logs
Bonnes pratiques / mitigations
} catch (err: unknown) {
const code = (err as { code?: string }).code;
if (code === 'SUBSCRIPTION_REQUIRED') {
setSubscriptionRequired(true);
} else {
setError('Une erreur est survenue. Veuillez réessayer.'); // toujours un fallback
}
}
- Règle : tout
catchdoit avoir une brancheelse(oudefault) qui affiche un feedback utilisateur explicite. - Contexte technique : React Native / Expo — 09-03-2026
Auto-reset d'un état dégradé sur toute réponse 2xx
Risques
- Le client sort trop tôt d'un mode dégradé alors que la cause serveur est toujours présente
- Le bandeau ou l'état read-only clignote puis disparaît à tort
- Les utilisateurs retentent une action d'écriture qui va encore échouer
Symptômes
- Un GET réussi réinitialise
isReadOnlyouisDegraded - L'UI redevient "normale" alors que Redis ou un service critique est toujours indisponible
- Les erreurs reviennent immédiatement à la prochaine mutation
Bonnes pratiques / mitigations
- Ne réinitialiser l'état dégradé qu'après une requête d'écriture réussie
- Exclure
GETetHEADde la logique de reset - Conserver le mode dégradé tant qu'aucune mutation n'a prouvé le retour à la normale
- Contexte technique : React Native / Expo — 10-03-2026
Refresh store en fire-and-forget après mutation
Risques
- L'UI affiche un succès alors que la resynchronisation a échoué
- État local incohérent avec l'état serveur
- Erreurs silencieuses impossibles à diagnostiquer
Symptômes
- Mutation réussie puis store jamais rafraîchi
- Spinner coupé avant que l'écran soit réellement à jour
- Données anciennes qui persistent jusqu'au prochain reload
Bonnes pratiques / mitigations
awaitexplicite du refresh si l'UI dépend du résultat- Gestion d'erreur dédiée sur la phase de resynchronisation
- N'utiliser le fire-and-forget que pour un effet secondaire réellement non bloquant
- Contexte technique : React Native / Expo — 10-03-2026
État booléen UI dérivé hardcodé au lieu d'être calculé depuis le store
Risques
- Un état toggle (
isBookmarked,isLiked,isFollowed) initialisé àfalseen dur ne reflète jamais l'état réel - Le bouton est toujours en mode "ajouter" sans jamais passer en mode "supprimer"
Symptômes
const isBookmarked = false; // état local géré ci-dessous via state- Bouton bookmark/like toujours dans le même état visuel peu importe l'état réel
Bonnes pratiques / mitigations
// ❌ Anti-pattern — état hardcodé
const isBookmarked = false;
// ✅ Pattern correct — dérivé du store au rendu
const { bookmarks } = useCommunityStore();
const isBookmarked = bookmarks.some((b) => b.thread.id === threadId);
-
Règle : si le store contient la liste (bookmarks, likes, follows), l'état booléen se dérive avec
.some()ou.has() -
Contexte technique : React Native / Zustand — app-alexandrie story 4.4, 20-03-2026
Flag isLoading unique pour des opérations de natures différentes
Risques
- Un même flag (ex:
isBookmarking) utilisé à la fois pour les mutations (add/remove) et le chargement de la liste provoque des bugs visuels — spinner manquant au premier chargement si une mutation est en cours en parallèle
Symptômes
- Spinner absent au premier chargement de la liste bookmarks
- Bouton "ajouter" désactivé alors qu'aucune mutation n'est en cours
Bonnes pratiques / mitigations
// ❌ Anti-pattern — un seul flag pour tout
isBookmarking: boolean;
// ✅ Pattern correct — séparation claire
isBookmarking: boolean; // mutations add/remove
isLoadingBookmarks: boolean; // chargement de la liste (GET)
- Contexte technique : React Native / Zustand — app-alexandrie story 4.4, 20-03-2026
Zustand : optimistic update sur item absent de la liste principale
Risques
- Une action admin qui cherche l'item uniquement dans
state.threads(liste paginée principale) manque les items présents exclusivement dansstate.pinnedThreadsoustate.showcasedThreads - L'optimistic update ne se reflète pas visuellement même si l'appel API a réussi
Symptômes
- L'item mis à jour par une action admin n'apparaît pas dans la nouvelle sous-liste après l'action
- Bug reproductible uniquement quand l'item est épinglé / en vitrine mais pas dans la page courante du flux principal
Bonnes pratiques / mitigations
// ❌ Anti-pattern : cherche uniquement dans la liste principale paginée
const target = state.threads.find((t) => t.id === threadId);
// → manque les items présents uniquement dans pinnedThreads / showcasedThreads
// ✅ Pattern correct : fallback sur toutes les sous-listes du store
const target =
state.threads.find((t) => t.id === threadId) ??
state.pinnedThreads.find((t) => t.id === threadId) ??
state.showcasedThreads.find((t) => t.id === threadId);
-
Règle : toute action qui opère sur un item pouvant être présent dans plusieurs sous-listes doit chercher dans l'ensemble de ces listes
-
Règle complémentaire : ne pas mettre à jour une sous-liste (ex:
pinnedThreads) lors d'une action qui n'y a pas de rapport (ex: mise en vitrine ne touche paspinnedThreads) -
Contexte technique : React Native / Zustand — app-alexandrie 23-03-2026
Store Zustand : méthodes update* qui avalent les erreurs sans rethrow
Risques
- Une méthode store qui catch une erreur sans la relancer (
throw) avale silencieusement les erreurs métier (ex:UNSAFE_LINK) - L'écran appelant ne reçoit jamais l'erreur → impossible d'afficher un feedback à l'utilisateur
Symptômes
- L'action semble réussir côté UI mais la donnée n'a pas changé en base
- Erreurs métier (ex: lien interdit) invisibles pour l'utilisateur final
Bonnes pratiques / mitigations
// ❌ MAUVAIS — l'erreur est avalée, l'écran ne sait pas que ça a échoué
async updateThread(forumSlug, threadId, body) {
await communityService.updateThread(accessToken, forumSlug, threadId, body);
},
// ✅ BON — l'erreur est propagée pour que l'écran puisse réagir
async updateThread(forumSlug, threadId, body) {
try {
await communityService.updateThread(accessToken, forumSlug, threadId, body);
} catch (e) {
const err = e as Error & { code?: string };
throw err; // Le code d'erreur (ex: UNSAFE_LINK) est préservé sur l'objet
}
},
-
Règle : toute méthode store qui appelle le service réseau doit soit (1) relancer l'erreur enrichie avec
throw err, soit (2) la stocker dans le state (set({ error: err.message })). Jamais les deux à la fois sans rethrow si l'écran doit réagir au catch. -
Contexte technique : React Native / Zustand — app-alexandrie 24-03-2026
Catch Zustand : objet structuré throwé vs instance Error
Risques
- Quand
apiRequestpropage une erreur HTTP en throwant le JSON brut{ error: { code, message } }, un catch limité àerr instanceof Errorlaisse le message inaccessible → fallback générique ou message vide affiché
Symptômes
- Toast "Erreur de connexion au serveur" affiché même quand le serveur retourne un message explicite
err.messageundefined alors que(err as ApiError).error.messagecontient la cause réelle
Bonnes pratiques / mitigations
catch (err: unknown) {
let message = 'Erreur de connexion au serveur.';
if (err instanceof Error) {
message = err.message;
} else if (
typeof err === 'object' && err !== null &&
'error' in err &&
typeof (err as { error: { message?: string } }).error?.message === 'string'
) {
message = (err as { error: { message: string } }).error.message;
}
set({ error: message, isLoading: false });
}
- Règle : tout store qui appelle
apiRequestdirectement doit inspecter les deux cas —Errornatif et objet structuré{ error: { message } } - Contexte technique : React Native / Zustand — app-alexandrie review 5.2, 27-03-2026
Flag booléen global pour des actions par entité — préférer Set<string>
Risques
- Un flag
isLoading: booleanglobal pour une action par item (follow, like, bookmark sur une liste) désactive tous les boutons dès qu'une action est en cours sur un seul item
Symptômes
- Cliquer "Suivre" sur une carte désactive tous les autres boutons "Suivre" de la liste
- Impossible de déclencher deux actions en parallèle sur des entités différentes
Bonnes pratiques / mitigations
// ❌ Anti-pattern — un seul flag pour toutes les instances
followIsLoading: boolean;
// ✅ Pattern correct — un Set des IDs en cours
followingInProgress: Set<string>;
// Selector
isFollowInProgress: (userId: string) => get().followingInProgress.has(userId),
// Mutation
set((state) => {
const next = new Set(state.followingInProgress);
next.add(targetUserId);
return { followingInProgress: next };
});
// Après succès/erreur
next.delete(targetUserId);
- Règle : toute action async sur des items d'une liste (like, bookmark, follow, reaction) doit utiliser
Set<EntityId>au lieu d'unbooleanglobal - Contexte technique : React Native / Zustand — app-alexandrie review 5.3, 28-03-2026
Clé d'erreur partagée entre action et liste dans un store Zustand
Risques
- Une clé d'erreur partagée entre une mutation (follow/unfollow) et un fetch de liste (followers, followings) affiche une erreur d'action périmée comme erreur de chargement
- Ex :
followError: 'ALREADY_FOLLOWING'stocké depuis une action précédente s'affiche dans l'écran "Abonnés" comme "Erreur de chargement"
Symptômes
- Écran de liste affiche une erreur après une action précédente sans rapport direct
- Bug intermittent difficile à reproduire, corrélé à l'ordre des actions utilisateur
Bonnes pratiques / mitigations
// ❌ Anti-pattern — clé partagée
followError: string | null;
// ✅ Pattern correct — clés séparées par nature
followError: string | null; // erreur de followUser/unfollowUser
followersError: string | null; // erreur de fetchFollowers
followingsError: string | null; // erreur de fetchFollowings
- Règle : dans un store qui gère à la fois des mutations et des listes paginées, chaque opération doit avoir sa propre clé d'erreur
- Contexte technique : React Native / Zustand — app-alexandrie review 5.3, 28-03-2026
emit Vue annonçant une mutation serveur sans listener parent → caches stale
Risques
- Un composant enfant émet un événement (
emit('approved')) après une mutation côté serveur, mais aucun parent n'écoute - L'enfant met bien à jour son état local, mais les caches parents qui dérivent du même statut (badges accordion, verrous cascade, prop
previousAggregateconsommée par d'autres enfants) restent stale indéfiniment, jusqu'au prochain changement d'écran/route - Bug invisible dans les tests structurels (
content.includes) et passe inaperçu en revue parce que le code enfant est "correct"
Symptômes
- Tout autre affichage du même statut dans le parent ou dans des sous-composants frères reste "Publiée" alors que la planche est "Approuvée" en DB
- L'UI ne se rafraîchit qu'au prochain reload manuel ou navigation
Bonnes pratiques / mitigations
# Repérer les emits qui annoncent une mutation serveur
grep -rn "defineEmits" apps/frontend/src/components
# Pour chaque emit trouvé, chercher s'il a au moins un listener parent
grep -rn "@<eventName>=" apps/frontend/src/pages apps/frontend/src/components
Zéro listener = bug latent (sauf si l'emit est purement informatif — analytics, debug).
Règle : tout emit qui annonce une mutation persistée serveur (création, suppression, changement de statut, validation) doit avoir au moins un listener parent qui :
- Invalide les caches locaux dérivés de la même donnée (Map de statuts, computed, props transitives)
- Recharge la slice agrégée si le parent passe une prop construite à partir d'un autre fetch (
previousAggregate, dashboard data) - Ne se contente PAS de l'optimistic update de l'enfant — la source de vérité reste serveur, le cache parent doit refléter l'état serveur post-mutation
<!-- ❌ Enfant émet, parent ignore. Le badge de l'accordion reste stale -->
<PlancheTraceeCard :tenue-id="planche.tenueId" mode="previous" />
<!-- ✅ Listener explicite qui invalide les caches dérivés -->
<PlancheTraceeCard
:tenue-id="planche.tenueId"
mode="previous"
@approved="onPlancheApproved(planche.tenueId)"
/>
Couverture : test de mount complet via @vue/test-utils qui simule l'événement et vérifie que le rendu parent change. Les tests readFileSync + content.includes('emit') valident que le code enfant émet, pas que le parent écoute.
- Contexte technique : Vue 3 — RL799_V2 29-04-2026
Templates Vue — références orphelines invisibles à tsc --noEmit
Risques
- Une variable supprimée du
<script setup>mais encore référencée dans le<template>ne génère pas d'erreur compile avectsc --noEmitseul (Volar moins strict quetscpur sur les expressions template) - Le composant crashe uniquement au runtime :
Cannot read properties of undefinedou[Vue warn] Property "X" was accessed during render but is not defined
Symptômes
- Refactor où on extrait un composable et oublie de destructurer une variable, ou on retire un import devenu (apparemment) inutilisé
- Typecheck passe, tests structurels passent, page charge mais une section ne s'affiche pas / un bouton ne fait rien
[Vue warn]dans la console au mount
Bonnes pratiques / mitigations
Recommandation outillage : migrer le typecheck du projet de tsc --noEmit vers vue-tsc -p tsconfig.typecheck.json --noEmit. Avec vue-tsc, les expressions template sont strictement typées contre le <script setup> exposé. Une ref orpheline → erreur de compile, pas warning runtime.
Checklist étendue avant de marquer une extraction "done" :
# Pour chaque symbole supprimé/non destructuré, grep dans le template
for symbol in normalizedQuery directoryOfficeLabel; do
echo "=== $symbol ==="
# Patterns template critiques
grep -nE "v-(if|else-if|show)=\"[^\"]*\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
grep -nE "\\{\\{[^}]*\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
grep -nE "(:|@)[a-z-]+=\"[^\"]*\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
done
QA visuel obligatoire post-refactor : pour tout refactor qui touche une page importante, ouvrir la page en browser dev avant de pousser :
-
naviguer aux états critiques (search, modals, toggle accordion)
-
vérifier la console : zéro
[Vue warn]ouReferenceError -
tester au moins un workflow complet par section refactorée
-
Contexte technique : Vue 3 / Volar /
vue-tsc— RL799_V2 29-04-2026
Symboles orphelins après suppression d'un bloc — checklist grep
Risques
- Refactor où on supprime un bloc cohérent (state + computeds + handlers) en utilisant un Edit ciblé. Tout compile, tous les tests passent, mais au runtime :
ReferenceError: <symbole> is not definedau mount - Le symbole supprimé était encore référencé dans un lifecycle hook (
onMounted,onUnmounted), un watcher, ou un handler asynchrone non inclus dans le bloc supprimé
Symptômes
- Page qui ne mount plus après refactor, alors que typecheck OK + tests verts
- Erreur visible uniquement à
cmd+Rsur la page
Bonnes pratiques / mitigations
# Pour chaque symbole déclaré dans le bloc à supprimer
for symbol in updatePointerType pointerMql closeInsertMenuOnOutsideClick; do
echo "=== $symbol ==="
grep -n "\\b$symbol\\b" apps/frontend/src/pages/<Page>.vue
done
Doit retourner uniquement les lignes du bloc à supprimer. Si une référence apparaît hors du bloc → ne pas supprimer sans traiter explicitement (déplacer, adapter, ou laisser).
Lieux à vérifier en priorité :
| Lieu | Fréquence du piège |
|---|---|
onMounted(() => { ... }) |
⚠️⚠️⚠️ très fréquent |
onUnmounted(() => { ... }) |
⚠️⚠️⚠️ très fréquent (cleanup) |
watch(() => x, () => { fn() }) |
⚠️⚠️ fréquent |
Handler async (.then(() => fn())) |
⚠️ rare mais existe |
| Computed dans une autre section du fichier | ⚠️ rare |
Template (@click, :disabled) |
typecheck attrape via SFC plugin |
Garde-fou complémentaire : QA visuel obligatoire post-refactor (cf. risque-templates-vue-references-orphelines).
- Contexte technique : Vue 3 — RL799_V2 29-04-2026
Extraction .vue → composable .ts révèle des bugs de typage latents
Risques
- Vue 3 + Volar compile les blocs
<script setup>avec une stratégie d'inférence moins agressive quetscstrict pur. Un handler peut compiler dans le.vuesi le runtime n'utilise pas le champ manquant - Le composable extrait est compilé par
tsc -p tsconfig.typecheck.jsonhors contexte Vue → toutes les règles strictes s'appliquent → la divergence de type devient une erreur bloquante - C'est un effet de bord positif mais qui peut bloquer l'extraction tant qu'on n'a pas diagnostiqué la divergence
Symptômes
Type 'X' is not assignable to type 'Y'.
The types of '<champ>.<sous-champ>' are incompatible…
Type 'A' is missing the following properties from type 'B': …
Cas typique : un emit Vue annonçait Detail (type pour la liste) alors que la fonction service renvoyait Data (type pour la mutation). Silencieux dans le .vue d'origine, devenu visible dans le .ts extrait.
Bonnes pratiques / mitigations
Trois cas typiques quand l'erreur apparaît après extraction :
- Le type annoncé ne matche pas le type réellement émis : aligner sur le type réellement émis plutôt que sur le type annoncé dans
defineEmits. Si possible, corriger aussi la signature de l'émetteur — mais c'est un autre scope - Le
.vueexploitait un cast implicite :v-if="x.foo"réduit le union type. Dans un.tsextrait, narrow explicitement avec unifou un type guard - Volar n'analysait pas un chemin de type complexe : type récursif, génériques imbriqués,
Pick<...>dans une union → extraire un alias intermédiaire propre dans@<module>/types.ts
Quoi faire face à l'erreur :
- NE PAS mettre
as Foopour faire taire le compilateur — c'est probablement masquer le même bug sous un autre nom - Identifier lequel des deux types est correct (généralement celui que la fonction service / l'API renvoie réellement)
- Aligner la signature du handler/composable sur ce type-là
- Documenter dans un commentaire au-dessus du handler que le type émis diverge du type annoncé dans
defineEmits(si on ne corrige pas l'émetteur dans le même refactor) - Ouvrir un TODO si la correction de l'émetteur est hors scope
Recommandation outillage : vue-tsc plutôt que tsc pur en typecheck (cf. risque-templates-vue-references-orphelines). Ce genre de divergence aurait été détecté avant le refactor.
- Contexte technique : Vue 3 / Volar /
vue-tsc— RL799_V2 29-04-2026