Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local
95_a_capitaliser.md remis à l'état initial vide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
36 KiB
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
- Erreurs silencieuses / écrans blancs
- Mélange server state / client state
- Appels API en state local d’écran
- Performances : sur-renders + bundle
- Accessibilité oubliée (a11y)
- Catch silencieux — erreur inconnue sans feedback utilisateur
- Auto-reset d’un état dégradé sur toute réponse 2xx
- Refresh store en fire-and-forget après mutation
- Loading infini sur écran gated par droits distants
- Jest React Native — config node bloque les composants
.tsx - Bouton OAuth présent mais handler vide après refacto UI
- Double système d'espacement dans un monorepo Expo
- Dimensions d'image via tokens
spacing(React Native) - Écran détail Expo Router — store vide en deep link / reload
useEffectfetch — guard incomplet sur les états terminaux- Store Zustand : collections sans clé de contexte (navigation inter-contexte)
useSearchParams()sansSuspensecasse le build Next.js App Router- Type
ViewDatadupliqué entre couche serveur et composant UI (Next.js) - Composant React dans un fichier
.ts—React.createElementworkaround - Double validation de segment dynamique App Router (layout + page)
- Faux test négatif — tester le helper au lieu de tester l'exclusion
- État booléen UI dérivé hardcodé au lieu d'être calculé depuis le store
- Flag
isLoadingunique pour des opérations de natures différentes - Consent state :
falseambigu entre "pas de décision" et "refus explicite" - Script inline : interpolation directe au lieu de
JSON.stringify - Next.js App Router :
window.location.reload()au lieu derouter.refresh() useTransition+ optimistic update : snapshot capturé aprèssetStatewindow.confirm()dans une app React/Next.jsimport typedepuissrc/server/**dans un composant client- Inline styles dans les composants dashboard
- Classes Tailwind invalides courantes (bugs silencieux)
- Next.js :
<img>natif interdit dans les composants useTransitionglobal pour des listes d'items interactifsuseCallbackinutile quand le callback est wrappé en inline au render- Formulaire React avec
defaultValuesanskeyprop
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)
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/dataet 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
catchqui 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
catchdoit avoir une brancheelse(oudefault) 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
isReadOnlyouisDegraded - 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
GETetHEADde 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
awaitexplicite 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
Loading infini sur écran gated par droits distants
Risques
- Un écran protégé reste bloqué dans un faux
loadingaprè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
entitlementsou autorisations laissés ànullaprès erreuruseEffectou logique d’entrée qui retrigger le fetch à chaque rendu
Bonnes pratiques / mitigations
- Distinguer explicitement
loading,error,ready - Ne pas réutiliser
nullcomme é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
Jest React Native — config node bloque les composants .tsx
Risques
SyntaxError: Cannot use import statement outside a modulelors de l’import d’un barrel.tsqui 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
.tsqui 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.tsxavec une config séparée (@testing-library/react-native+babel-jest) - Exporter le
StyleSheetde chaque composant pour le tester sans JSX (voir pattern dédié dans10_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
- 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
disabledavec 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 d’espacement dans un monorepo Expo
Risques
- Deux échelles d’espacement coexistent avec des noms différents pour des valeurs identiques (
Spacing.three = 16vsspacing.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 avecimport { 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 viderconstants/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.tsavecSpacing = { 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 d’image via tokens spacing (React Native)
Risques
- Si
spacing.hugechange 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.hugepour 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
É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
// 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
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 === 0etisLoading === 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) returnempê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 !== currentForumSlugavant 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 boundarySuspensedepuis la page/layout serveur
Symptômes
Error: useSearchParams() should be wrapped in a suspense boundaryaunext 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 sansSuspensedepuis 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/etsrc/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 .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
ReactElementou 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.tsxetpage.tsxd'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é àfalseen 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
Consent state : false ambigu entre "pas de décision" et "refus explicite"
Risques
- Sans champ
decided,analytics: falsepeut 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
dangerouslySetInnerHTMLet aux attributsdata-*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 (viarevalidatePath) 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, donccategoriespeut 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
constavant le premiersetState -
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()etwindow.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-importsdoit couvrir lesimport typeaussi
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/ousrc/lib/ -
Configurer
no-restricted-importsavecallowTypeImports: falsepour 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 → utiliseropacity-50ouopacity-60 -
w-35→ invalide. Scale saute dew-32àw-36→ utiliserw-36 -
box-border→ redondant. Tailwind Preflight applique déjàbox-sizing: border-boxglobalement -
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>denext/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
isPendingglobal 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.idpour les boutons d'item (désactive uniquement l'item en cours) -
pendingId !== nullpour les boutons globaux (ex: "Ajouter") -
finallygarantit 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.memone 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
-
useCallbackn'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 :
- Passer les données en props et laisser l'enfant appeler le handler avec ses propres props
- Accepter que
memone soit pas protégé et supprimer leuseCallbackinutile
-
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,defaultSelectedne 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
keydistincte par entité -
Contexte technique : React / Next.js — app-template-resto 21-03-2026