Files
_Assistant_Lead_Tech/knowledge/frontend/risques/state.md
MaksTinyWorkshop ef99f2a2ca capitalisation
2026-03-28 12:50:07 +01:00

14 KiB

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

} 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

// ❌ 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 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

// ❌ 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

// ❌ 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 apiRequest propage une erreur HTTP en throwant le JSON brut { error: { code, message } }, un catch limité à err instanceof Error laisse 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.message undefined alors que (err as ApiError).error.message contient 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 apiRequest directement doit inspecter les deux cas — Error natif 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: boolean global 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'un boolean global
  • 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