12 KiB
Frontend — Risques & vigilance : Next.js
Extrait de la base de connaissance Lead_tech. Voir
knowledge/frontend/risques/README.mdpour l'index complet.
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
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
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
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
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
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