# 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

---

<a id="risque-correlation-multi-onglets-sessionstorage"></a>
## Corrélation d'un retour multi-onglets : `localStorage`, pas `sessionStorage`

### Risques

- Un flux qui ouvre un NOUVEL onglet (checkout Stripe web, OAuth popup) devant renvoyer un signal corrélé (nonce anti-rejeu) pose le nonce en `sessionStorage`
- `sessionStorage` n'est PAS partagé entre onglets : l'onglet de retour a un `sessionStorage` vide → le nonce n'est jamais transmis → la validation doit être tolérante (accepte les messages sans nonce) donc **inopérante**

### Symptômes

- La corrélation anti-message-forgé devient une no-op silencieuse ; impossible d'exiger la présence du nonce

### Bonnes pratiques / mitigations

- Stocker le nonce en `localStorage` (partagé same-origin entre onglets), jamais en `sessionStorage`
- Avec `localStorage`, le nonce est réellement lisible côté retour → on peut **EXIGER** sa présence (mode strict) au lieu de la tolérer ; nettoyer la clé après usage
- `BroadcastChannel` reste le canal de transport (same-origin) mais ne porte pas l'état de session — d'où le besoin du storage partagé
- Contexte technique : Next.js / web — app-alexandrie (review ux-parcours-1/8 + bo-2), 04-06-2026
