# Frontend — Risques & vigilance : State > Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour 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`/`data` et 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 `catch` qui 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 ```typescript } 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 `catch` doit avoir une branche `else` (ou `default`) 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 `isReadOnly` ou `isDegraded` - 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 `GET` et `HEAD` de 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 - `await` explicite 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é à `false` en 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 ```typescript // ❌ 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 ```typescript // ❌ 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 dans `state.pinnedThreads` ou `state.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 ```typescript // ❌ 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 pas `pinnedThreads`) - 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 ```typescript // ❌ 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