14 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