Refonte Structure

This commit is contained in:
MaksTinyWorkshop
2026-03-25 08:32:13 +01:00
parent d8a947eb79
commit 9b7af9f1b0
55 changed files with 4743 additions and 4906 deletions

View File

@@ -0,0 +1,378 @@
# Frontend — Risques & vigilance : Next.js
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
---
<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-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-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-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-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-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