mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Capitalisation complète — app-alexandrie & app-template-resto (23-03-2026)
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>
This commit is contained in:
@@ -7,7 +7,7 @@ Ce fichier recense des risques front-end susceptibles de provoquer :
|
||||
- dette technique rapide,
|
||||
- régressions UX/perf/a11y.
|
||||
|
||||
Dernière mise à jour : 20-03-2026
|
||||
Dernière mise à jour : 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
@@ -39,6 +39,26 @@ Dernière mise à jour : 20-03-2026
|
||||
- [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)
|
||||
|
||||
---
|
||||
|
||||
@@ -440,3 +460,553 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user