mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Refonte Structure
This commit is contained in:
378
knowledge/frontend/risques/nextjs.md
Normal file
378
knowledge/frontend/risques/nextjs.md
Normal 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
|
||||
Reference in New Issue
Block a user