Files
_Assistant_Lead_Tech/10_frontend_risques_et_vigilance.md
openclaw d8a947eb79 Lead_tech: intégrer capitalisations (24-03-2026)
Backend — Risques & vigilance:
- Code d’erreur générique sur 409 (conflict) — +index/+ancre/+section
- Tests e2e d’autorisation avec buildApp isolé — +index/+ancre/+section
- MAJ date

Frontend — Risques & vigilance:
- Guard de rôle via return conditionnel dans le render — +index/+ancre/+section
- Méthodes Zustand sans rethrow — +index/+ancre/+section
- Regex globale singleton (/g) — +index/+ancre/+section
- MAJ date

Divers:
- Purge 95_a_capitaliser.md (tampon vidé)
2026-03-24 12:32:51 +01:00

40 KiB
Raw Blame History

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 : 24-03-2026


Règles dutilisation

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

Index


Auth côté client (mauvaise séparation des responsabilités)

Risques

  • Le front “décide” des permissions au lieu dappliquer un contrat backend
  • Affichage dactions interdites / fuite dinformations dans lUI
  • Tokens stockés de façon dangereuse (XSS)

Symptômes

  • Différences entre “ce que lUI permet” et “ce que lAPI 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 lUI ≠ 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)

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 derreur 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 lepic 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” nest jamais rafraîchi
  • Code review qui force un refactor vers un store/cache au milieu dun 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)

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

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

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 dun état dégradé sur toute réponse 2xx

Risques

  • Le client sort trop tôt dun 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
  • LUI 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é quaprès une requête décriture réussie
  • Exclure GET et HEAD de la logique de reset
  • Conserver le mode dégradé tant quaucune mutation na prouvé le retour à la normale
  • Contexte technique : React Native / Expo — 10-03-2026

Refresh store en fire-and-forget après mutation

Risques

  • LUI 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 jusquau prochain reload

Bonnes pratiques / mitigations

  • await explicite du refresh si lUI dépend du résultat
  • Gestion derreur dédiée sur la phase de resynchronisation
  • Nutiliser le fire-and-forget que pour un effet secondaire réellement non bloquant
  • Contexte technique : React Native / Expo — 10-03-2026

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
  • Lutilisateur ne voit ni état derreur 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 dentré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 dentrée
  • Contexte technique : React Native / Expo / store dentitlements — 10-03-2026

Jest React Native — config node bloque les composants .tsx

Risques

  • SyntaxError: Cannot use import statement outside a module lors de limport dun barrel .ts qui réexporte des .tsx
  • Impossible dimporter 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

Bouton OAuth présent mais handler vide après refacto UI

Risques

  • LOAuth est silencieusement cassé sur le nouvel écran — zéro erreur au démarrage, zéro crash
  • LAC "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 napparaî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

Double système despacement dans un monorepo Expo

Risques

  • Deux échelles despacement coexistent avec des noms différents pour des valeurs identiques (Spacing.three = 16 vs spacing.base = 16)
  • Laudit "zéro hardcode" ne détecte pas linconsistance 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 lancien 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

Dimensions dimage via tokens spacing (React Native)

Risques

  • Si spacing.huge change pour une raison despacement, la taille de limage change silencieusement
  • Régression visuelle sans que personne ne réalise limpact — 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

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

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

// 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 davoir vérifié que le store a bien été peuplé
  • Contexte technique : Expo Router / Zustand — app-alexandrie story 4.1, 20-03-2026

useEffect fetch — guard incomplet sur les états terminaux

Risques

  • Si létat "zéro résultat intentionnel" (ex : paywallRequired) nest 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

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

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


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


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

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


Composant React dans un fichier .tsReact.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


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


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


É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

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

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


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

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


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

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


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

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


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

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


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


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


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


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


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

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


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

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


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

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


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


Guard de rôle via return conditionnel dans le render

Risques

  • Flash “Accès refusé” pendant le chargement du store auth (user === null au montage)
  • UX instable (re-render successif qui corrige le faux négatif)

Symptômes

  • if (user?.role !== "ADMIN") return <AccessDenied /> directement dans le composant

Bonnes pratiques / mitigations

  • Router côté client après détermination du rôle, et rendre un placeholder tant que létat est inconnu
useEffect(() => {
  if (user !== null && user.role !== 'ADMIN') router.replace('/(tabs)');
}, [user, router]);

return user === null || user.role !== 'ADMIN' ? <View /> : <AdminScreen />;

Méthodes Zustand qui avalent les erreurs (sans rethrow)

Risques

  • Erreurs métier (ex: UNSAFE_LINK) invisibles pour lécran → pas de feedback utilisateur

Symptômes

  • try/catch dans le store qui ne relance pas lerreur et ne met pas à jour un état derreur

Bonnes pratiques / mitigations

  • Relancer lerreur pour que lécran gère lUX, ou stocker explicitement lerreur dans le state (selon design)
async updateThread(forumSlug, threadId, body) {
  try {
    await communityService.updateThread(accessToken, forumSlug, threadId, body);
  } catch (e) {
    const err = e as Error & { code?: string };
    throw err; // ou set({ error: err.message }) si lUX lexige
  }
}

Regex globale singleton (/g) partagée au niveau module

Risques

  • État lastIndex partagé si la regex est réutilisée avec .test()/.exec() → bugs stateful difficiles à diagnostiquer

Symptômes

  • Regex définie en constante module: const LINK_PATTERN = /https?:\/\/\S+/gi;
  • Fonction qui “marche” avec replace mais casse après un refactor qui ajoute .test()

Bonnes pratiques / mitigations

  • Créer une instance nouvelle par appel (factory), éviter le singleton module pour /g ou /y
function makeLinkPattern(): RegExp { return /https?:\/\/\S+/gi; }
function processLinks(content: string) { return content.replace(makeLinkPattern(), ...); }