mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Refonte Structure
This commit is contained in:
280
knowledge/frontend/risques/state.md
Normal file
280
knowledge/frontend/risques/state.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Frontend — Risques & vigilance : State
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-erreurs-silencieuses"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-melange-server-client-state"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-api-state-local-ecran"></a>
|
||||
## 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)
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-catch-silencieux"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-auto-reset-etat-degrade"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-refresh-store-fire-and-forget"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-boolean-ui-hardcode-store"></a>
|
||||
## É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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-flag-isloading-unique-nature-differente"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-zustand-optimistic-update-sous-listes"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-zustand-erreur-sans-rethrow"></a>
|
||||
## 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
|
||||
Reference in New Issue
Block a user