# Front-end — Risques & vigilance

Ce fichier recense des risques front-end susceptibles de provoquer :

- bugs subtils,
- comportements inattendus,
- dette technique rapide,
- régressions UX/perf/a11y.

Dernière mise à jour : 23-03-2026

---

## Règles d’utilisation

- Chaque entrée doit dire :
  - ce qui peut mal se passer,
  - comment on le voit (symptômes),
  - comment on le maîtrise (mitigation).
- Si c’est lié à une stack / version : on note le contexte.

---

## Index

- [Auth côté client](#risque-auth-cote-client)
- [Erreurs silencieuses / écrans blancs](#risque-erreurs-silencieuses)
- [Mélange server state / client state](#risque-melange-server-client-state)
- [Appels API en state local d’écran](#risque-api-state-local-ecran)
- [Performances : sur-renders + bundle](#risque-performances-sur-renders)
- [Accessibilité oubliée (a11y)](#risque-accessibilite-oubliee)
- [Catch silencieux — erreur inconnue sans feedback utilisateur](#risque-catch-silencieux)
- [Auto-reset d’un état dégradé sur toute réponse 2xx](#risque-auto-reset-etat-degrade)
- [Refresh store en fire-and-forget après mutation](#risque-refresh-store-fire-and-forget)
- [Loading infini sur écran gated par droits distants](#risque-loading-infini-ecran-gated)
- [Jest React Native — config node bloque les composants `.tsx`](#risque-jest-rn-config-node)
- [Bouton OAuth présent mais handler vide après refacto UI](#risque-oauth-handler-vide)
- [Double système d'espacement dans un monorepo Expo](#risque-double-systeme-espacement)
- [Dimensions d'image via tokens `spacing` (React Native)](#risque-dimensions-image-via-spacing)
- [Écran détail Expo Router — store vide en deep link / reload](#risque-store-vide-deep-link)
- [`useEffect` fetch — guard incomplet sur les états terminaux](#risque-useeffect-guard-incomplet)
- [Store Zustand : collections sans clé de contexte (navigation inter-contexte)](#risque-zustand-collection-sans-cle-contexte)
- [`useSearchParams()` sans `Suspense` casse le build Next.js App Router](#risque-usesearchparams-sans-suspense)
- [Type `ViewData` dupliqué entre couche serveur et composant UI (Next.js)](#risque-type-viewdata-duplique)
- [Composant React dans un fichier `.ts` — `React.createElement` workaround](#risque-composant-react-fichier-ts)
- [Double validation de segment dynamique App Router (layout + page)](#risque-double-validation-segment-app-router)
- [Faux test négatif — tester le helper au lieu de tester l'exclusion](#risque-faux-test-negatif)
- [État booléen UI dérivé hardcodé au lieu d'être calculé depuis le store](#risque-boolean-ui-hardcode-store)
- [Flag `isLoading` unique pour des opérations de natures différentes](#risque-flag-isloading-unique-nature-differente)
- [Consent state : `false` ambigu entre "pas de décision" et "refus explicite"](#risque-consent-state-false-ambigu)
- [Script inline : interpolation directe au lieu de `JSON.stringify`](#risque-script-inline-interpolation-directe)
- [Next.js App Router : `window.location.reload()` au lieu de `router.refresh()`](#risque-window-location-reload-nextjs)
- [`useTransition` + optimistic update : snapshot capturé après `setState`](#risque-usetransition-snapshot-apres-setstate)
- [`window.confirm()` dans une app React/Next.js](#risque-window-confirm-react)
- [`import type` depuis `src/server/**` dans un composant client](#risque-import-type-server-composant-client)
- [Inline styles dans les composants dashboard](#risque-inline-styles-dashboard)
- [Classes Tailwind invalides courantes (bugs silencieux)](#risque-tailwind-classes-invalides)
- [Next.js : `<img>` natif interdit dans les composants](#risque-img-natif-nextjs)
- [`useTransition` global pour des listes d'items interactifs](#risque-usetransition-global-liste-items)
- [`useCallback` inutile quand le callback est wrappé en inline au render](#risque-usecallback-inutile-inline)
- [Formulaire React avec `defaultValue` sans `key` prop](#risque-formulaire-defaultvalue-sans-key)
- [Zustand : optimistic update sur item absent de la liste principale](#risque-zustand-optimistic-update-sous-listes)

---

<a id="risque-auth-cote-client"></a>
## Auth côté client (mauvaise séparation des responsabilités)

### Risques

- Le front “décide” des permissions au lieu d’appliquer un contrat backend
- Affichage d’actions interdites / fuite d’informations dans l’UI
- Tokens stockés de façon dangereuse (XSS)

### Symptômes

- Différences entre “ce que l’UI permet” et “ce que l’API accepte”
- Bugs “ça marche chez moi” selon sessions/rôles
- Incohérences sur refresh / multi-tab

### Bonnes pratiques / mitigations

- Le backend reste source de vérité (authz)
- Cacher l’UI ≠ sécuriser : toujours sécuriser côté API
- Stockage tokens : privilégier cookies httpOnly si modèle adapté
- Gérer proprement expiration/refresh + révocation

### Contexte technique

- Observé : (à compléter)
- Stack : (à préciser)

---

<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-performances-sur-renders"></a>
## Performances : sur-renders + bundle non maîtrisé

### Risques

- App lente sur mobile
- Bundle qui grossit sans contrôle
- Chargements inutiles (images, libs)

### Symptômes

- Input lag
- Temps de chargement qui dérive à chaque feature
- Requêtes réseaux inutiles

### Bonnes pratiques / mitigations

- Lazy loading routes/features
- Mesurer (au minimum) : temps de chargement + re-renders critiques
- Politique images (formats, tailles, lazy)
- Audit régulier des dépendances

---

<a id="risque-accessibilite-oubliee"></a>
## Accessibilité oubliée (a11y)

### Risques

- App inutilisable au clavier/lecteur d’écran
- Régressions silencieuses sur focus/labels

### Symptômes

- Modales impossibles à fermer au clavier
- Inputs sans labels/erreurs non annoncées
- Focus “perdu”

### Bonnes pratiques / mitigations

- Checklist a11y minimale sur chaque écran clé
- Gestion de focus (modales, erreurs formulaire)
- Labels/aria cohérents + tests simples

---

<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-loading-infini-ecran-gated"></a>
## Loading infini sur écran gated par droits distants

### Risques

- Un écran protégé reste bloqué dans un faux `loading` après une erreur de chargement des entitlements
- Un effet relance automatiquement la récupération en boucle sans action utilisateur
- L’utilisateur ne voit ni état d’erreur ni issue de sortie claire

### Symptômes

- Spinner infini sur un écran soumis à permissions distantes
- `entitlements` ou autorisations laissés à `null` après erreur
- `useEffect` ou logique d’entrée qui retrigger le fetch à chaque rendu

### Bonnes pratiques / mitigations

- Distinguer explicitement `loading`, `error`, `ready`
- Ne pas réutiliser `null` comme état ambigu "pas encore chargé" et "chargement en erreur"
- Bloquer les retries automatiques en boucle après erreur
- Réautoriser un retry seulement via action utilisateur explicite ou nouvelle condition d’entrée
- Contexte technique : React Native / Expo / store d’entitlements — 10-03-2026

---

<a id="risque-jest-rn-config-node"></a>
## Jest React Native — config node bloque les composants `.tsx`

### Risques

- `SyntaxError: Cannot use import statement outside a module` lors de l’import d’un barrel `.ts` qui réexporte des `.tsx`
- Impossible d’importer des composants React Native dans les tests — JSX non transformé

### Symptômes

- Erreur de syntaxe inattendue au run des tests sur un fichier `.ts` qui importe un `.tsx`
- Les tests de tokens passent mais tout test touchant un composant échoue

### Bonnes pratiques / mitigations

- `transform: { ‘^.+\\.ts$’: ‘ts-jest’ }` ne transforme que `.ts` — pas `.tsx`
- **Pattern recommandé** : tester la logique pure (tokens, valeurs de style) dans `.spec.ts`, le rendu visuel dans `.spec.tsx` avec une config séparée (`@testing-library/react-native` + `babel-jest`)
- Exporter le `StyleSheet` de chaque composant pour le tester sans JSX (voir pattern dédié dans `10_frontend_patterns_valides.md`)
- Contexte technique : React Native / Jest / ts-jest — app-alexandrie 19-03-2026

---

<a id="risque-oauth-handler-vide"></a>
## Bouton OAuth présent mais handler vide après refacto UI

### Risques

- L’OAuth est silencieusement cassé sur le nouvel écran — zéro erreur au démarrage, zéro crash
- L’AC "toutes les fonctionnalités préservées" peut être coché alors que le bouton est mort

### Symptômes

- `<Button title="Google" onPress={() => {}} />` — handler vide après copie depuis un ancien écran
- OAuth fonctionnel sur l’écran précédent (`welcome.tsx`) mais absent sur le nouvel écran refactorisé

### Bonnes pratiques / mitigations

- Toute refacto UI qui introduit un bouton OAuth doit brancher le hook existant (`useGoogleAuth(onSuccess)`)
- Si la story exclut explicitement la fonctionnalité : soit le bouton n’apparaît pas, soit `disabled` avec un label explicite ("bientôt disponible")
- **Checklist review** : chercher `onPress={() => {}}` sur tous les boutons OAuth dans les écrans refactorisés
- Contexte technique : Expo Router / React Native — app-alexandrie story 0.3, 19-03-2026

---

<a id="risque-double-systeme-espacement"></a>
## Double système d’espacement dans un monorepo Expo

### Risques

- Deux échelles d’espacement coexistent avec des noms différents pour des valeurs identiques (`Spacing.three = 16` vs `spacing.base = 16`)
- L’audit "zéro hardcode" ne détecte pas l’inconsistance car les deux sont des constantes nommées
- Les deux échelles peuvent diverger silencieusement

### Symptômes

- `import { Spacing } from ‘@/constants/theme’` coexiste avec `import { spacing } from ‘@/theme’`
- Certains screens refactorisés utilisent l’ancien système sans que personne ne le détecte

### Bonnes pratiques / mitigations

- Dès la création de `src/theme/spacing.ts`, supprimer ou vider `constants/theme.ts` (sauf constantes vraiment spécifiques : `MaxContentWidth`, `BottomTabInset`)
- Faire un `grep from ‘@/constants/theme’` à chaque story pour détecter les usages résiduels
- **Cause racine** : le template Expo génère `constants/theme.ts` avec `Spacing = { one, two, three... }` — à purger explicitement lors de la story design tokens
- Contexte technique : Expo / React Native — app-alexandrie story 0.5, 19-03-2026

---

<a id="risque-dimensions-image-via-spacing"></a>
## Dimensions d’image via tokens `spacing` (React Native)

### Risques

- Si `spacing.huge` change pour une raison d’espacement, la taille de l’image change silencieusement
- Régression visuelle sans que personne ne réalise l’impact — les deux changements semblent indépendants

### Symptômes

- `width: spacing.huge, height: spacing.huge` pour une image dont la taille est fixée par la spec Figma

### Bonnes pratiques / mitigations

```typescript
// Correct : constante locale ou token dédié
const THUMBNAIL_SIZE = 48; // Figma spec node 1-16147

// OU token dans un fichier sizes.ts dédié si la valeur est partagée
export const sizes = { thumbnail: 48, avatar: 40 } as const;
```

**Règle** : `spacing` = espacement entre éléments. `sizes` ou constantes locales = dimensions de composants.

- Contexte technique : React Native / design tokens — app-alexandrie story 0.4, 19-03-2026

---

<a id="risque-store-vide-deep-link"></a>
## Écran détail Expo Router — store vide en deep link / reload

### Risques

- L’écran détail (`[slug].tsx`) lit ses données depuis un store Zustand peuplé par l’écran liste
- En deep link, kill + reopen ou navigation OS back, le store est vide → "introuvable" affiché à tort

### Symptômes

- Écran détail vide ou erreur "non trouvé" sur accès direct (pas via la liste)
- Fonctionne normalement en navigation standard mais échoue sur reload

### Bonnes pratiques / mitigations

```typescript
// useEffect de secours dans l’écran détail
useEffect(() => {
  if (!accessToken) return;
  if (items.length > 0 || isLoading || errorState) return;
  void fetchItems(accessToken);
}, [accessToken, items.length, isLoading, errorState, fetchItems]);
```

- Ne pas afficher "introuvable" avant d’avoir vérifié que le store a bien été peuplé
- Contexte technique : Expo Router / Zustand — app-alexandrie story 4.1, 20-03-2026

---

<a id="risque-useeffect-guard-incomplet"></a>
## `useEffect` fetch — guard incomplet sur les états terminaux

### Risques

- Si l’état "zéro résultat intentionnel" (ex : `paywallRequired`) n’est pas dans les conditions de court-circuit, le fetch est re-déclenché à chaque re-render ou focus
- Boucle de fetch infini sur un état métier normal

### Symptômes

- `forums.length === 0` et `isLoading === false` → le guard ne court-circuite pas → fetch re-déclenché en boucle
- Visible en focus sur l’écran depuis un autre onglet

### Bonnes pratiques / mitigations

```typescript
// ❌ Pattern à risque — re-fetch si paywallRequired (forums vide + isLoading false)
if (forums.length > 0 || isLoading) return;

// ✅ Pattern correct — court-circuit sur l’état terminal
if (forums.length > 0 || isLoading || paywallRequired) return;
```

**Règle** : les états "zéro résultat intentionnel" (liste vide + flag métier) doivent être traités comme "données présentes" dans le guard de fetch.

- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie story 4.1, 20-03-2026

---

<a id="risque-zustand-collection-sans-cle-contexte"></a>
## Store Zustand : collections sans clé de contexte (navigation inter-contexte)

### Risques

- Un store qui stocke des collections dépendant d'un paramètre de navigation (forumSlug, threadId...) sans stocker ce paramètre affiche des données périmées lors d'une navigation inter-contexte

### Symptômes

- Naviguer du forum A vers le forum B affiche encore les catégories/threads du forum A
- Guard `if (items.length > 0) return` empêche le rechargement lors d'un changement de contexte

### Bonnes pratiques / mitigations

- Stocker la clé de contexte avec les données : `categoriesForumSlug: string | null`
- Invalider si `categoriesForumSlug !== currentForumSlug` avant de retourner depuis le cache
- Ou supprimer le guard et dépendre uniquement du changement de paramètre dans le `useEffect`

- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie 23-03-2026

---

<a id="risque-usesearchparams-sans-suspense"></a>
## `useSearchParams()` sans `Suspense` casse le build Next.js App Router

### Risques

- Un composant client utilisant `useSearchParams()` peut provoquer un échec de prerender/build s'il est rendu sans boundary `Suspense` depuis la page/layout serveur

### Symptômes

- `Error: useSearchParams() should be wrapped in a suspense boundary` au `next build`
- Fonctionne en dev mais échoue à la CI/CD

### Bonnes pratiques / mitigations

- Isoler le composant client qui utilise `useSearchParams()` et le rendre sous `<Suspense fallback={...}>` au niveau de la page
- Ne jamais appeler `useSearchParams()` directement dans un composant rendu sans `Suspense` depuis un Server Component

- Contexte technique : Next.js App Router récent / Turbopack — app-template-resto 16-03-2026

---

<a id="risque-type-viewdata-duplique"></a>
## Type `ViewData` dupliqué entre couche serveur et composant UI (Next.js)

### Risques

- TypeScript accepte deux structures identiques par structural typing — si le type source évolue, la couche UI reste désynchronisée sans erreur de compilation tant que les formes correspondent

### Symptômes

- Deux définitions du même type dans `src/server/` et `src/app/`
- Champ ajouté côté serveur mais absent dans le composant UI sans warning

### Bonnes pratiques / mitigations

```typescript
// ✅ La couche UI importe et re-exporte
export type { PublicHomeViewData } from "@/server/public/getPublicHomeData";

// ❌ À éviter — redéfinition locale
export type PublicHomeViewData = { tenantName: string; ... };
```

- Règle : le type appartient à la couche qui le produit. La couche UI importe uniquement.

- Contexte technique : Next.js App Router / TypeScript — app-template-resto 16-03-2026

---

<a id="risque-composant-react-fichier-ts"></a>
## Composant React dans un fichier `.ts` — `React.createElement` workaround

### Risques

- Code illisible vs JSX natif
- Fausse impression que le fichier est "sans JSX" — peut tromper les outils de linting et les reviewers
- Empêche l'utilisation de la syntaxe JSX si on doit ajouter des enfants complexes

### Symptômes

- `React.createElement(...)` dans un fichier `.ts`

### Bonnes pratiques / mitigations

- Tout fichier exportant une fonction retournant un `ReactElement` ou utilisant React doit avoir l'extension `.tsx`
- Sans exception

- Contexte technique : TypeScript / React — app-template-resto 16-03-2026

---

<a id="risque-double-validation-segment-app-router"></a>
## Double validation de segment dynamique App Router (layout + page)

### Risques

- Si le layout fait `notFound()` sur un segment invalide ET que la page répète la même condition, les deux deviennent désynchronisés silencieusement lors d'une modification

### Symptômes

- Même condition de validation dans `layout.tsx` et `page.tsx` d'un même segment
- Modification du layout n'est pas reportée dans la page (comportement divergent)

### Bonnes pratiques / mitigations

- Si le layout garde, la page consomme — une seule responsabilité par couche
- La page doit faire confiance à son layout parent
- **Règle** : un seul composant est responsable de la garde sur un segment dynamique

- Contexte technique : Next.js App Router — app-template-resto 17-03-2026

---

<a id="risque-faux-test-negatif"></a>
## Faux test négatif — tester le helper au lieu de tester l'exclusion

### Risques

- Un test nommé "X n'utilise pas Y" qui appelle Y en interne est un test normal mal documenté, pas un test d'exclusion
- Donne une fausse confiance sur le comportement par défaut du helper

### Symptômes

- Test intitulé "sans fallback, la valeur EN vide n'est pas remplacée" mais qui appelle le helper avec fallback activé

### Bonnes pratiques / mitigations

- Un vrai test négatif vérifie que X n'importe pas Y, ou que le comportement par défaut empêche l'effet indésirable
- Pour un helper à fallback optionnel : tester explicitement le cas `fallbackToFr=false` (défaut) avec une valeur vide

- Contexte technique : TypeScript / Jest — app-template-resto 17-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-consent-state-false-ambigu"></a>
## Consent state : `false` ambigu entre "pas de décision" et "refus explicite"

### Risques

- Sans champ `decided`, `analytics: false` peut signifier "première visite" ou "refus explicite" — indistinguables
- Le banner de consentement réapparaît à chaque visite après un refus, violant l'AC de persistance du choix

### Symptômes

- Banner qui réapparaît après rechargement malgré un refus explicite

### Bonnes pratiques / mitigations

```typescript
type ConsentState = {
  analytics: boolean;
  decided: boolean; // true = l'utilisateur a fait un choix (cookie présent)
};

const DEFAULT: ConsentState = { analytics: false, decided: false };

// À la lecture du cookie :
if (!cookieValue) return DEFAULT; // decided=false (première visite)
return { analytics: parsed.analytics, decided: true };
```

- L'état initial du banner doit être `!decided`, pas `!analytics`

- Contexte technique : Next.js / cookies — app-template-resto 21-03-2026

---

<a id="risque-script-inline-interpolation-directe"></a>
## Script inline : interpolation directe au lieu de `JSON.stringify`

### Risques

- Injection XSS potentielle via une valeur de configuration interpolée directement dans un `<Script>` inline
- La regex de validation en amont peut évoluer et laisser passer des valeurs dangereuses

### Symptômes

- `` {`gtag('config', '${measurementId}');`} `` — interpolation directe sans échappement

### Bonnes pratiques / mitigations

```tsx
// ❌ Anti-pattern — interpolation directe
{`gtag('config', '${measurementId}');`}

// ✅ Pattern correct — JSON.stringify garantit l'échappement
{`gtag('config', ${JSON.stringify(measurementId)});`}
```

- S'applique aussi aux `dangerouslySetInnerHTML` et aux attributs `data-*` injectés en JS

- Contexte technique : Next.js / `<Script>` — app-template-resto 21-03-2026

---

<a id="risque-window-location-reload-nextjs"></a>
## Next.js App Router : `window.location.reload()` au lieu de `router.refresh()`

### Risques

- Full reload = perd l'état React, navigation complète, plus lent
- `router.refresh()` est l'outil idoine : retrigger le fetch des Server Components sans détruire l'état client

### Symptômes

- `window.location.reload()` après un Server Action dans un Client Component
- Flash de rechargement visible, perte de l'état local (scroll, focus, état de formulaire)

### Bonnes pratiques / mitigations

```tsx
// ❌ Anti-pattern — full reload
await createCategoryAction(formData);
window.location.reload();

// ✅ Pattern correct — RSC diff, préserve l'état client
const router = useRouter();
await createCategoryAction(formData);
router.refresh();
```

- `router.refresh()` refetch uniquement les Server Components affectés (via `revalidatePath`) et applique un diff. L'état des Client Components est préservé.

- Contexte technique : Next.js App Router — app-template-resto 21-03-2026

---

<a id="risque-usetransition-snapshot-apres-setstate"></a>
## `useTransition` + optimistic update : snapshot capturé après `setState`

### Risques

- Stale closure classique : le snapshot est capturé après `setState`, donc `categories` peut déjà référencer la nouvelle liste au moment du rollback

### Symptômes

- Rollback optimiste qui ne restaure pas l'ancienne valeur
- Après une erreur serveur, l'état reste sur la nouvelle liste au lieu de revenir à l'état précédent

### Bonnes pratiques / mitigations

```tsx
// ❌ Anti-pattern — snapshot capturé après setState
const newList = [...categories];
setCategories(newList);
startTransition(async () => {
  try { await action(); }
  catch { setCategories(categories); } // peut être newList
});

// ✅ Pattern correct — snapshot AVANT toute mutation d'état
const snapshot = categories; // capturer AVANT setCategories
setCategories(newList);
startTransition(async () => {
  try { await action(); }
  catch { setCategories(snapshot); } // rollback garanti
});
```

- **Règle** : toujours assigner le snapshot dans un `const` **avant** le premier `setState`

- Contexte technique : React / Next.js App Router — app-template-resto 21-03-2026

---

<a id="risque-window-confirm-react"></a>
## `window.confirm()` dans une app React/Next.js

### Risques

- Bloque le thread principal
- Ne fonctionne pas en SSR
- Non stylable, UX mobile mauvaise

### Symptômes

- `if (!confirm("Supprimer ?")) return;` dans un Client Component

### Bonnes pratiques / mitigations

```tsx
// ❌ Anti-pattern
if (!confirm("Supprimer ?")) return;

// ✅ Pattern correct — confirmation inline via état React
const [deletingId, setDeletingId] = useState<string | null>(null);

{deletingId === item.id && (
  <div>
    <span>Supprimer « {item.label} » ?</span>
    <button onClick={() => { setDeletingId(null); doDelete(item.id); }}>Confirmer</button>
    <button onClick={() => setDeletingId(null)}>Annuler</button>
  </div>
)}
```

- S'applique aussi à `window.alert()` et `window.prompt()`

- Contexte technique : React / Next.js — app-template-resto 21-03-2026

---

<a id="risque-import-type-server-composant-client"></a>
## `import type` depuis `src/server/**` dans un composant client

### Risques

- Violation de boundary même si l'import est type-only (effacé à la compilation)
- Ouvre la porte à des imports runtime si le code est refactoré rapidement
- La règle ESLint `no-restricted-imports` doit couvrir les `import type` aussi

### Symptômes

- `import type { Foo } from "@/server/..."` dans un fichier `"use client"`
- Passe en review car le compilateur ne bloque pas les type-only imports

### Bonnes pratiques / mitigations

- Types partagés entre server et client doivent vivre dans `src/types/` ou `src/lib/`
- Configurer `no-restricted-imports` avec `allowTypeImports: false` pour les paths serveur

- Contexte technique : Next.js App Router / TypeScript — app-template-resto 22-03-2026

---

<a id="risque-inline-styles-dashboard"></a>
## Inline styles dans les composants dashboard

### Risques

- Contourne le système Tailwind + tokens CSS
- Crée des incohérences visuelles non détectées par le linter

### Symptômes

- `style={{ color: '#123456', marginTop: 8 }}` dans un composant dashboard

### Bonnes pratiques / mitigations

- Bloquer en code review systématiquement tout `style={{...}}` dans les composants dashboard
- Exception acceptable uniquement : animations CSS dynamiques (valeurs calculées au runtime)

- Contexte technique : React / Tailwind — app-template-resto 22-03-2026

---

<a id="risque-tailwind-classes-invalides"></a>
## Classes Tailwind invalides courantes (bugs silencieux)

### Risques

- Classes Tailwind invalides sont silencieusement ignorées — aucun warning, comportement visuellement cassé

### Symptômes

- Item masqué affiché à pleine opacité (`opacity-55` → invalide)
- Largeur incorrecte (`w-35` → invalide)

### Bonnes pratiques / mitigations

Erreurs courantes :
- `opacity-55` → invalide. Scale : 0/5/10/20/25/30/40/50/60/70/75/80/90/95/100 → utiliser `opacity-50` ou `opacity-60`
- `w-35` → invalide. Scale saute de `w-32` à `w-36` → utiliser `w-36`
- `box-border` → redondant. Tailwind Preflight applique déjà `box-sizing: border-box` globalement

- Toujours vérifier les classes custom/non-standard avec l'extension Tailwind IntelliSense

- Contexte technique : Tailwind CSS — app-template-resto 22-03-2026

---

<a id="risque-img-natif-nextjs"></a>
## Next.js : `<img>` natif interdit dans les composants

### Risques

- Warning ESLint `@next/next/no-img-element` → avec `--max-warnings=0` : erreur CI
- Pas de lazy loading, pas d'optimisation WebP, risque de layout shift (CLS)

### Symptômes

- `<img src="..." />` dans un composant Next.js

### Bonnes pratiques / mitigations

- Toujours utiliser `<Image>` de `next/image` à la place
- Exception acceptable : composants de test ou storybook uniquement

- Contexte technique : Next.js / ESLint — app-template-resto 22-03-2026

---

<a id="risque-usetransition-global-liste-items"></a>
## `useTransition` global pour des listes d'items interactifs

### Risques

- `isPending` global désactive **tous** les boutons de tous les items pendant qu'une opération est en cours sur un seul item
- Sur mobile : UX bloquée, impossible d'agir pendant qu'une autre opération tourne

### Symptômes

- Clic sur "Masquer" pour l'item A → boutons des items B et C grisés

### Bonnes pratiques / mitigations

```tsx
// ❌ Avant — bloque tout
const [isPending, startTransition] = useTransition();
// render : disabled={isPending}

// ✅ Après — per-item
const [pendingId, setPendingId] = useState<string | null>(null);

function handleToggle(id: string) {
  setPendingId(id);
  (async () => {
    try { await toggleAction(id); }
    catch (err) { handleError(err); }
    finally { setPendingId(null); }
  })();
}
// render : disabled={pendingId === item.id}
```

**Règles :**
- `pendingId === item.id` pour les boutons d'item (désactive uniquement l'item en cours)
- `pendingId !== null` pour les boutons globaux (ex: "Ajouter")
- `finally` garantit la réinitialisation même en cas d'erreur

- Contexte technique : React / Next.js — app-template-resto 22-03-2026

---

<a id="risque-usecallback-inutile-inline"></a>
## `useCallback` inutile quand le callback est wrappé en inline au render

### Risques

- Le handler stable est re-wrappé dans une arrow inline lors du passage en prop → nouvelle référence à chaque render → `React.memo` ne peut pas éviter le re-render

### Symptômes

```tsx
const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓

// Mais au render :
<ItemCard onToggle={() => handleToggle(item.id)} />
//                 ↑ nouvelle closure à chaque render → memo inutile
```

### Bonnes pratiques / mitigations

- `useCallback` n'a de valeur que si le callback est passé **directement** en prop, sans re-wrapping
- Si la signature doit capturer des variables de boucle, deux options :
  1. Passer les données en props et laisser l'enfant appeler le handler avec ses propres props
  2. Accepter que `memo` ne soit pas protégé et supprimer le `useCallback` inutile
- Ne pas laisser un `useCallback` "pour faire bien" si son effet réel est nul

- Contexte technique : React — app-template-resto 22-03-2026

---

<a id="risque-formulaire-defaultvalue-sans-key"></a>
## Formulaire React avec `defaultValue` sans `key` prop

### Risques

- `defaultValue`, `defaultChecked`, `defaultSelected` ne s'appliquent qu'au montage
- Si le composant est réutilisé (même nœud DOM, nouvelle prop) sans être démonté, les valeurs ne se mettent pas à jour

### Symptômes

- L'utilisateur édite l'entité A, clique sur "Modifier" pour l'entité B → le formulaire affiche encore les données de A

### Bonnes pratiques / mitigations

```tsx
// Fix obligatoire : key unique basée sur l'ID de l'entité éditée
<EntityForm
  key={formState.mode === "edit" ? formState.entity.id : `create-${formState.contextId}`}
  ...
/>
```

- **Règle** : tout formulaire d'édition réutilisé pour plusieurs entités doit avoir une `key` distincte par entité

- Contexte technique : React / Next.js — app-template-resto 21-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
