Pourquoi Next.js 16 change votre façon de coder ?
Next.js est un framework React qui permet de créer des applications web modernes avec une excellente performance et une expérience développeur optimale. La version 16 apporte des améliorations significatives en termes de rendu et d'optimisation.
Pourquoi Next.js ?
Next.js résout les défis majeurs du développement React moderne en offrant plusieurs modes de rendu adaptés à chaque besoin.
- Performance : Rendu côté serveur pour un temps de chargement initial rapide
- SEO : Contenu prérendu pour une meilleure indexation
- Flexibilité : Choix du mode de rendu par page ou composant
- DX : Hot reload, TypeScript, routing automatique
1// Structure d'un projet Next.js 162app/3├── layout.tsx // Layout racine4├── page.tsx // Page d'accueil5├── about/6│ └── page.tsx // Route /about7└── blog/8 ├── page.tsx // Liste des articles9 └── [slug]/10 └── page.tsx // Article individuel11
12// Exemple de page simple13export default function Home() {14 return (15 <main>16 <h1>Bienvenue sur Next.js 16</h1>17 </main>18 );19}Quand utiliser le SSR plutôt que le SSG ?
Le Server-Side Rendering (SSR) génère le HTML sur le serveur à chaque requête. Idéal pour le contenu dynamique et personnalisé.
Comment fonctionne le SSR ?
À chaque requête, Next.js exécute le composant côté serveur, récupère les données nécessaires, et envoie le HTML complet au client.
Client demande une page
Serveur exécute le composant React
Données récupérées (API, DB)
HTML complet envoyé au client
1// Page SSR avec données dynamiques2export default async function UserProfile({3 params4}: {5 params: Promise<{ id: string }>6}) {7 const { id } = await params;8
9 // Cette fonction s'exécute côté serveur à chaque requête10 const user = await fetch(`https://api.example.com/users/${id}`, {11 cache: 'no-store' // Pas de cache, données toujours fraîches12 }).then(res => res.json());13
14 return (15 <div>16 <h1>{user.name}</h1>17 <p>Dernière connexion : {new Date(user.lastLogin).toLocaleString()}</p>18 </div>19 );20}21
22// Les composants sont Server Components par défaut dans Next.js 16Démo SSR en temps réel
Cliquez pour simuler une requête SSR. Notez que l'heure change à chaque chargement.
Comment générer des pages statiques performantes ?
La Static Site Generation (SSG) génère le HTML au moment du build. Parfait pour le contenu qui change rarement.
Build Time Generation
Les pages sont générées une seule fois pendant le build et servies comme fichiers statiques depuis un CDN.
- ⚡Ultra rapide : Pas de calcul serveur, juste du HTML statique
- 💰Économique : Hébergement CDN peu coûteux
- 🔒Sécurisé : Pas de serveur dynamique à protéger
1// Page statique générée au build2export default async function BlogPost({3 params4}: {5 params: Promise<{ slug: string }>6}) {7 const { slug } = await params;8
9 // Cette fonction s'exécute UNE SEULE FOIS au build10 const post = await fetch(`https://api.example.com/posts/${slug}`, {11 cache: 'force-cache' // Cache permanent12 }).then(res => res.json());13
14 return (15 <article>16 <h1>{post.title}</h1>17 <div dangerouslySetInnerHTML={{ __html: post.content }} />18 </article>19 );20}21
22// Génère toutes les pages possibles au build23export async function generateStaticParams() {24 const posts = await fetch('https://api.example.com/posts')25 .then(res => res.json());26
27 return posts.map((post: { slug: string }) => ({28 slug: post.slug,29 }));30}Démo SSG
Simulez une page statique. L'heure reste fixe car elle a été générée au build.
Comment mettre à jour vos pages sans rebuild complet ?
L'Incremental Static Regeneration (ISR) combine le meilleur des deux mondes : vitesse du statique avec fraîcheur du dynamique.
Le meilleur des deux mondes
ISR sert une page statique instantanément, puis la régénère en arrière-plan selon un intervalle défini.
Première requête : génération au build
Requêtes suivantes : page statique en cache
Après X secondes : régénération en arrière-plan
Nouvelle version servie aux prochains visiteurs
1// Page ISR avec revalidation toutes les 60 secondes2export default async function ProductPage({3 params4}: {5 params: Promise<{ id: string }>6}) {7 const { id } = await params;8
9 // Revalidation toutes les 60 secondes10 const product = await fetch(`https://api.example.com/products/${id}`, {11 next: { revalidate: 60 } // Clé magique ISR12 }).then(res => res.json());13
14 return (15 <div>16 <h1>{product.name}</h1>17 <p className="text-2xl font-bold">{product.price} €</p>18 <p className="text-sm text-gray-500">19 Stock : {product.stock} unités20 </p>21 </div>22 );23}24
25// Ou revalidation à la demande (on-demand)26export async function generateStaticParams() {27 const products = await fetch('https://api.example.com/products')28 .then(res => res.json());29
30 return products.map((product: { id: string }) => ({31 id: product.id,32 }));33}Démo ISR
La page est mise en cache pendant 60 secondes. Après ce délai, elle se régénère en arrière-plan.
Dans quels cas privilégier le rendu côté client ?
Le Client-Side Rendering (CSR) envoie un HTML quasi-vide au navigateur. Le JavaScript se charge ensuite, fetch les données côté client, puis affiche le contenu final. Le serveur Next.js ne fait que servir des fichiers statiques.
Analogie : Le meuble IKEA
Imaginez recevoir un carton plat contenant toutes les pièces détachées d'un meuble. Le serveur vous envoie le carton (HTML vide + JS), puis vous assemblez tout chez vous (navigateur). Le meuble final apparaît seulement après assemblage complet.
Cycle de vie du CSR
Voici ce qui se passe réellement lorsqu'un utilisateur charge une page CSR dans Next.js.
<div id="root"></div>useEffect1'use client';2
3import { useState, useEffect } from 'react';4
5interface User {6 id: number;7 name: string;8 email: string;9}10
11export default function UsersPage() {12 const [users, setUsers] = useState<User[]>([]);13 const [loading, setLoading] = useState(true);14
15 // Fetch côté client au montage du composant16 useEffect(() => {17 async function fetchUsers() {18 try {19 const res = await fetch('/api/users');20 const data = await res.json();21 setUsers(data);22 } catch (error) {23 console.error('Erreur fetch:', error);24 } finally {25 setLoading(false);26 }27 }28
29 fetchUsers();30 }, []); // Exécuté une seule fois après le premier render31
32 if (loading) {33 return <div>Chargement des utilisateurs...</div>;34 }35
36 return (37 <div>38 <h1>Liste des utilisateurs</h1>39 <ul>40 {users.map((user) => (41 <li key={user.id}>42 {user.name} - {user.email}43 </li>44 ))}45 </ul>46 </div>47 );48}Caractéristiques de performance
Le CSR offre un compromis spécifique entre rapidité initiale et temps d'affichage final.
Quand utiliser le CSR ?
Le Client-Side Rendering brille dans des contextes spécifiques où l'interactivité prime sur le SEO.
- Dashboards privés - Zones protégées par authentification (pas besoin de SEO)
- Applications interactives - Outils, calculateurs, éditeurs en temps réel
- Pages avec données en temps réel - Chats, notifications, live feeds
- PWAs (Progressive Web Apps) - Applications web installables
Pièges à éviter avec CSR
'use client'. Privilégiez le rendu serveur par défaut et isolez uniquement les composants interactifs.useEffect qui dépendent les uns des autres. Cela crée des cascades de requêtes et ralentit la page. Privilégiez React Query ou SWR pour gérer le cache.Comment mixer rendering strategies intelligemment ?
Le mode Hybrid combine Server Components (SSR) et Client Components (CSR) dans la même page. Next.js 13+ permet de mixer stratégiquement les deux approches pour optimiser performance et interactivité.
Par défaut, tous les composants sont des Server Components. Seuls ceux marqués avec 'use client' deviennent des Client Components.
Analogie : Le gâteau décoré
Imaginez un gâteau préparé en cuisine (serveur) et livré déjà cuit. Le client reçoit le gâteau prêt à manger, mais peut ajouter du glaçage ou des décorations sur place. La base est servie toute faite, seules les finitions sont appliquées côté client.
Règle d'or du mode Hybrid
Maximum en Server Components, minimum en Client Components. Gardez le plus de logique possible côté serveur. Isolez uniquement les parties qui nécessitent de l'interactivité (événements, state, hooks React).
Pattern recommandé : Server Component parent
Structurez votre application avec des Server Components en racine et des Client Components isolés pour les interactions.
1// app/products/page.tsx (Server Component par défaut)2import { ProductList } from './product-list';3import { AddToCartButton } from './add-to-cart-button';4
5export default async function ProductsPage() {6 // Fetch côté serveur - données disponibles immédiatement7 const products = await fetch('https://api.example.com/products').then(r => r.json());8
9 return (10 <div>11 <h1>Nos produits</h1>12 {/* Server Component - rendu HTML statique */}13 <ProductList products={products} />14 </div>15 );16}17
18// app/products/product-list.tsx (Server Component)19import { AddToCartButton } from './add-to-cart-button';20
21export function ProductList({ products }: { products: Product[] }) {22 return (23 <ul>24 {products.map((product) => (25 <li key={product.id}>26 <h2>{product.name}</h2>27 <p>{product.price} €</p>28 {/* Client Component isolé - seulement pour l'interactivité */}29 <AddToCartButton productId={product.id} />30 </li>31 ))}32 </ul>33 );34}35
36// app/products/add-to-cart-button.tsx (Client Component)37'use client';38
39import { useState } from 'react';40
41export function AddToCartButton({ productId }: { productId: number }) {42 const [added, setAdded] = useState(false);43
44 const handleClick = () => {45 // Logique interactive côté client46 setAdded(true);47 // ... appel API pour ajouter au panier48 };49
50 return (51 <button onClick={handleClick}>52 {added ? 'Ajouté !' : 'Ajouter au panier'}53 </button>54 );55}Gérer les mismatches avec suppressHydrationWarning
Lorsque vous avez un contenu intentionnellement différent entre serveur et client (comme une date ou un thème), utilisez suppressHydrationWarning pour éviter les avertissements React.
1'use client';2
3export function CurrentTime() {4 const [time, setTime] = useState(new Date().toLocaleTimeString());5
6 useEffect(() => {7 const interval = setInterval(() => {8 setTime(new Date().toLocaleTimeString());9 }, 1000);10 return () => clearInterval(interval);11 }, []);12
13 // Supprime l'avertissement car le contenu serveur/client est volontairement différent14 return <div suppressHydrationWarning>{time}</div>;15}Optimiser la taille du bundle JavaScript
Placer la frontière 'use client' au plus bas niveau possible réduit la quantité de JavaScript envoyée au navigateur.
'use client' force l'envoi de tout le code au client, même les parties statiques.'use client'. Le reste du layout reste en Server Component (HTML pur, 0 JS).1// ❌ Mauvais : Tout le layout devient client2'use client';3
4export default function Layout({ children }: { children: React.ReactNode }) {5 const [menuOpen, setMenuOpen] = useState(false);6
7 return (8 <div>9 <Header />10 <button onClick={() => setMenuOpen(!menuOpen)}>Menu</button>11 {children}12 <Footer />13 </div>14 );15}16
17// ✅ Bon : Seul le bouton menu est client18import { MenuButton } from './menu-button';19
20export default function Layout({ children }: { children: React.ReactNode }) {21 return (22 <div>23 <Header />24 <MenuButton /> {/* Composant client isolé */}25 {children}26 <Footer />27 </div>28 );29}30
31// menu-button.tsx32'use client';33export function MenuButton() {34 const [menuOpen, setMenuOpen] = useState(false);35 return <button onClick={() => setMenuOpen(!menuOpen)}>Menu</button>;36}Quand utiliser le mode Hybrid ?
Le mode Hybrid est la solution par défaut pour la plupart des applications Next.js modernes.
- Layout + Header/Footer - Parties statiques en Server, navigation interactive en Client
- Blog avec commentaires - Articles en SSG, section commentaires en Client
- E-commerce - Liste produits en SSR, panier et filtres en Client
- Dashboard avec widgets - Layout en Server, graphiques interactifs en Client
Pièges à éviter avec Hybrid
'use client' sur un composant parent force tous ses enfants à devenir des Client Components, même s'ils n'ont pas besoin d'interactivité. Descendez la frontière au plus bas niveau possible.'use client'. Utilisez Route Handlers ou Server Actions pour les appels sécurisés.Résumé : Quand utiliser quoi ?
Choisissez la bonne stratégie selon vos besoins.
Quand marquer un composant "use client" ?
Les Client Components s'exécutent dans le navigateur et permettent l'interactivité. Utilisez-les uniquement quand nécessaire.
Quand utiliser 'use client' ?
Réservez les Client Components aux cas où vous avez besoin d'interactivité ou des APIs du navigateur.
- ✓Event handlers : onClick, onChange, onSubmit...
- ✓State & Effects : useState, useEffect, useContext...
- ✓Browser APIs : localStorage, window, navigator...
- ✓Librairies client-only : charting, animations...
1'use client'; // Directive obligatoire en haut du fichier2
3import { useState } from 'react';4
5export function Counter() {6 const [count, setCount] = useState(0);7
8 return (9 <div>10 <p>Compteur : {count}</p>11 <button onClick={() => setCount(count + 1)}>12 Incrémenter13 </button>14 </div>15 );16}17
18// ❌ Sans 'use client', cette erreur apparaît :19// "You're importing a component that needs useState.20// It only works in a Client Component but none of its21// parents are marked with 'use client'"Bonne Pratique
Gardez vos Server Components au plus haut niveau possible et ne passez en Client Component qu'à la dernière minute. Cela optimise le bundle JavaScript envoyé au client.
Démo Client Component
Ce composant s'exécute côté client et peut gérer l'interactivité en temps réel.
Comment charger du code uniquement quand nécessaire ?
Dynamic Import - Lazy Loading Stratégique
Le dynamic import permet de charger des composants ou bibliothèques à la demande, uniquement lorsqu'ils sont nécessaires, au lieu de tout inclure dans le bundle initial.
Analogie : Imaginez Netflix. Vous ne téléchargez pas tous les films avant de commencer à regarder. Vous streamez le contenu à la demande. Le dynamic import fonctionne de la même manière : il ne charge que ce dont vous avez besoin, au moment où vous en avez besoin.
Syntaxe de Base
1import dynamic from 'next/dynamic';2
3// Chargement lazy d'un composant lourd4const HeavyChart = dynamic(() => import('./components/heavy-chart'), {5 loading: () => <div>Chargement du graphique...</div>,6 ssr: false7});8
9export default function Dashboard() {10 return (11 <div>12 <h1>Tableau de bord</h1>13 {/* Le composant ne sera chargé que lorsqu'il sera rendu */}14 <HeavyChart data={chartData} />15 </div>16 );17}Impact sur le Bundle Size
Le dynamic import permet de réduire drastiquement la taille du bundle initial en déplaçant le code vers des chunks séparés chargés à la demande.
Quand Utiliser le Dynamic Import
Scénarios d'Utilisation Optimaux
Le dynamic import est particulièrement efficace dans les situations suivantes.
Exemple Complet avec Options
1import dynamic from 'next/dynamic';2import { Suspense } from 'react';3
4// Composant lourd chargé dynamiquement5const RichTextEditor = dynamic(6 () => import('@/components/rich-text-editor'),7 {8 // État de chargement personnalisé9 loading: () => (10 <div className="h-64 flex items-center justify-center bg-muted/20 rounded-lg">11 <div className="text-center">12 <div className="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full mx-auto" />13 <p className="mt-2 text-sm text-muted-foreground">Chargement de l'éditeur...</p>14 </div>15 </div>16 ),17 // Désactiver le SSR si le composant utilise des APIs navigateur18 ssr: false19 }20);21
22export default function ArticleEditor() {23 return (24 <div className="space-y-6">25 <h1>Créer un Article</h1>26
27 {/* Le composant ne sera chargé qu'au premier rendu */}28 <RichTextEditor29 placeholder="Commencez à écrire..."30 onSave={(content) => console.log(content)}31 />32 </div>33 );34}Pièges à Éviter
1// ❌ MAUVAIS - Composant trop léger (overhead > bénéfice)2const SmallButton = dynamic(() => import('./button'));3
4// ✅ BON - Garder les petits composants dans le bundle principal5import { Button } from './button';1// ❌ MAUVAIS - Pas de feedback visuel pendant le chargement2const Chart = dynamic(() => import('./chart'));3
4// ✅ BON - Loading state pour UX fluide5const Chart = dynamic(() => import('./chart'), {6 loading: () => <Skeleton className="h-64 w-full" />7});1// ❌ MAUVAIS - Désactiver SSR sans raison (perte de SEO)2const ProductCard = dynamic(() => import('./product-card'), {3 ssr: false4});5
6// ✅ BON - Garder SSR activé si possible7const ProductCard = dynamic(() => import('./product-card'));8
9// ✅ BON - Désactiver SSR uniquement si nécessaire10const BrowserOnlyMap = dynamic(() => import('./map'), {11 ssr: false // Utilise window/navigator12});Points Clés à Retenir
Le dynamic import est un outil puissant d'optimisation, mais doit être utilisé avec discernement.
Comment remplacer vos API routes par des Server Actions ?
Server Actions - RPC Sécurisé et Type-Safe
Les Server Actions sont des fonctions exécutées côté serveur, appelées directement depuis des Client Components. Elles offrent une alternative moderne aux routes API traditionnelles.
Analogie : Imaginez un appel téléphonique sécurisé. Au lieu de crier vos informations bancaires dans la rue (API REST publique), vous utilisez un téléphone crypté avec vérification d'identité automatique (Server Action). La communication est directe, sécurisée, et vous n'avez pas besoin de configurer une ligne dédiée pour chaque conversation.
Syntaxe de Base
1'use server';2
3import { z } from 'zod';4import { revalidatePath } from 'next/cache';5
6// Schéma de validation7const FormSchema = z.object({8 title: z.string().min(3, 'Titre trop court'),9 description: z.string().max(500, 'Description trop longue'),10});11
12// Server Action13export async function createPost(formData: FormData) {14 // Validation côté serveur15 const validatedFields = FormSchema.safeParse({16 title: formData.get('title'),17 description: formData.get('description'),18 });19
20 if (!validatedFields.success) {21 return {22 errors: validatedFields.error.flatten().fieldErrors,23 };24 }25
26 // Logique métier (accès database, etc.)27 const { title, description } = validatedFields.data;28
29 await db.post.create({30 data: { title, description },31 });32
33 // Revalidation du cache34 revalidatePath('/posts');35
36 return { success: true };37}Appel depuis un Client Component
1'use client';2
3import { useTransition } from 'react';4import { createPost } from '@/app/_lib/server/actions';5
6export function CreatePostForm() {7 const [isPending, startTransition] = useTransition();8
9 async function handleSubmit(formData: FormData) {10 startTransition(async () => {11 const result = await createPost(formData);12
13 if (result.errors) {14 // Afficher les erreurs15 console.error(result.errors);16 } else {17 // Succès18 toast.success('Post créé avec succès');19 }20 });21 }22
23 return (24 <form action={handleSubmit}>25 <input name="title" required />26 <textarea name="description" required />27 <button type="submit" disabled={isPending}>28 {isPending ? 'Création...' : 'Créer'}29 </button>30 </form>31 );32}Avantages des Server Actions
Les Server Actions offrent plusieurs bénéfices significatifs par rapport aux routes API traditionnelles.
Pattern Avancé avec enhanceAction
1'use server';2
3import { enhanceAction } from '@/lib/server/enhance-action';4import { revalidatePath } from 'next/cache';5import { z } from 'zod';6
7const FormSchema = z.object({8 title: z.string().min(3),9 description: z.string().max(500),10});11
12// Action avec validation et auth automatiques13export const submitDemoFormAction = enhanceAction(14 async (data, { user }) => {15 // data est déjà validé selon FormSchema16 // user est automatiquement vérifié (auth: true)17
18 // Logique métier19 await db.post.create({20 data: {21 ...data,22 authorId: user.id,23 },24 });25
26 // Revalidation27 revalidatePath('/dashboard');28
29 return { success: true };30 },31 {32 schema: FormSchema,33 auth: true, // Requiert authentification34 }35);enhanceAction est un pattern de Scanorr qui encapsule la validation Zod, l'authentification, et la gestion d'erreurs dans une abstraction réutilisable.Cycle de Vie d'une Server Action
Comprendre le flux complet d'exécution d'une Server Action.
Server Actions vs API Routes
Pièges à Éviter
1// ❌ MAUVAIS - Aucune validation serveur2'use server';3export async function updateProfile(formData: FormData) {4 await db.user.update({5 data: { name: formData.get('name') } // Dangereux !6 });7}8
9// ✅ BON - Validation serveur obligatoire10'use server';11import { z } from 'zod';12
13const schema = z.object({14 name: z.string().min(2).max(50),15});16
17export async function updateProfile(formData: FormData) {18 const validated = schema.parse({19 name: formData.get('name')20 });21 await db.user.update({ data: validated });22}1// ❌ MAUVAIS - Pas de 'use server' = exécution client2export async function deletePost(id: string) {3 await db.post.delete({ where: { id } });4}5
6// ✅ BON - 'use server' en haut du fichier7'use server';8
9export async function deletePost(id: string) {10 await db.post.delete({ where: { id } });11}1// ❌ MAUVAIS - Exposer des données privées au client2'use server';3export async function getUser(id: string) {4 const user = await db.user.findUnique({ where: { id } });5 return user; // Contient password, email privé, etc.6}7
8// ✅ BON - Ne retourner que les données nécessaires9'use server';10export async function getUser(id: string) {11 const user = await db.user.findUnique({12 where: { id },13 select: { name: true, avatar: true } // Seulement public14 });15 return user;16}1// ❌ MAUVAIS - Cache non invalidé, UI désynchronisée2'use server';3export async function createPost(data: PostData) {4 await db.post.create({ data });5 return { success: true };6}7
8// ✅ BON - Revalidation du cache9'use server';10import { revalidatePath } from 'next/cache';11
12export async function createPost(data: PostData) {13 await db.post.create({ data });14 revalidatePath('/posts');15 return { success: true };16}Points Clés à Retenir
Les Server Actions sont un paradigme moderne pour les mutations côté serveur dans Next.js.
Comment afficher du contenu progressivement ?
Le streaming React résout le problème du "tout ou rien" : au lieu d'attendre que TOUTES les données soient chargées avant d'afficher la page, Next.js envoie progressivement le HTML par chunks dès qu'il est prêt. Résultat : perception de vitesse 2-3× plus rapide et amélioration significative des Core Web Vitals.
Le Problème du Tout ou Rien
Sans streaming, l'utilisateur voit une page blanche jusqu'à ce que TOUTES les données soient chargées, même si 95% de la page est prête.
Sans Streaming
Page blanche pendant 3.5 secondes
Avec Streaming
Shell visible en 0.5s
Comment Fonctionne le Streaming React
React 18 + Next.js 13+ envoient le HTML par chunks progressifs au lieu d'attendre la génération complète côté serveur.
1. Shell HTML envoyé immédiatement
Layout, navigation, header sont rendus côté serveur et envoyés instantanément (50-100ms).
2. Parties dynamiques en parallèle
Chaque composant dans un Suspense charge ses données en parallèle (pas en séquence).
3. Chunks progressifs envoyés
Dès qu'un composant est prêt, son HTML est envoyé au client et hydraté automatiquement.
4. Hydratation sélective
React hydrate uniquement les composants visibles en priorité (visible > hors écran).
Pattern #1 : Suspense Granulaires
Éviter un seul Suspense global. Créer un Suspense par composant lent pour du streaming parallèle optimal.
- Simple à implémenter
- Moins de code
- Page blanche jusqu'à tout charger
- Mauvaise UX
- LCP élevé
- •À éviter en production
- Shell HTML instantané
- Streaming parallèle
- LCP optimal
- Plus de code
- Complexité accrue
- •Dashboard
- •Pages avec données multiples
- •Production
- Meilleur des deux mondes
- Shell instantané
- Données à jour
- Experimental
- Next.js 16+ uniquement
- •E-commerce
- •Pages produit
- •Blog avec commentaires
Suspense Unique | Suspense Granulaires | Partial Prerendering (PPR) |
|---|---|---|
Un seul Suspense wrappant toute la page | Multiples Suspense pour chaque composant lent | Shell statique + slots dynamiques (Next.js 16+) |
Avantages
| Avantages
| Avantages
|
Inconvenients
| Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
| Cas d'usage
|
1// ❌ MAUVAIS : Un seul Suspense global2import { Suspense } from 'react';3
4export default function DashboardPage() {5 return (6 <Suspense fallback={<PageLoader />}>7 <Header />8 <UserProfile />9 <SlowDataTable />10 <Footer />11 </Suspense>12 );13}14
15// Problème :16// - Page blanche jusqu'à ce que SlowDataTable soit chargé (3.5s)17// - Header, UserProfile et Footer (rapides) bloqués par SlowDataTable18// - LCP : 3.5s (très mauvais)19// - Perception : lent et frustrant1// ✅ BON : Suspense granulaires (un par composant lent)2import { Suspense } from 'react';3
4export default function DashboardPage() {5 return (6 <div>7 {/* Header : pas de Suspense (rapide) */}8 <Header />9
10 {/* UserProfile : Suspense dédié */}11 <Suspense fallback={<ProfileSkeleton />}>12 <UserProfile />13 </Suspense>14
15 {/* SlowDataTable : Suspense dédié */}16 <Suspense fallback={<TableSkeleton />}>17 <SlowDataTable />18 </Suspense>19
20 {/* Footer : pas de Suspense (rapide) */}21 <Footer />22 </div>23 );24}25
26// Avantages :27// ✅ Header visible en 50ms (shell HTML)28// ✅ UserProfile visible en 1s (chargé en parallèle)29// ✅ SlowDataTable visible en 3.5s (streaming indépendant)30// ✅ LCP : 50ms (Header = LCP)31// ✅ Perception : instantané et progressifPattern #2 : loading.tsx Automatique
Next.js wrap automatiquement votre page dans un Suspense si vous créez un fichier loading.tsx. Pratique pour du streaming global.
1// app/dashboard/loading.tsx2export default function Loading() {3 return (4 <div className="space-y-6 p-8 animate-pulse">5 <div className="h-12 bg-muted/50 rounded-lg w-1/3" />6 <div className="h-64 bg-muted/50 rounded-lg" />7 <div className="h-96 bg-muted/50 rounded-lg" />8 </div>9 );10}11
12// app/dashboard/page.tsx13export default async function DashboardPage() {14 // Next.js wrap automatiquement dans :15 // <Suspense fallback={<Loading />}>16 // <DashboardPage />17 // </Suspense>18
19 const data = await fetchDashboardData(); // Async Server Component20
21 return (22 <div>23 <h1>Dashboard</h1>24 <DataTable data={data} />25 </div>26 );27}28
29// Équivalent manuel :30// app/dashboard/page.tsx (sans loading.tsx)31import { Suspense } from 'react';32
33async function DashboardContent() {34 const data = await fetchDashboardData();35 return <DataTable data={data} />;36}37
38export default function DashboardPage() {39 return (40 <Suspense fallback={<Loading />}>41 <DashboardContent />42 </Suspense>43 );44}45
46// Quand utiliser loading.tsx :47// ✅ Toute la page est lente (API unique)48// ✅ Pas besoin de Suspense granulaires49// ❌ Éviter si multiples sources de données (préférer Suspense granulaires)Pattern #3 : Partial Prerendering (PPR)
Le meilleur des deux mondes : shell statique généré à la build + slots dynamiques streamés à la demande. Experimental dans Next.js 16+.
Concept du PPR
Le PPR génère un shell statique (layout, header, navigation) à la build et identifie les slots dynamiques (données utilisateur, panier, prix en temps réel).
À la requête, le shell est servi instantanément depuis le CDN (TTFB ~10ms), puis les slots sont streamés en parallèle avec les données fraîches.
1// next.config.js2/** @type {import('next').NextConfig} */3const nextConfig = {4 experimental: {5 ppr: 'incremental', // Active PPR de manière incrémentale6 },7};8
9export default nextConfig;10
11// app/product/[id]/page.tsx12import { Suspense } from 'react';13
14export const experimental_ppr = true; // Active PPR pour cette page15
16export default function ProductPage({ params }: { params: { id: string } }) {17 return (18 <div>19 {/* STATIQUE : Généré à la build, servi depuis CDN */}20 <Header />21 <Breadcrumb productId={params.id} />22
23 {/* DYNAMIQUE : Streamé à la demande */}24 <Suspense fallback={<PriceSkeleton />}>25 <ProductPrice productId={params.id} />26 </Suspense>27
28 {/* STATIQUE : Généré à la build */}29 <ProductDescription productId={params.id} />30
31 {/* DYNAMIQUE : Streamé à la demande */}32 <Suspense fallback={<StockSkeleton />}>33 <ProductStock productId={params.id} />34 </Suspense>35
36 {/* DYNAMIQUE : Streamé à la demande */}37 <Suspense fallback={<ReviewsSkeleton />}>38 <ProductReviews productId={params.id} />39 </Suspense>40 </div>41 );42}43
44// Résultat :45// 1. Shell HTML statique servi en 10ms depuis CDN46// 2. ProductPrice, ProductStock, ProductReviews streamés en parallèle47// 3. TTFB : 10ms (shell) + streaming progressif (données fraîches)48// 4. Meilleur des deux mondes : vitesse statique + données dynamiquesAmélioration Mesurable des Core Web Vitals
Impact concret du streaming sur les métriques de performance et l'expérience utilisateur.
Sans Streaming
Page blanche jusqu'au chargement complet
Hydratation bloquante de tout le DOM
Utilisateur attend 3.5s sans feedback visuel
Avec Streaming
-86% grâce au shell HTML instantané
-66% grâce à l'hydratation sélective
Shell visible en 500ms, feedback visuel immédiat
Impact Business
- Taux de rebond : -32% (perception de vitesse)
- Temps sur page : +58% (engagement amélioré)
- Taux de conversion : +19% (friction réduite)
- Score SEO Google : +15 points (LCP amélioré)
Quelles techniques pour un frontend ultra-rapide ?
Les Core Web Vitals sont les métriques essentielles pour mesurer l'expérience utilisateur. Next.js fournit des outils natifs pour optimiser chacune de ces métriques.
Core Web Vitals : Les Métriques Critiques
Google définit 4 métriques clés pour évaluer la performance perçue par l'utilisateur. Chaque métrique a un seuil à respecter.
LCP - Largest Contentful Paint
Temps de chargement du plus grand élément visible
INP - Interaction to Next Paint
Réactivité aux interactions utilisateur
CLS - Cumulative Layout Shift
Stabilité visuelle (éléments qui bougent)
TTFB - Time to First Byte
Temps de réponse initial du serveur
Solutions Next.js par Métrique
Next.js fournit des optimisations natives pour chaque Core Web Vital. Voici comment les utiliser efficacement.
Optimiser LCP avec next/image
1import Image from 'next/image';2
3export function HeroSection() {4 return (5 <Image6 src="/hero.jpg"7 alt="Hero"8 width={1200}9 height={600}10 priority // Charge immédiatement (pas de lazy loading)11 placeholder="blur" // Affiche un placeholder flou pendant le chargement12 blurDataURL="data:image/..." // Base64 de l'image floue13 quality={85} // Compression optimale (par défaut 75)14 />15 );16}Optimiser INP avec Server Actions
1'use server';2
3import { revalidatePath } from 'next/cache';4
5export async function updateProfile(formData: FormData) {6 const name = formData.get('name');7
8 // Traitement côté serveur (pas de JS client)9 await db.user.update({ name });10
11 // Revalidation instantanée12 revalidatePath('/profile');13
14 return { success: true };15}16
17// Côté client : useOptimistic pour UI instantanée18'use client';19
20import { useOptimistic } from 'react';21
22export function ProfileForm() {23 const [optimisticName, setOptimisticName] = useOptimistic(name);24
25 return (26 <form action={async (formData) => {27 setOptimisticName(formData.get('name')); // UI instantanée28 await updateProfile(formData); // Serveur en arrière-plan29 }}>30 <input name="name" defaultValue={optimisticName} />31 </form>32 );33}Optimiser CLS avec dimensions explicites
1// ❌ MAUVAIS : Provoque un Layout Shift2<img src="/banner.jpg" alt="Banner" />3
4// ✅ BON : Dimensions explicites5<Image6 src="/banner.jpg"7 alt="Banner"8 width={1200}9 height={300}10 className="w-full h-auto" // Responsive mais ratio préservé11/>12
13// ✅ BON : Skeleton Loader14export function ProductCard({ loading }: { loading: boolean }) {15 if (loading) {16 return (17 <div className="h-[400px] animate-pulse bg-muted/50 rounded-lg">18 <div className="h-48 bg-muted" /> {/* Image placeholder */}19 <div className="p-4 space-y-2">20 <div className="h-6 bg-muted rounded" /> {/* Titre */}21 <div className="h-4 bg-muted rounded w-2/3" /> {/* Prix */}22 </div>23 </div>24 );25 }26
27 return <div className="h-[400px]">...</div>;28}Optimiser TTFB avec Edge Runtime et Streaming
1// Edge Runtime : Déployé sur CDN global2export const runtime = 'edge';3
4export async function GET(request: Request) {5 const data = await fetchData();6 return Response.json(data);7}8
9// Static Rendering : TTFB quasi-instantané10export default async function ProductPage({ params }: { params: { id: string } }) {11 const product = await db.product.findUnique({12 where: { id: params.id }13 });14
15 return <ProductDetail product={product} />;16}17
18export async function generateStaticParams() {19 return [{ id: '1' }, { id: '2' }]; // Pages pré-générées20}21
22// Streaming avec Suspense : HTML partiel envoyé immédiatement23export default function DashboardPage() {24 return (25 <div>26 <h1>Dashboard</h1>27 <Suspense fallback={<Skeleton />}>28 <SlowComponent /> {/* Chargé en streaming */}29 </Suspense>30 </div>31 );32}Optimisation du Bundle JavaScript
Réduire la taille du bundle améliore le Time to Interactive (TTI) et le First Contentful Paint (FCP). Next.js offre plusieurs stratégies.
1. Tree Shaking : Importer uniquement ce qui est utilisé
- API riche
- Documentation complète
- 291 KB minifiée
- Import tout ou rien
- Non tree-shakable
- •Applications legacy
- •Besoins complets de manipulation de dates
- 12 KB par fonction
- Tree-shakable
- Import sélectif
- API différente de Moment
- Migration nécessaire
- •Applications modernes
- •Optimisation bundle
- •Next.js recommandé
Moment.js | date-fns |
|---|---|
Librairie de dates populaire mais volumineuse | Alternative moderne et modulaire |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
2. Dynamic Imports : Code Splitting automatique
1import dynamic from 'next/dynamic';2
3// ❌ MAUVAIS : Import statique (toujours chargé)4import HeavyChart from '@/components/heavy-chart';5
6// ✅ BON : Dynamic import (chargé à la demande)7const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {8 loading: () => <div>Chargement du graphique...</div>,9 ssr: false // Désactive le SSR si le composant utilise des APIs navigateur10});11
12export function Dashboard() {13 const [showChart, setShowChart] = useState(false);14
15 return (16 <div>17 <button onClick={() => setShowChart(true)}>18 Afficher le graphique19 </button>20
21 {showChart && <HeavyChart />} {/* Chargé uniquement si affiché */}22 </div>23 );24}25
26// Impact : TTI réduit de 40% sur /dashboard27// Bundle initial : 450 KB → 180 KB28// Bundle HeavyChart : 270 KB (chargé à la demande)3. Server Components : Zéro JavaScript par défaut
1// ✅ Server Component (par défaut dans App Router)2// Rendu côté serveur, zéro JS envoyé au client3export default async function BlogPost({ params }: { params: { slug: string } }) {4 const post = await db.post.findUnique({ where: { slug: params.slug } });5
6 return (7 <article>8 <h1>{post.title}</h1>9 <div dangerouslySetInnerHTML={{ __html: post.content }} />10
11 {/* Client Component uniquement pour l'interactivité */}12 <LikeButton postId={post.id} />13 </article>14 );15}16
17// components/like-button.tsx18'use client'; // ⚠️ Ajoute du JS au bundle client19
20import { useState } from 'react';21
22export function LikeButton({ postId }: { postId: string }) {23 const [liked, setLiked] = useState(false);24
25 return (26 <button onClick={() => setLiked(!liked)}>27 {liked ? '❤️' : '🤍'} J'aime28 </button>29 );30}31
32// Impact : Réduction de 70% du JavaScript client33// Avant (tout en client) : 250 KB de JS34// Après (Server Components) : 75 KB de JS (uniquement LikeButton)Optimisation Images et Polices
Les images et polices représentent souvent 60-80% du poids d'une page. Next.js automatise leur optimisation.
Images : next/image automatise tout
Sans next/image
- • Format PNG/JPG volumineux
- • Pas de lazy loading
- • Chargé en pleine résolution
- • Impact : 2.5 MB de données
Avec next/image
- • Format WebP/AVIF moderne
- • Lazy loading automatique
- • Responsive (srcset généré)
- • Impact : 180 KB (-93%)
1import Image from 'next/image';2
3export function ProductGallery() {4 return (5 <div className="grid grid-cols-3 gap-4">6 {products.map((product) => (7 <Image8 key={product.id}9 src={product.image}10 alt={product.name}11 width={400}12 height={400}13 quality={85} // 85 = bon compromis qualité/poids14 loading="lazy" // Par défaut (sauf si priority={true})15 placeholder="blur" // Placeholder flou pendant chargement16 blurDataURL={product.blurHash}17 />18 ))}19 </div>20 );21}22
23// Next.js génère automatiquement :24// - /product.webp?w=640&q=8525// - /product.webp?w=750&q=8526// - /product.webp?w=828&q=8527// - /product.webp?w=1080&q=8528// Le navigateur choisit la taille adaptéePolices : next/font élimine le Flash of Unstyled Text
1import { Inter, Roboto_Mono } from 'next/font/google';2
3// Chargement optimisé avec preload et display:swap4const inter = Inter({5 subsets: ['latin'],6 display: 'swap', // Affiche texte immédiatement, swap dès que police chargée7 preload: true, // Précharge dans <head>8 variable: '--font-inter', // Variable CSS9});10
11const robotoMono = Roboto_Mono({12 subsets: ['latin'],13 display: 'swap',14 variable: '--font-mono',15});16
17export default function RootLayout({ children }: { children: React.ReactNode }) {18 return (19 <html lang="fr" className={`${inter.variable} ${robotoMono.variable}`}>20 <body className="font-sans">{children}</body>21 </html>22 );23}24
25// CSS généré automatiquement :26// @font-face {27// font-family: '__Inter_xyz';28// src: url(/_next/static/media/abc-123.woff2) format('woff2');29// font-display: swap;30// }31//32// Impact : FOUT éliminé, CLS amélioréImpact Mesurable sur les Performances
Résultats concrets d'optimisation sur une application Next.js de production
Avant Optimisation
Après Optimisation
-57% grâce à next/image priority
-68% grâce à Server Actions + useOptimistic
-72% grâce à dimensions explicites + skeletons
-70% grâce à Server Components + dynamic imports
Résultat Business
- Taux de conversion : +23%
- Taux de rebond : -18%
- Temps de session : +42%
- Score Google Lighthouse : 95/100
Ouvrez le simulateur pour comparer React.memo, useMemo et useCallback avec des mesures reelles de performance.
Comment optimiser vos Server Components ?
Les optimisations backend ont un impact direct sur TTFB et LCP. Une requête lente peut bloquer tout le rendu de la page. Voici les techniques essentielles pour améliorer les performances côté serveur.
Le Problème N+1 : L'Erreur la Plus Coûteuse
Le problème N+1 survient quand on fait une requête par élément dans une boucle. C'est l'anti-pattern le plus fréquent et le plus facile à corriger.
N+1 Queries
101 queries • 1250ms
Query avec Include
1 query • 45ms
Exemple avec Prisma
1// ❌ MAUVAIS : N+1 Problem2export async function getPosts() {3 const posts = await db.post.findMany({ take: 100 });4
5 // ⚠️ 100 requêtes supplémentaires !6 const postsWithAuthors = await Promise.all(7 posts.map(async (post) => ({8 ...post,9 author: await db.user.findUnique({ where: { id: post.userId } })10 }))11 );12
13 return postsWithAuthors;14}15// Résultat : 101 queries, 1250ms16
17// ✅ BON : Include18export async function getPosts() {19 const posts = await db.post.findMany({20 take: 100,21 include: {22 author: true, // JOIN automatique23 comments: {24 include: {25 author: true // Nested include26 }27 }28 }29 });30
31 return posts;32}33// Résultat : 1 query, 45ms34
35// Impact : Amélioration de 27x (1250ms → 45ms)- Code simple
- Facile à comprendre
- 101 queries SQL
- Temps : 1250ms
- Scalabilité impossible
- •Jamais en production
- •Prototype rapide uniquement
- 1 query SQL
- Temps : 45ms
- Amélioration 20x-100x
- Nécessite planification
- Schéma relationnel requis
- •Production recommandée
- •Toute relation parent-enfant
N+1 Queries | Query avec Include |
|---|---|
Requêtes séquentielles pour chaque relation | Une seule requête avec JOIN |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
Database Indexing : De O(n) à O(log n)
Les index transforment des scans séquentiels (lents) en recherches arborescentes (rapides). Un index bien placé peut améliorer une requête de 100x à 1000x.
Types d'Index et Cas d'Usage
- O(log n) vs O(n)
- Supporte <, >, =, BETWEEN
- Amélioration 100x-1000x
- Espace disque
- Ralentit INSERT/UPDATE
- •Colonnes WHERE fréquentes
- •Foreign keys
- •ORDER BY
- Moins d'espace disque
- Plus rapide
- Ciblé sur cas fréquents
- Requiert prédiction du pattern
- Pas utilisé si WHERE différent
- •status = "active"
- •deleted_at IS NULL
- •Filtres récurrents
- Optimise queries multi-colonnes
- Couvre plusieurs cas
- Ordre critique
- Taille importante
- Maintenance complexe
- •WHERE a = ? AND b = ?
- •ORDER BY a, b
- •Groupes fréquents
B-tree Index | Partial Index | Composite Index |
|---|---|---|
Index standard pour égalité et ranges | Index conditionnel sur sous-ensemble de données | Index sur plusieurs colonnes (ordre important) |
Avantages
| Avantages
| Avantages
|
Inconvenients
| Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
| Cas d'usage
|
1. B-tree Index : L'Index Standard
Sans Index (Seq Scan)
- • Scan séquentiel : O(n)
- • 100,000 rows scannées
- • Temps : 450ms
- • CPU : 100%
Avec B-tree Index
- • Index Scan : O(log n)
- • 45 rows scannées
- • Temps : 3ms
- • CPU : 5%
1-- Exemple : Recherche d'inspections par compte2-- Sans index :3SELECT * FROM property_inspections4WHERE account_id = '123';5-- Seq Scan sur 100,000 rows : 450ms6
7-- Créer un index B-tree8CREATE INDEX idx_inspections_account_id9ON property_inspections(account_id);10
11-- Avec index :12SELECT * FROM property_inspections13WHERE account_id = '123';14-- Index Scan sur 45 rows : 3ms15
16-- Amélioration : 150x plus rapide2. Partial Index : Index Conditionnel
1-- Cas d'usage : Filtrer uniquement les inspections actives2-- 90% des requêtes cherchent status IN ('draft', 'pending')3-- 10% cherchent status = 'completed'4
5-- ❌ MAUVAIS : Index complet (indexe aussi les 'completed')6CREATE INDEX idx_inspections_account_status7ON property_inspections(account_id, status);8-- Taille : 12 MB9
10-- ✅ BON : Partial Index (indexe uniquement les actives)11CREATE INDEX idx_inspections_active12ON property_inspections(account_id)13WHERE status IN ('draft', 'pending');14-- Taille : 2 MB (5x plus petit)15-- Performance : 2x plus rapide (moins de données à scanner)16
17-- Requête optimisée automatiquement18SELECT * FROM property_inspections19WHERE account_id = '123' AND status = 'draft';20-- Index Scan sur idx_inspections_active : 1ms21
22-- ⚠️ Requête NON optimisée par cet index23SELECT * FROM property_inspections24WHERE account_id = '123' AND status = 'completed';25-- Seq Scan (l'index ne couvre pas 'completed')3. Composite Index : L'Ordre est CRITIQUE
1-- ⚠️ L'ORDRE DES COLONNES EST CRUCIAL2
3-- Cas 1 : Index (account_id, created_at)4CREATE INDEX idx_inspections_account_date5ON property_inspections(account_id, created_at);6
7-- ✅ Optimise ces requêtes :8SELECT * FROM property_inspections9WHERE account_id = '123'; -- Utilise l'index10
11SELECT * FROM property_inspections12WHERE account_id = '123' AND created_at > '2024-01-01'; -- Utilise l'index13
14SELECT * FROM property_inspections15WHERE account_id = '123' ORDER BY created_at DESC; -- Utilise l'index16
17-- ❌ N'optimise PAS ces requêtes :18SELECT * FROM property_inspections19WHERE created_at > '2024-01-01'; -- Seq Scan (created_at n'est pas en première position)20
21-- Règle : Un index composite (a, b, c) optimise :22-- WHERE a = ?23-- WHERE a = ? AND b = ?24-- WHERE a = ? AND b = ? AND c = ?25-- Mais PAS :26-- WHERE b = ?27-- WHERE c = ?28-- WHERE b = ? AND c = ?29
30-- Solution : Créer des index séparés si nécessaire31CREATE INDEX idx_inspections_date32ON property_inspections(created_at); -- Pour requêtes sans account_idHiérarchie de Cache : Du Plus Rapide au Plus Lent
Le cache idéal dépend de la fréquence de mise à jour et de la durée de vie souhaitée. Next.js offre plusieurs niveaux de cache.
1. React cache()
~msCache mémoire pendant le rendu. Déduplication automatique des requêtes identiques.
2. Next.js Data Cache
secondes/minutesCache serveur persistant. Revalidation par temps ou tag.
3. Redis / KV
minutes/heuresCache distribué. Partagé entre instances. Idéal pour sessions, rate limiting.
4. CDN Cache
heures/joursCache global au plus près de l'utilisateur. Pour assets statiques et pages SSG.
niveaux de cache
Du plus rapide (ms)
au plus lent (jours)
1. React cache() : Déduplication de Requêtes
1import { cache } from 'react';2
3// Sans cache : 3 requêtes identiques4export async function getUser(id: string) {5 return await db.user.findUnique({ where: { id } });6}7
8// ✅ Avec cache : 1 seule requête, résultat partagé9export const getUser = cache(async (id: string) => {10 console.log('DB query:', id); // Appelé 1 seule fois11 return await db.user.findUnique({ where: { id } });12});13
14// Page : Plusieurs composants appellent getUser('123')15export default async function ProfilePage() {16 const user = await getUser('123'); // Query DB17
18 return (19 <div>20 <Header user={await getUser('123')} /> {/* Cache hit */}21 <Sidebar user={await getUser('123')} /> {/* Cache hit */}22 <Profile user={await getUser('123')} /> {/* Cache hit */}23 </div>24 );25}26
27// Résultat : 1 query au lieu de 42. Next.js Data Cache : Revalidation Granulaire
1// Revalidation par temps2export async function getPosts() {3 const res = await fetch('https://api.example.com/posts', {4 next: { revalidate: 60 } // Cache 60 secondes5 });6 return res.json();7}8
9// Revalidation par tag10export async function getPost(id: string) {11 const res = await fetch(`https://api.example.com/posts/${id}`, {12 next: { tags: ['posts', `post-${id}`] }13 });14 return res.json();15}16
17// Invalider le cache manuellement18import { revalidateTag, revalidatePath } from 'next/cache';19
20export async function updatePost(id: string, data: any) {21 await db.post.update({ where: { id }, data });22
23 // Invalide tous les posts24 revalidateTag('posts');25
26 // Invalide un post spécifique27 revalidateTag(`post-${id}`);28
29 // Invalide une route complète30 revalidatePath('/blog');31}32
33// Impact : TTFB réduit de 800ms à 50ms sur hits de cache3. Redis / KV : Cache Distribué
1import { kv } from '@vercel/kv';2
3// Cache distribué pour sessions utilisateur4export async function getUserSession(userId: string) {5 // Vérifier le cache d'abord6 const cached = await kv.get(`session:${userId}`);7 if (cached) return cached;8
9 // Si pas en cache, query DB10 const session = await db.session.findUnique({ where: { userId } });11
12 // Store in cache (expire après 1 heure)13 await kv.setex(`session:${userId}`, 3600, JSON.stringify(session));14
15 return session;16}17
18// Rate limiting avec Redis19export async function checkRateLimit(ip: string) {20 const key = `rate-limit:${ip}`;21 const count = await kv.incr(key);22
23 // Expire après 1 minute24 if (count === 1) {25 await kv.expire(key, 60);26 }27
28 // Max 100 requêtes par minute29 if (count > 100) {30 throw new Error('Rate limit exceeded');31 }32}33
34// Impact : Queries DB réduites de 90%Optimisation des Requêtes SQL
Au-delà des index, l'écriture des requêtes SQL a un impact énorme. Voici les patterns à adopter et éviter.
1. SELECT uniquement les colonnes nécessaires
1-- ❌ MAUVAIS : SELECT * (transfert inutile de données)2SELECT * FROM property_inspections3WHERE account_id = '123';4-- Transfert : 5.2 MB pour 1000 rows5
6-- ✅ BON : SELECT colonnes nécessaires7SELECT id, address, status, created_at8FROM property_inspections9WHERE account_id = '123';10-- Transfert : 180 KB pour 1000 rows (-97%)11
12-- Avec Prisma13// ❌ MAUVAIS14const inspections = await db.propertyInspection.findMany({15 where: { accountId: '123' }16});17
18// ✅ BON19const inspections = await db.propertyInspection.findMany({20 where: { accountId: '123' },21 select: {22 id: true,23 address: true,24 status: true,25 createdAt: true26 }27});2. Pagination avec Cursor (pas OFFSET)
1-- ❌ MAUVAIS : OFFSET (scanne toutes les rows précédentes)2SELECT * FROM posts3ORDER BY created_at DESC4LIMIT 20 OFFSET 10000;5-- Scan : 10,020 rows pour retourner 20 rows6-- Temps : 850ms sur page 5007
8-- ✅ BON : Cursor-based pagination9SELECT * FROM posts10WHERE created_at < '2024-01-15T10:30:00Z'11ORDER BY created_at DESC12LIMIT 20;13-- Scan : 20 rows14-- Temps : 5ms sur page 500 (170x plus rapide)15
16-- Avec Prisma17// ✅ Cursor pagination18const posts = await db.post.findMany({19 take: 20,20 cursor: lastPost ? { id: lastPost.id } : undefined,21 orderBy: { createdAt: 'desc' }22});23
24// Retourner le cursor pour la page suivante25return {26 posts,27 nextCursor: posts[posts.length - 1]?.id28};3. Aggregations en SQL (pas en application)
1// ❌ MAUVAIS : Aggregation en JavaScript2const inspections = await db.propertyInspection.findMany({3 where: { accountId: '123' }4}); // Transfert de 10,000 rows5
6const stats = {7 total: inspections.length,8 completed: inspections.filter(i => i.status === 'completed').length,9 avgScore: inspections.reduce((sum, i) => sum + i.score, 0) / inspections.length10};11// Temps : 450ms, Transfert : 8 MB12
13// ✅ BON : Aggregation en SQL14const stats = await db.propertyInspection.aggregate({15 where: { accountId: '123' },16 _count: { id: true },17 _avg: { score: true }18});19
20const completed = await db.propertyInspection.count({21 where: { accountId: '123', status: 'completed' }22});23// Temps : 12ms, Transfert : 200 bytes24
25// SQL généré :26-- SELECT COUNT(id), AVG(score)27-- FROM property_inspections28-- WHERE account_id = '123';29--30-- SELECT COUNT(*)31-- FROM property_inspections32-- WHERE account_id = '123' AND status = 'completed';33
34// Impact : 37x plus rapide, 40,000x moins de données transféréesEXPLAIN ANALYZE : Diagnostiquer les Requêtes Lentes
EXPLAIN ANALYZE montre exactement comment PostgreSQL exécute une requête. C'est l'outil #1 pour identifier les goulots d'étranglement.
Lire un Plan d'Exécution
1-- Analyser une requête2EXPLAIN ANALYZE3SELECT * FROM property_inspections4WHERE account_id = '123' AND status = 'draft';5
6-- Résultat SANS index :7Seq Scan on property_inspections (cost=0.00..2543.00 rows=45 width=1024)8 (actual time=0.123..450.234 rows=45 loops=1)9 Filter: ((account_id = '123') AND (status = 'draft'))10 Rows Removed by Filter: 9995511Planning Time: 0.345 ms12Execution Time: 450.567 ms13
14-- ⚠️ Signes d'alerte :15-- 1. "Seq Scan" : Scan séquentiel (pas d'index utilisé)16-- 2. "Rows Removed by Filter: 99955" : 99,955 rows scannées pour rien17-- 3. "Execution Time: 450.567 ms" : Très lent18
19-- Solution : Créer un index20CREATE INDEX idx_inspections_account_status21ON property_inspections(account_id, status);22
23-- Résultat AVEC index :24Index Scan using idx_inspections_account_status on property_inspections25 (cost=0.29..12.45 rows=45 width=1024)26 (actual time=0.023..2.456 rows=45 loops=1)27 Index Cond: ((account_id = '123') AND (status = 'draft'))28Planning Time: 0.234 ms29Execution Time: 2.567 ms30
31-- ✅ Améliorations :32-- 1. "Index Scan" : Index utilisé33-- 2. "Index Cond" : Filtrage via index (pas scan)34-- 3. "Execution Time: 2.567 ms" : 175x plus rapideSignes d'Alerte dans EXPLAIN
Problèmes Critiques
- •Seq Scan
Scan séquentiel au lieu d'index
- •Rows Removed: 99%+
Presque toutes les rows filtrées après scan
- •loops > 1
Nested loops (N+1 probable)
- •cost: 10000+
Coût estimé très élevé
Bons Signes
- •Index Scan
Index utilisé efficacement
- •Index Only Scan
Données lues depuis l'index uniquement
- •rows estimées = actual
Statistiques à jour
- •Execution < 10ms
Performance optimale
Workflow d'Optimisation
Identifier la requête lente
Logs, monitoring, profiler Next.js
Lancer EXPLAIN ANALYZE
Analyser le plan d'exécution
Créer les index nécessaires
B-tree, partial, ou composite selon le cas
Re-lancer EXPLAIN ANALYZE
Vérifier l'amélioration
Mesurer en production
Confirmer les gains réels
Comment mesurer et améliorer vos Core Web Vitals ?
Mesure et Comparaison des Performances#
Mesurer les performances de vos composants React est essentiel pour prendre des decisions d'optimisation eclairees. Cette section vous montre comment utiliser les outils de profilage et comparer concretement differentes strategies d'optimisation.
Pourquoi mesurer les performances ?
L'optimisation prematuree est la racine de tous les maux, mais l'optimisation informee est essentielle
- Identifier les bottlenecks : Concentrez vos efforts la ou ils ont le plus d'impact
- Valider les hypotheses : Une optimisation peut parfois ralentir le code
- Suivre les regressions : Detectez les degradations de performance au fil du temps
- Communiquer l'impact : Chiffrez les ameliorations pour justifier le temps investi
Outils de Mesure Disponibles#
React et le navigateur offrent plusieurs outils pour mesurer les performances :
React DevTools Profiler
Outil visuel integre au navigateur
- Enregistre les sessions de rendu
- Visualise le Flame Graph des composants
- Identifie les composants lents
- Disponible dans Chrome et Firefox
React Profiler API
Mesure programmatique des rendus
- Composant Profiler integre a React
- Callback onRender avec metriques
- Mesure actualDuration et baseDuration
- Utilisable en production
Performance API
API native du navigateur
- performance.mark() pour marquer des points
- performance.measure() pour calculer durees
- Haute precision (microseconde)
- Integre au Performance Panel
Custom Timers
Mesures simples avec Date ou performance.now()
- Date.now() pour simplicite
- performance.now() pour precision
- Facile a integrer dans le code
- Limite au contexte synchrone
Utilisation de l'API Profiler#
Le composant Profiler de React permet de mesurer le temps de rendu de vos composants directement dans votre application :
1import { Profiler, ProfilerOnRenderCallback } from 'react';2
3function MyComponent() {4 const onRender: ProfilerOnRenderCallback = (5 id, // "id" du Profiler qui vient de commit6 phase, // "mount" ou "update"7 actualDuration, // Temps passe a render cette mise a jour8 baseDuration, // Temps estime sans memoization9 startTime, // Quand React a commence a render10 commitTime // Quand React a commite cette mise a jour11 ) => {12 console.log(`${id} (${phase}): ${actualDuration.toFixed(2)}ms`);13
14 // Envoyez ces metriques a votre service d'analytics15 if (actualDuration > 100) {16 console.warn('Rendu lent detecte !');17 }18 };19
20 return (21 <Profiler id="MyComponent" onRender={onRender}>22 <div>23 {/* Votre composant ici */}24 </div>25 </Profiler>26 );27}Difference entre actualDuration et baseDuration
Comprendre les metriques de performance
actualDuration : Temps reel passe a rendre le composant et ses enfants. Ce temps inclut les benefices des optimisations comme React.memo et useMemo.
baseDuration : Temps estime si aucun composant n'etait memorise. React le calcule en additionnant les durees de rendu les plus recentes de chaque composant.
Si actualDuration est beaucoup plus petit que baseDuration, vos optimisations fonctionnent bien !
Demo Interactive : Comparaison des Strategies#
Testez par vous-meme les differences entre les strategies d'optimisation sur une liste de produits avec recherche en temps reel.
Comparez React.memo, useMemo et useCallback avec des mesures reelles de temps de rendu sur un benchmark interactif.
Analyse des Resultats#
Les resultats du simulateur montrent clairement l'impact de chaque optimisation :
Sans optimisation
Temps de rendu : ~100-150ms
Problemes :
- Filtrage recalcule a chaque render
- Tous les items re-render systematiquement
- Calculs couteux repetes inutilement
Avec React.memo
Temps de rendu : ~40-60ms
Amelioration ~50% :
- Items ne re-render que si changes
- Filtrage toujours couteux
- Bon compromis effort/resultat
Avec useMemo
Temps de rendu : ~30-50ms
Amelioration ~60% :
- Filtrage optimise
- Items re-render toujours
- Utile pour listes tres longues
Tout optimise
Temps de rendu : ~5-15ms
Amelioration ~90% :
- Combinaison de toutes les optimisations
- Performance optimale
- Necessite plus de code
Quand optimiser ?
Regles pragmatiques pour decider
Optimisez si :
- Le temps de rendu depasse 50ms de maniere reguliere
- Les utilisateurs se plaignent de ralentissements ou de lag
- Le composant est rendu frequemment (ex: scroll, hover)
- Le profiler DevTools montre un bottleneck clair
N'optimisez PAS si :
- Le rendu prend moins de 16ms (60 FPS)
- Le composant est rarement affiche
- L'optimisation complexifie excessivement le code
- Vous n'avez pas mesure le probleme
Difference Dev vs Production#
Les performances en mode developpement ne refletent PAS les performances en production :
1// Mode Developpement2- React inclut des warnings et checks supplementaires3- Source maps et stack traces detaillees4- Hot Module Replacement actif5- Double render en Strict Mode (React 18+)6→ 2-3x plus lent que production7
8// Mode Production9- Code minifie et optimise10- Tree-shaking applique11- Pas de dev warnings12- Un seul render par update13→ Performance reelle pour les utilisateurs14
15// Toujours tester en production !16npm run build17npm run startBest Practice : Mesurer en Production
Utilisez des outils de Real User Monitoring (RUM)
Pour mesurer les performances reelles :
- Vercel Analytics : Core Web Vitals automatiques
- Google Lighthouse : Audit complet de performance
- WebPageTest : Tests multi-localisations
- Custom RUM : Profiler API + analytics backend
Comment sécuriser votre app Next.js en production ?
La sécurité est primordiale dans Next.js. Ce guide couvre les vulnérabilités OWASP Top 10 et les solutions concrètes avec Supabase, Zod et les Security Headers.
OWASP Top 10 : Vulnérabilités Critiques
Les 5 vulnérabilités les plus fréquentes dans les applications Next.js et comment les prévenir efficacement.
1. Broken Access Control
Code Vulnérable
Vérifier ownership uniquement côté client
Solution Sécurisée
Vérification serveur + Row-Level Security (RLS)
1// ❌ VULNÉRABLE : Vérification client uniquement2'use client';3
4export function DeleteButton({ inspectionId, userId }: { inspectionId: string; userId: string }) {5 const handleDelete = async () => {6 // ⚠️ userId peut être modifié dans DevTools !7 await fetch('/api/inspections', {8 method: 'DELETE',9 body: JSON.stringify({ inspectionId, userId })10 });11 };12 return <button onClick={handleDelete}>Supprimer</button>;13}14
15// ✅ SÉCURISÉ : Vérification serveur + RLS Supabase16'use server';17
18import { createClient } from '@/lib/supabase/server';19
20export async function deleteInspection(inspectionId: string) {21 const supabase = await createClient();22
23 // Récupération userId authentifié (impossible à falsifier)24 const { data: { user } } = await supabase.auth.getUser();25 if (!user) throw new Error('Unauthorized');26
27 // RLS Supabase vérifie automatiquement l'ownership28 const { error } = await supabase29 .from('property_inspections')30 .delete()31 .eq('id', inspectionId);32 // .eq('user_id', user.id); // ✅ RLS vérifie automatiquement33
34 if (error) throw error;35}2. Cryptographic Failures
1// ✅ Supabase Auth : Hashing sécurisé avec bcrypt2// Jamais stocker de mots de passe en clair !3
4// Inscription utilisateur5const { data, error } = await supabase.auth.signUp({6 email: 'user@example.com',7 password: 'SuperSecretPassword123!', // ✅ Hashé automatiquement avec bcrypt8});9
10// Supabase utilise :11// - bcrypt pour hasher les mots de passe12// - 10 rounds de salting (coût optimal)13// - Stockage dans auth.users (table protégée)14
15// ✅ Chiffrement des données sensibles avec pgcrypto16// Exemple : Chiffrer les numéros de carte bancaire17CREATE EXTENSION IF NOT EXISTS pgcrypto;18
19INSERT INTO payments (user_id, card_number_encrypted)20VALUES (21 '123',22 pgp_sym_encrypt('4111-1111-1111-1111', 'secret-key-from-env')23);24
25SELECT pgp_sym_decrypt(card_number_encrypted, 'secret-key-from-env') AS card_number26FROM payments WHERE user_id = '123';3. Injection (SQL, XSS, Command)
1// ❌ SQL INJECTION VULNERABLE2const { data } = await supabase3 .from('users')4 .select('*')5 .eq('email', userInput); // ⚠️ Si userInput = "' OR '1'='1", retourne TOUS les users6
7// ✅ SÉCURISÉ : Parameterized Queries (Supabase utilise automatiquement)8const { data } = await supabase9 .from('users')10 .select('*')11 .eq('email', userInput); // ✅ userInput est automatiquement échappé12
13// ❌ XSS VULNERABLE : Injection JavaScript14export function UserComment({ comment }: { comment: string }) {15 return <div dangerouslySetInnerHTML={{ __html: comment }} />;16 // ⚠️ Si comment = "<script>alert('XSS')</script>", exécute le script !17}18
19// ✅ SÉCURISÉ : React échappe automatiquement20export function UserComment({ comment }: { comment: string }) {21 return <div>{comment}</div>; // ✅ <script> est affiché comme texte22}23
24// ✅ Si HTML nécessaire : Sanitize avec DOMPurify25import DOMPurify from 'isomorphic-dompurify';26
27export function UserComment({ comment }: { comment: string }) {28 const sanitized = DOMPurify.sanitize(comment, {29 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],30 ALLOWED_ATTR: ['href']31 });32 return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;33}4. Insecure Design : IDOR (Insecure Direct Object Reference)
1// ❌ IDOR VULNERABLE : IDs prévisibles séquentiels2// URL : /api/inspections/1234 (attaquant peut tester 1235, 1236...)3export async function GET(request: Request, { params }: { params: { id: string } }) {4 const { id } = params; // ⚠️ ID prévisible5 const inspection = await db.inspection.findUnique({ where: { id: parseInt(id) } });6 return Response.json(inspection);7}8
9// ✅ SÉCURISÉ : UUIDs non-prévisibles + vérification ownership10// URL : /api/inspections/550e8400-e29b-41d4-a716-44665544000011import { v4 as uuidv4 } from 'uuid';12import { createClient } from '@/lib/supabase/server';13
14export async function GET(request: Request, { params }: { params: { id: string } }) {15 const supabase = await createClient();16 const { data: { user } } = await supabase.auth.getUser();17 if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });18
19 // UUID impossible à deviner + vérification ownership20 const { data: inspection, error } = await supabase21 .from('property_inspections')22 .select('*')23 .eq('id', params.id) // ✅ UUID : 550e8400-e29b-41d4-a716-44665544000024 .eq('user_id', user.id) // ✅ Vérification ownership25 .single();26
27 if (error || !inspection) {28 return Response.json({ error: 'Not found' }, { status: 404 });29 }30
31 return Response.json(inspection);32}5. Security Misconfiguration
1// ❌ VULNÉRABLE : Exposer des secrets côté client2// .env3NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co4NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ✅ OK (publique)5SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ⚠️ NE JAMAIS exposer !6
7// ❌ Code vulnérable8'use client';9const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; // ⚠️ undefined côté client !10// Si utilisé avec NEXT_PUBLIC_, la clé serait exposée dans le bundle JS11
12// ✅ SÉCURISÉ : Service key uniquement serveur13'use server';14import { createClient } from '@supabase/supabase-js';15
16export async function deleteUserAdmin(userId: string) {17 const supabase = createClient(18 process.env.NEXT_PUBLIC_SUPABASE_URL!,19 process.env.SUPABASE_SERVICE_ROLE_KEY! // ✅ Accessible uniquement serveur20 );21
22 // Actions admin (bypass RLS)23 await supabase.auth.admin.deleteUser(userId);24}Row-Level Security (RLS) Supabase
RLS garantit que chaque utilisateur accède uniquement à ses propres données, même en cas de faille dans le code applicatif.
1-- Activer RLS sur la table2ALTER TABLE property_inspections ENABLE ROW LEVEL SECURITY;3
4-- Politique : Users accèdent uniquement à leurs propres inspections5CREATE POLICY "Users access own inspections"6ON property_inspections7FOR ALL -- SELECT, INSERT, UPDATE, DELETE8USING (auth.uid() = user_id) -- Vérifie que l'user authentifié = user_id de la row9WITH CHECK (auth.uid() = user_id); -- Vérifie lors d'INSERT/UPDATE10
11-- Test de sécurité : Un user ne peut PAS accéder aux données d'un autre12-- User A (auth.uid() = '123') tente d'accéder à une inspection de User B (user_id = '456')13SELECT * FROM property_inspections WHERE id = 'uuid-de-user-b';14-- Résultat : 0 rows (RLS bloque automatiquement)15
16-- Politique Admin : Bypass RLS pour admins17CREATE POLICY "Admins access all inspections"18ON property_inspections19FOR ALL20USING (21 EXISTS (22 SELECT 1 FROM user_roles23 WHERE user_id = auth.uid() AND role = 'admin'24 )25);26
27-- Impact : Même si un développeur oublie .eq('user_id', user.id),28-- RLS protège automatiquement les données !Input Validation avec Zod
Zod permet de valider et typer les données côté serveur et client avec un seul schéma.
- UX réactive
- Feedback immédiat
- Contournable (DevTools)
- Pas de protection réelle
- Faille de sécurité critique
- •Jamais en production
- •Amélioration UX seulement
- UX réactive
- Sécurité garantie
- Protection OWASP
- Duplication du code de validation
- •Production (obligatoire)
- •Applications sécurisées
- •APIs publiques
Validation Client Uniquement | Validation Client + Serveur |
|---|---|
Validation côté navigateur avec JavaScript | Double validation (UX + sécurité) |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
1import { z } from 'zod';2
3// Schéma de validation réutilisable4const InspectionSchema = z.object({5 title: z.string()6 .min(3, 'Titre trop court')7 .max(100, 'Titre trop long')8 .regex(/^[a-zA-Z0-9s-]+$/, 'Caractères spéciaux interdits'),9
10 email: z.string()11 .email('Email invalide')12 .toLowerCase() // Normalisation13 .trim(), // Supprime espaces14
15 price: z.number()16 .positive('Prix doit être positif')17 .max(1000000, 'Prix trop élevé'),18
19 category: z.enum(['residential', 'commercial', 'industrial']),20
21 address: z.object({22 street: z.string().min(5),23 zipCode: z.string().regex(/^d{5}$/, 'Code postal invalide'),24 }),25});26
27// Type TypeScript généré automatiquement28type Inspection = z.infer<typeof InspectionSchema>;29
30// Utilisation côté serveur31'use server';32
33export async function createInspection(formData: FormData) {34 // Validation avec Zod35 const result = InspectionSchema.safeParse({36 title: formData.get('title'),37 email: formData.get('email'),38 price: parseFloat(formData.get('price') as string),39 category: formData.get('category'),40 address: {41 street: formData.get('street'),42 zipCode: formData.get('zipCode'),43 },44 });45
46 // Si validation échoue47 if (!result.success) {48 return {49 error: result.error.flatten().fieldErrors,50 // { title: ['Titre trop court'], email: ['Email invalide'] }51 };52 }53
54 // Données validées et typées55 const validatedData = result.data; // ✅ Type-safe56
57 // Insertion en DB58 await supabase.from('inspections').insert(validatedData);59}Security Headers Essentiels
Les headers HTTP sécurisent l'application contre XSS, clickjacking, MIME sniffing et autres attaques.
1// next.config.js2module.exports = {3 async headers() {4 return [5 {6 source: '/:path*',7 headers: [8 {9 key: 'X-Content-Type-Options',10 value: 'nosniff', // ✅ Empêche MIME sniffing (ex: .txt exécuté comme .js)11 },12 {13 key: 'X-Frame-Options',14 value: 'DENY', // ✅ Empêche clickjacking (iframe)15 },16 {17 key: 'Strict-Transport-Security',18 value: 'max-age=31536000; includeSubDomains', // ✅ Force HTTPS19 },20 {21 key: 'Content-Security-Policy',22 value: [23 "default-src 'self'", // ✅ Ressources uniquement du même domaine24 "script-src 'self' 'unsafe-inline' https://vercel.live", // Scripts autorisés25 "style-src 'self' 'unsafe-inline'", // Styles autorisés26 "img-src 'self' data: https:", // Images autorisées27 "font-src 'self' data:", // Polices autorisées28 "connect-src 'self' https://*.supabase.co", // APIs autorisées29 "frame-ancestors 'none'", // ✅ Équivalent X-Frame-Options30 ].join('; '),31 },32 {33 key: 'Referrer-Policy',34 value: 'strict-origin-when-cross-origin', // ✅ Limite info envoyée aux sites externes35 },36 {37 key: 'Permissions-Policy',38 value: 'camera=(), microphone=(), geolocation=()', // ✅ Désactive APIs sensibles39 },40 ],41 },42 ];43 },44};45
46// Vérifier les headers : https://securityheaders.com47// Score cible : A+Pièges Critiques à Éviter
Les erreurs de sécurité les plus fréquentes dans les applications Next.js et comment les détecter.
1. Valider seulement côté client
La validation client peut être contournée. Toujours valider côté serveur avec Zod.
2. Exposer SUPABASE_SERVICE_ROLE_KEY avec NEXT_PUBLIC_
Jamais préfixer les secrets avec NEXT_PUBLIC_ (expose dans le bundle client).
3. Oublier d'activer RLS sur les nouvelles tables
Par défaut, RLS est désactivé. Toujours activer avec ALTER TABLE ... ENABLE ROW LEVEL SECURITY.
4. Utiliser dangerouslySetInnerHTML sans DOMPurify
Vulnérabilité XSS. Toujours sanitize avec DOMPurify avant d'injecter du HTML.
5. Pas de rate limiting sur APIs publiques
Implémenter rate limiting avec Upstash Redis ou Vercel Edge Config pour éviter abus.
6. Utiliser IDs séquentiels (1, 2, 3...) au lieu d'UUIDs
IDOR vulnerability. Toujours utiliser UUIDs (uuid_generate_v4() dans Postgres).
Checklist Sécurité Avant Production
Vérifications essentielles avant de déployer une application Next.js sécurisée.
Quelles bonnes pratiques React appliquer dans Next.js ?
Maîtriser les patterns React permet d'écrire du code plus maintenable, performant et lisible. Ce guide couvre les anti-patterns courants et les solutions modernes.
Hooks Anti-Patterns : Les Erreurs Fréquentes
Les 3 erreurs les plus courantes avec les hooks React et leurs solutions élégantes.
1. useEffect Abuse : Dérivation Directe vs useEffect
Anti-Pattern
useEffect pour calculer valeur dérivée
Pattern Correct
Calculer pendant le render
1// ❌ ANTI-PATTERN : useEffect inutile2function ProductList({ products }: { products: Product[] }) {3 const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);4
5 // ⚠️ Cause 2 renders au lieu d'1 !6 useEffect(() => {7 setFilteredProducts(products.filter(p => p.inStock));8 }, [products]);9
10 return <div>{filteredProducts.map(...)}</div>;11}12
13// ✅ SOLUTION : Dérivation directe14function ProductList({ products }: { products: Product[] }) {15 // ✅ Calculé pendant le render (1 seul render)16 const filteredProducts = products.filter(p => p.inStock);17
18 return <div>{filteredProducts.map(...)}</div>;19}20
21// ✅ Si calcul coûteux : useMemo22function ProductList({ products }: { products: Product[] }) {23 const filteredProducts = useMemo(24 () => products.filter(p => p.inStock && expensiveCalculation(p)),25 [products] // Re-calcule uniquement si products change26 );27
28 return <div>{filteredProducts.map(...)}</div>;29}30
31// Règle : useEffect est pour les side effects (fetch, subscription, DOM)32// PAS pour la dérivation d'état !2. Multiple useState : État Unifié avec useReducer
1// ❌ ANTI-PATTERN : Multiples useState liés2function CheckoutForm() {3 const [name, setName] = useState('');4 const [email, setEmail] = useState('');5 const [address, setAddress] = useState('');6 const [loading, setLoading] = useState(false);7 const [error, setError] = useState<string | null>(null);8 const [success, setSuccess] = useState(false);9
10 const handleSubmit = async () => {11 setLoading(true);12 setError(null);13 // ... logique complexe avec 6 setters14 };15}16
17// ✅ SOLUTION : useReducer pour état complexe18type State = {19 form: { name: string; email: string; address: string };20 status: 'idle' | 'loading' | 'success' | 'error';21 error: string | null;22};23
24type Action =25 | { type: 'UPDATE_FIELD'; field: keyof State['form']; value: string }26 | { type: 'SUBMIT_START' }27 | { type: 'SUBMIT_SUCCESS' }28 | { type: 'SUBMIT_ERROR'; error: string };29
30function reducer(state: State, action: Action): State {31 switch (action.type) {32 case 'UPDATE_FIELD':33 return { ...state, form: { ...state.form, [action.field]: action.value } };34 case 'SUBMIT_START':35 return { ...state, status: 'loading', error: null };36 case 'SUBMIT_SUCCESS':37 return { ...state, status: 'success' };38 case 'SUBMIT_ERROR':39 return { ...state, status: 'error', error: action.error };40 default:41 return state;42 }43}44
45function CheckoutForm() {46 const [state, dispatch] = useReducer(reducer, {47 form: { name: '', email: '', address: '' },48 status: 'idle',49 error: null,50 });51
52 const handleSubmit = async () => {53 dispatch({ type: 'SUBMIT_START' });54 try {55 await submitForm(state.form);56 dispatch({ type: 'SUBMIT_SUCCESS' });57 } catch (error) {58 dispatch({ type: 'SUBMIT_ERROR', error: error.message });59 }60 };61
62 return (63 <form>64 <input65 value={state.form.name}66 onChange={(e) => dispatch({ type: 'UPDATE_FIELD', field: 'name', value: e.target.value })}67 />68 {state.status === 'loading' && <Spinner />}69 {state.status === 'error' && <Error message={state.error} />}70 </form>71 );72}3. Prop Drilling : Context API ou Zustand
1// ❌ ANTI-PATTERN : Prop drilling sur 5 niveaux2<App user={user}>3 <Layout user={user}>4 <Dashboard user={user}>5 <Sidebar user={user}>6 <UserProfile user={user} /> {/* user passé 5 fois ! */}7 </Sidebar>8 </Dashboard>9 </Layout>10</App>11
12// ✅ SOLUTION 1 : Context API (pour données rarement modifiées)13import { createContext, useContext } from 'react';14
15const UserContext = createContext<User | null>(null);16
17export function UserProvider({ children, user }: { children: React.ReactNode; user: User }) {18 return <UserContext.Provider value={user}>{children}</UserContext.Provider>;19}20
21export function useUser() {22 const user = useContext(UserContext);23 if (!user) throw new Error('useUser must be inside UserProvider');24 return user;25}26
27// Utilisation28function App({ user }: { user: User }) {29 return (30 <UserProvider user={user}>31 <Layout>32 <Dashboard>33 <Sidebar>34 <UserProfile /> {/* ✅ Pas de props ! */}35 </Sidebar>36 </Dashboard>37 </Layout>38 </UserProvider>39 );40}41
42function UserProfile() {43 const user = useUser(); // ✅ Accès direct44 return <div>{user.name}</div>;45}46
47// ✅ SOLUTION 2 : Zustand (pour état UI fréquemment modifié)48import { create } from 'zustand';49
50const useCartStore = create<CartState>((set) => ({51 items: [],52 addItem: (item) => set((state) => ({ items: [...state.items, item] })),53 removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),54}));55
56// Utilisation dans n'importe quel composant (sans Provider !)57function CartButton() {58 const items = useCartStore(state => state.items); // ✅ Re-render uniquement si items change59 return <button>Panier ({items.length})</button>;60}61
62function ProductCard({ product }: { product: Product }) {63 const addItem = useCartStore(state => state.addItem); // ✅ Pas de re-render si items change64 return <button onClick={() => addItem(product)}>Ajouter</button>;65}Component Composition Patterns
Patterns avancés pour créer des composants réutilisables et flexibles.
1. Children Pattern : Layout Composable
1// ✅ Pattern : Children pour composition flexible2function Card({ children }: { children: React.ReactNode }) {3 return (4 <div className="rounded-lg border p-6 shadow-sm">5 {children}6 </div>7 );8}9
10// Utilisation : Composition libre11<Card>12 <h2>Titre</h2>13 <p>Description</p>14 <button>Action</button>15</Card>16
17// Alternative : Slots nommés18function Card({19 header,20 content,21 footer22}: {23 header: React.ReactNode;24 content: React.ReactNode;25 footer: React.ReactNode;26}) {27 return (28 <div className="card">29 <div className="card-header">{header}</div>30 <div className="card-content">{content}</div>31 <div className="card-footer">{footer}</div>32 </div>33 );34}35
36<Card37 header={<h2>Titre</h2>}38 content={<p>Description</p>}39 footer={<button>Action</button>}40/>2. Render Props : Logique Réutilisable
1// ✅ Pattern : Render Props pour partager logique2interface MousePosition {3 x: number;4 y: number;5}6
7function MouseTracker({ render }: { render: (position: MousePosition) => React.ReactNode }) {8 const [position, setPosition] = useState({ x: 0, y: 0 });9
10 useEffect(() => {11 const handleMouseMove = (e: MouseEvent) => {12 setPosition({ x: e.clientX, y: e.clientY });13 };14
15 window.addEventListener('mousemove', handleMouseMove);16 return () => window.removeEventListener('mousemove', handleMouseMove);17 }, []);18
19 return <>{render(position)}</>;20}21
22// Utilisation : Render props23<MouseTracker24 render={({ x, y }) => (25 <div>26 Position : {x}, {y}27 </div>28 )}29/>30
31// Alternative moderne : Custom Hook32function useMousePosition() {33 const [position, setPosition] = useState({ x: 0, y: 0 });34
35 useEffect(() => {36 const handleMouseMove = (e: MouseEvent) => {37 setPosition({ x: e.clientX, y: e.clientY });38 };39
40 window.addEventListener('mousemove', handleMouseMove);41 return () => window.removeEventListener('mousemove', handleMouseMove);42 }, []);43
44 return position;45}46
47// Utilisation : Custom Hook (plus simple !)48function App() {49 const { x, y } = useMousePosition();50 return <div>Position : {x}, {y}</div>;51}3. Compound Components : API Déclarative
1// ✅ Pattern : Compound Components (ex: Tabs)2import { createContext, useContext, useState } from 'react';3
4const TabsContext = createContext<{5 activeTab: string;6 setActiveTab: (tab: string) => void;7} | null>(null);8
9function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {10 const [activeTab, setActiveTab] = useState(defaultTab);11
12 return (13 <TabsContext.Provider value={{ activeTab, setActiveTab }}>14 <div className="tabs">{children}</div>15 </TabsContext.Provider>16 );17}18
19function TabsList({ children }: { children: React.ReactNode }) {20 return <div className="tabs-list">{children}</div>;21}22
23function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) {24 const context = useContext(TabsContext);25 if (!context) throw new Error('TabsTrigger must be inside Tabs');26
27 const { activeTab, setActiveTab } = context;28
29 return (30 <button31 onClick={() => setActiveTab(value)}32 className={activeTab === value ? 'active' : ''}33 >34 {children}35 </button>36 );37}38
39function TabsContent({ value, children }: { value: string; children: React.ReactNode }) {40 const context = useContext(TabsContext);41 if (!context) throw new Error('TabsContent must be inside Tabs');42
43 if (context.activeTab !== value) return null;44
45 return <div className="tabs-content">{children}</div>;46}47
48// Export comme namespace49export const TabsComponent = {50 Root: Tabs,51 List: TabsList,52 Trigger: TabsTrigger,53 Content: TabsContent,54};55
56// Utilisation : API déclarative et flexible57import { TabsComponent } from './tabs';58
59<TabsComponent.Root defaultTab="account">60 <TabsComponent.List>61 <TabsComponent.Trigger value="account">Compte</TabsComponent.Trigger>62 <TabsComponent.Trigger value="password">Mot de passe</TabsComponent.Trigger>63 </TabsComponent.List>64
65 <TabsComponent.Content value="account">66 <p>Gérer votre compte</p>67 </TabsComponent.Content>68
69 <TabsComponent.Content value="password">70 <p>Changer votre mot de passe</p>71 </TabsComponent.Content>72</TabsComponent.Root>4. Higher-Order Components (HOC) : Enrichir Composants
1// ✅ Pattern : HOC pour ajouter fonctionnalité2function withAuth<P extends object>(Component: React.ComponentType<P>) {3 return function AuthenticatedComponent(props: P) {4 const { user, loading } = useAuth();5
6 if (loading) return <Spinner />;7 if (!user) return <Redirect to="/login" />;8
9 return <Component {...props} />;10 };11}12
13// Utilisation14function DashboardPage() {15 return <div>Dashboard privé</div>;16}17
18export default withAuth(DashboardPage);19
20// ⚠️ HOC moins utilisé aujourd'hui, préférer :21// - Custom Hooks (plus simple)22// - Render Props23// - Compound Components24
25// Alternative moderne : Middleware Route26// app/dashboard/layout.tsx27import { redirect } from 'next/navigation';28import { createClient } from '@/lib/supabase/server';29
30export default async function DashboardLayout({ children }: { children: React.ReactNode }) {31 const supabase = await createClient();32 const { data: { user } } = await supabase.auth.getUser();33
34 if (!user) redirect('/login'); // ✅ Redirect côté serveur35
36 return <>{children}</>;37}Performance Optimization : Éviter Re-renders Inutiles
Techniques pour optimiser les performances React et réduire les re-renders.
1. React.memo : Memoization de Composant
1// ❌ PROBLÈME : Re-render à chaque fois que parent re-render2function ProductCard({ product }: { product: Product }) {3 console.log('ProductCard render');4 return <div>{product.name}</div>;5}6
7function ProductList({ products }: { products: Product[] }) {8 const [count, setCount] = useState(0);9
10 // ⚠️ Chaque setCount() re-render TOUS les ProductCard !11 return (12 <div>13 <button onClick={() => setCount(count + 1)}>Count: {count}</button>14 {products.map(p => <ProductCard key={p.id} product={p} />)}15 </div>16 );17}18
19// ✅ SOLUTION : React.memo pour éviter re-render si props identiques20const ProductCard = React.memo(function ProductCard({ product }: { product: Product }) {21 console.log('ProductCard render'); // ✅ Ne log que si product change22 return <div>{product.name}</div>;23});24
25// ⚠️ Attention : React.memo compare par référence (===)26// Si product est un nouvel objet à chaque render, memo est inutile !27
28// ✅ Custom comparison pour deep equality29const ProductCard = React.memo(30 function ProductCard({ product }: { product: Product }) {31 return <div>{product.name}</div>;32 },33 (prevProps, nextProps) => prevProps.product.id === nextProps.product.id34);Cas d'utilisation concrets de React.memo
- 1. Items de liste : Dans une liste de produits, actualités ou messages où chaque item est indépendant
- 2. Composants de visualisation : Graphiques, cartes, tableaux qui sont coûteux à rendre
- 3. Sections de formulaire : Isoler les sections d'un formulaire complexe pour éviter re-render global
- 4. Composants feuilles : Composants en bout d'arbre sans enfants, purement présentationnels
- 5. Widgets indépendants : Timer, compteur, horloge qui ne dépendent pas du state parent
2. useMemo : Memoization de Valeur
1// ❌ PROBLÈME : Calcul coûteux à chaque render2function ProductList({ products }: { products: Product[] }) {3 const [filter, setFilter] = useState('');4
5 // ⚠️ Re-calcule à chaque render (même si products/filter identiques)6 const filteredProducts = products7 .filter(p => p.name.includes(filter))8 .sort((a, b) => expensiveSort(a, b)); // Calcul coûteux9
10 return <div>{filteredProducts.map(...)}</div>;11}12
13// ✅ SOLUTION : useMemo pour memoizer le résultat14function ProductList({ products }: { products: Product[] }) {15 const [filter, setFilter] = useState('');16
17 const filteredProducts = useMemo(18 () => products19 .filter(p => p.name.includes(filter))20 .sort((a, b) => expensiveSort(a, b)),21 [products, filter] // ✅ Re-calcule uniquement si products ou filter change22 );23
24 return <div>{filteredProducts.map(...)}</div>;25}26
27// ⚠️ N'utilisez useMemo que si :28// 1. Le calcul est coûteux (>10ms)29// 2. La valeur est passée à React.memo ou useEffect deps30// Sinon, useMemo ajoute de la complexité inutile !Cas d'utilisation concrets de useMemo
- 1. Filtrage et tri de listes longues : Liste de >1000 produits avec recherche/filtres multiples
- 2. Agrégations de données : Calcul de sommes, moyennes, groupBy sur datasets volumineux
- 3. Transformations coûteuses : Parsing de JSON complexe, formatting de dates, conversions d'unités
- 4. Objets de configuration : Création d'objets complexes passés à des composants memoizés
- 5. Calculs mathématiques : Matrices, statistiques, algorithmes récursifs ou itératifs lourds
- 6. Dérivation d'état : Calculer un état dérivé complexe à partir du state principal
3. useCallback : Memoization de Fonction
1// ❌ PROBLÈME : Nouvelle fonction à chaque render2function ProductList({ products }: { products: Product[] }) {3 const [count, setCount] = useState(0);4
5 // ⚠️ handleClick est une NOUVELLE fonction à chaque render6 // ProductCard re-render même si props.product identique (onClick change)7 const handleClick = (id: string) => {8 console.log('Clicked', id);9 };10
11 return (12 <div>13 <button onClick={() => setCount(count + 1)}>Count: {count}</button>14 {products.map(p => <ProductCard key={p.id} product={p} onClick={handleClick} />)}15 </div>16 );17}18
19// ✅ SOLUTION : useCallback pour référence stable20function ProductList({ products }: { products: Product[] }) {21 const [count, setCount] = useState(0);22
23 // ✅ handleClick garde la même référence entre renders24 const handleClick = useCallback((id: string) => {25 console.log('Clicked', id);26 }, []); // Deps vide = fonction jamais re-créée27
28 return (29 <div>30 <button onClick={() => setCount(count + 1)}>Count: {count}</button>31 {products.map(p => <ProductCard key={p.id} product={p} onClick={handleClick} />)}32 </div>33 );34}35
36// ⚠️ Si fonction dépend de state/props :37const handleClick = useCallback((id: string) => {38 console.log('Clicked', id, count); // count dans closure39}, [count]); // ✅ Re-créée uniquement si count changeCas d'utilisation concrets de useCallback
- 1. Event handlers dans des listes : onClick, onChange passés à des items de liste memoizés
- 2. Callbacks avec debounce/throttle : Recherche en temps réel, scroll handlers, resize listeners
- 3. Callbacks dans useEffect dependencies : Éviter re-exécution d'effet si fonction stable
- 4. Callbacks passés à des composants memoizés : Préserver les bénéfices de React.memo
- 5. Fonctions dans Context : Fonctions partagées via Context pour éviter re-render global
- 6. Handlers de formulaire complexes : Validation, transformation de données, soumission async
4. Virtualization : Listes Longues
1// ❌ PROBLÈME : Render 10 000 éléments (lag + freeze)2function ProductList({ products }: { products: Product[] }) {3 return (4 <div>5 {products.map(p => <ProductCard key={p.id} product={p} />)} {/* 10 000 DOM nodes ! */}6 </div>7 );8}9
10// ✅ SOLUTION : react-window (virtualization)11import { FixedSizeList } from 'react-window';12
13function ProductList({ products }: { products: Product[] }) {14 return (15 <FixedSizeList16 height={600} // Hauteur container17 itemCount={products.length} // Nombre total items18 itemSize={100} // Hauteur d'un item19 width="100%"20 >21 {({ index, style }) => (22 <div style={style}>23 <ProductCard product={products[index]} />24 </div>25 )}26 </FixedSizeList>27 );28}29
30// Impact :31// Avant : 10 000 DOM nodes32// Après : ~10 DOM nodes (seulement les visibles)33// Performance : 60 FPS vs 5 FPSState Management : Choisir le Bon Outil
Guide de décision pour choisir entre useState, Context, Zustand et React Query selon le cas d'usage.
- Simple
- Pas de dépendance externe
- Performant
- Prop drilling
- Difficile à partager
- •État formulaire
- •Toggle UI
- •État composant isolé
- Natif React
- Évite prop drilling
- Simple pour état global
- Re-render de tous consumers
- Pas de devtools
- •Theme
- •Langue
- •Auth user
- •Données rarement modifiées
- 2 KB
- API simple
- Performant (re-render sélectif)
- Devtools
- Client-only
- Une librairie de plus
- •UI state complexe
- •Panier
- •Filtres
- •Modals
- Cache intelligent
- Refetch auto
- Optimistic updates
- Devtools
- Courbe d'apprentissage
- Inutile sans API
- •Données API
- •Server state
- •Synchronisation serveur
useState/useReducer | Context API | Zustand | React Query / TanStack Query |
|---|---|---|---|
État local au composant React | Partage d'état global sans prop drilling | State management client minimaliste | Server state management (cache API) |
Avantages
| Avantages
| Avantages
| Avantages
|
Inconvenients
| Inconvenients
| Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
| Cas d'usage
| Cas d'usage
|
Règle de Décision
Error Boundaries : Gérer les Erreurs React
Capturer les erreurs React et afficher un fallback UI sans crasher toute l'application.
1// ✅ Error Boundary avec react-error-boundary2import { ErrorBoundary } from 'react-error-boundary';3
4function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {5 return (6 <div className="error-fallback">7 <h2>Une erreur est survenue</h2>8 <pre>{error.message}</pre>9 <button onClick={resetErrorBoundary}>Réessayer</button>10 </div>11 );12}13
14function App() {15 return (16 <ErrorBoundary17 FallbackComponent={ErrorFallback}18 onReset={() => {19 // Reset app state (ex: clear cache)20 }}21 onError={(error, errorInfo) => {22 // Log error to Sentry/Bugsnag23 console.error('Error caught:', error, errorInfo);24 }}25 >26 <DangerousComponent />27 </ErrorBoundary>28 );29}30
31// ⚠️ Error Boundaries ne capturent PAS :32// - Event handlers (onClick, onChange)33// - Async code (setTimeout, fetch)34// - Server-side rendering35// - Errors dans Error Boundary lui-même36
37// Pour event handlers, utiliser try/catch :38function DangerousButton() {39 const handleClick = async () => {40 try {41 await riskyOperation();42 } catch (error) {43 toast.error('Erreur : ' + error.message);44 }45 };46
47 return <button onClick={handleClick}>Action risquée</button>;48}Pièges Courants à Éviter
Les erreurs React les plus fréquentes et comment les détecter rapidement.
1. useEffect en excès pour dérivation d'état
Calculer directement pendant le render au lieu de useEffect. Utiliser useMemo si coûteux.
2. Multiples useState pour état lié
Utiliser useReducer pour état complexe (ex: form avec loading/error/success).
3. Fonctions inline dans props de composants memoizés
Utiliser useCallback pour référence stable si composant utilise React.memo.
4. Mutation directe de state
Toujours créer un nouvel objet/array (spread operator ou méthodes immutables).
5. Oublier dependencies array dans useEffect/useMemo/useCallback
ESLint exhaustive-deps détecte automatiquement. Toujours activer cette règle.
6. Oublier key prop dans listes
Utiliser ID stable (jamais index). React ne peut pas optimiser les re-renders sans key.
Comment composer vos composants efficacement ?
Les patterns de composition permettent de créer des composants flexibles et maintenables en évitant les props booléens prolifiques. Inspirés des recommandations officielles de Vercel, ces patterns favorisent la composition over configuration.
Éviter les Boolean Props
Les props booléens créent une prolifération de configurations et rendent les composants difficiles à maintenir. Préférer la composition.
Problème
Explosion combinatoire des props booléens
Solution
Composition avec children et slots
1// ❌ ANTI-PATTERN : Boolean props proliferation2interface ModalProps {3 isOpen: boolean;4 onClose: () => void;5 showHeader?: boolean;6 showFooter?: boolean;7 showCloseButton?: boolean;8 size?: 'small' | 'medium' | 'large';9 variant?: 'primary' | 'secondary' | 'danger';10 centered?: boolean;11 fullScreen?: boolean;12 children: React.ReactNode;13}14
15function Modal({16 isOpen,17 onClose,18 showHeader = true,19 showFooter = false,20 showCloseButton = true,21 size = 'medium',22 variant = 'primary',23 centered = false,24 fullScreen = false,25 children26}: ModalProps) {27 // Logique complexe avec 10+ props booléens28 // Difficile à tester, maintenir et étendre29
30 return (31 <div className={cn(32 'modal',33 size === 'small' && 'modal-small',34 size === 'medium' && 'modal-medium',35 size === 'large' && 'modal-large',36 centered && 'modal-centered',37 fullScreen && 'modal-fullscreen'38 )}>39 {showHeader && <div className="modal-header">...</div>}40 <div className="modal-body">{children}</div>41 {showFooter && <div className="modal-footer">...</div>}42 {showCloseButton && <button onClick={onClose}>×</button>}43 </div>44 );45}46
47// Utilisation : Props booléens complexes48<Modal49 isOpen={isOpen}50 onClose={handleClose}51 showHeader={true}52 showFooter={true}53 showCloseButton={false}54 size="large"55 variant="primary"56 centered={true}57>58 Content59</Modal>1// ✅ SOLUTION : Composition avec Compound Components2import { createContext, useContext } from 'react';3
4const ModalContext = createContext<{5 isOpen: boolean;6 onClose: () => void;7} | null>(null);8
9function Modal({ isOpen, onClose, children }: {10 isOpen: boolean;11 onClose: () => void;12 children: React.ReactNode;13}) {14 if (!isOpen) return null;15
16 return (17 <ModalContext.Provider value={{ isOpen, onClose }}>18 <div className="modal-overlay">19 <div className="modal-container">20 {children}21 </div>22 </div>23 </ModalContext.Provider>24 );25}26
27function ModalHeader({ children }: { children: React.ReactNode }) {28 return <div className="modal-header">{children}</div>;29}30
31function ModalBody({ children }: { children: React.ReactNode }) {32 return <div className="modal-body">{children}</div>;33}34
35function ModalFooter({ children }: { children: React.ReactNode }) {36 return <div className="modal-footer">{children}</div>;37}38
39function ModalCloseButton() {40 const context = useContext(ModalContext);41 if (!context) throw new Error('ModalCloseButton must be inside Modal');42
43 return (44 <button onClick={context.onClose} className="modal-close">45 ×46 </button>47 );48}49
50// Export comme namespace51Modal.Header = ModalHeader;52Modal.Body = ModalBody;53Modal.Footer = ModalFooter;54Modal.CloseButton = ModalCloseButton;55
56export { Modal };57
58// Utilisation : Composition déclarative et flexible59<Modal isOpen={isOpen} onClose={handleClose}>60 <Modal.Header>61 <h2>Titre</h2>62 <Modal.CloseButton />63 </Modal.Header>64
65 <Modal.Body>66 <p>Contenu du modal</p>67 </Modal.Body>68
69 <Modal.Footer>70 <button onClick={handleClose}>Annuler</button>71 <button onClick={handleSave}>Enregistrer</button>72 </Modal.Footer>73</Modal>74
75// Variante sans header :76<Modal isOpen={isOpen} onClose={handleClose}>77 <Modal.Body>78 <p>Contenu simple</p>79 </Modal.Body>80</Modal>81
82// Variante avec seulement un CloseButton :83<Modal isOpen={isOpen} onClose={handleClose}>84 <Modal.CloseButton />85 <Modal.Body>86 <p>Contenu avec close button</p>87 </Modal.Body>88</Modal>89
90// Avantages :91// ✅ Flexibilité totale (composer comme souhaité)92// ✅ Pas de props booléens93// ✅ API déclarative et lisible94// ✅ Facile à tester et maintenirCompound Components : API Déclarative
Structure avec contexte partagé pour créer des composants liés (Tabs, Accordion, Select). Pattern inspiré de Radix UI et Shadcn.
1// ✅ Pattern : Compound Components (exemple Tabs)2import { createContext, useContext, useState } from 'react';3
4const TabsContext = createContext<{5 activeTab: string;6 setActiveTab: (tab: string) => void;7} | null>(null);8
9function Tabs({ children, defaultTab }: {10 children: React.ReactNode;11 defaultTab: string;12}) {13 const [activeTab, setActiveTab] = useState(defaultTab);14
15 return (16 <TabsContext.Provider value={{ activeTab, setActiveTab }}>17 <div className="tabs">{children}</div>18 </TabsContext.Provider>19 );20}21
22function TabsList({ children }: { children: React.ReactNode }) {23 return <div className="tabs-list">{children}</div>;24}25
26function TabsTrigger({ value, children }: {27 value: string;28 children: React.ReactNode;29}) {30 const context = useContext(TabsContext);31 if (!context) throw new Error('TabsTrigger must be inside Tabs');32
33 const { activeTab, setActiveTab } = context;34
35 return (36 <button37 onClick={() => setActiveTab(value)}38 className={activeTab === value ? 'active' : ''}39 role="tab"40 aria-selected={activeTab === value}41 >42 {children}43 </button>44 );45}46
47function TabsContent({ value, children }: {48 value: string;49 children: React.ReactNode;50}) {51 const context = useContext(TabsContext);52 if (!context) throw new Error('TabsContent must be inside Tabs');53
54 if (context.activeTab !== value) return null;55
56 return (57 <div className="tabs-content" role="tabpanel">58 {children}59 </div>60 );61}62
63// Export comme namespace64Tabs.List = TabsList;65Tabs.Trigger = TabsTrigger;66Tabs.Content = TabsContent;67
68export { Tabs };69
70// Utilisation : API déclarative71<Tabs defaultTab="account">72 <Tabs.List>73 <Tabs.Trigger value="account">Compte</Tabs.Trigger>74 <Tabs.Trigger value="password">Mot de passe</Tabs.Trigger>75 <Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>76 </Tabs.List>77
78 <Tabs.Content value="account">79 <h2>Paramètres du compte</h2>80 <p>Gérez vos informations personnelles</p>81 </Tabs.Content>82
83 <Tabs.Content value="password">84 <h2>Sécurité</h2>85 <p>Changez votre mot de passe</p>86 </Tabs.Content>87
88 <Tabs.Content value="notifications">89 <h2>Notifications</h2>90 <p>Gérez vos préférences de notifications</p>91 </Tabs.Content>92</Tabs>93
94// Avantages :95// ✅ État partagé via Context (activeTab)96// ✅ API déclarative et self-documenting97// ✅ Type-safe avec TypeScript98// ✅ Accessible (ARIA attributes)99// ✅ Extensible (facile d'ajouter Tabs.Icon, etc.)Children Over Render Props
Privilégier children pour la composition au lieu des render props (renderHeader, renderFooter). Plus simple et idiomatique en React.
1// ❌ ÉVITER : Render Props proliferation2interface CardProps {3 renderHeader?: () => React.ReactNode;4 renderBody: () => React.ReactNode;5 renderFooter?: () => React.ReactNode;6 renderActions?: () => React.ReactNode;7}8
9function Card({ renderHeader, renderBody, renderFooter, renderActions }: CardProps) {10 return (11 <div className="card">12 {renderHeader && <div className="card-header">{renderHeader()}</div>}13 <div className="card-body">{renderBody()}</div>14 {renderFooter && <div className="card-footer">{renderFooter()}</div>}15 {renderActions && <div className="card-actions">{renderActions()}</div>}16 </div>17 );18}19
20// Utilisation : Verbose et peu lisible21<Card22 renderHeader={() => <h2>Titre</h2>}23 renderBody={() => <p>Contenu</p>}24 renderFooter={() => <button>Action</button>}25/>1// ✅ PRÉFÉRER : Children pour composition2function Card({ children }: { children: React.ReactNode }) {3 return <div className="card">{children}</div>;4}5
6function CardHeader({ children }: { children: React.ReactNode }) {7 return <div className="card-header">{children}</div>;8}9
10function CardBody({ children }: { children: React.ReactNode }) {11 return <div className="card-body">{children}</div>;12}13
14function CardFooter({ children }: { children: React.ReactNode }) {15 return <div className="card-footer">{children}</div>;16}17
18// Export namespace19Card.Header = CardHeader;20Card.Body = CardBody;21Card.Footer = CardFooter;22
23export { Card };24
25// Utilisation : Simple et déclaratif26<Card>27 <Card.Header>28 <h2>Titre</h2>29 </Card.Header>30
31 <Card.Body>32 <p>Contenu de la carte</p>33 </Card.Body>34
35 <Card.Footer>36 <button>Action</button>37 </Card.Footer>38</Card>39
40// Variante : Composition flexible41<Card>42 <Card.Body>43 <p>Carte simple sans header ni footer</p>44 </Card.Body>45</Card>46
47// Avantages :48// ✅ Plus lisible (pas de fonctions imbriquées)49// ✅ Type-safe automatiquement50// ✅ Permet composition libre51// ✅ Idiomatique ReactState-Context-Interface : Dependency Injection
Séparer l'état (state), le contexte (context) et l'interface (composants) pour créer des APIs génériques et testables.
1// ✅ Pattern : State-Context-Interface2// 1. STATE : Logique métier (réutilisable et testable)3import { useState } from 'react';4
5export function useAccordionState(defaultOpen?: string) {6 const [openItem, setOpenItem] = useState<string | null>(defaultOpen ?? null);7
8 const toggle = (item: string) => {9 setOpenItem((current) => (current === item ? null : item));10 };11
12 const isOpen = (item: string) => openItem === item;13
14 return { openItem, toggle, isOpen };15}16
17// 2. CONTEXT : Partage de l'état (dependency injection)18import { createContext, useContext } from 'react';19
20type AccordionContextValue = ReturnType<typeof useAccordionState>;21
22const AccordionContext = createContext<AccordionContextValue | null>(null);23
24export function useAccordionContext() {25 const context = useContext(AccordionContext);26 if (!context) {27 throw new Error('Accordion components must be inside Accordion');28 }29 return context;30}31
32// 3. INTERFACE : Composants UI (consomment le contexte)33function Accordion({ children, defaultOpen }: {34 children: React.ReactNode;35 defaultOpen?: string;36}) {37 const state = useAccordionState(defaultOpen);38
39 return (40 <AccordionContext.Provider value={state}>41 <div className="accordion">{children}</div>42 </AccordionContext.Provider>43 );44}45
46function AccordionItem({ value, children }: {47 value: string;48 children: React.ReactNode;49}) {50 return <div className="accordion-item">{children}</div>;51}52
53function AccordionTrigger({ value, children }: {54 value: string;55 children: React.ReactNode;56}) {57 const { toggle, isOpen } = useAccordionContext();58
59 return (60 <button61 onClick={() => toggle(value)}62 className={isOpen(value) ? 'open' : 'closed'}63 >64 {children}65 </button>66 );67}68
69function AccordionContent({ value, children }: {70 value: string;71 children: React.ReactNode;72}) {73 const { isOpen } = useAccordionContext();74
75 if (!isOpen(value)) return null;76
77 return <div className="accordion-content">{children}</div>;78}79
80// Export81Accordion.Item = AccordionItem;82Accordion.Trigger = AccordionTrigger;83Accordion.Content = AccordionContent;84
85export { Accordion };86
87// Utilisation :88<Accordion defaultOpen="item-1">89 <Accordion.Item value="item-1">90 <Accordion.Trigger value="item-1">91 Question 192 </Accordion.Trigger>93 <Accordion.Content value="item-1">94 Réponse 195 </Accordion.Content>96 </Accordion.Item>97
98 <Accordion.Item value="item-2">99 <Accordion.Trigger value="item-2">100 Question 2101 </Accordion.Trigger>102 <Accordion.Content value="item-2">103 Réponse 2104 </Accordion.Content>105 </Accordion.Item>106</Accordion>107
108// Avantages :109// ✅ État testable indépendamment (useAccordionState)110// ✅ Context pour dependency injection111// ✅ Composants UI découplés de la logique112// ✅ Réutilisable (ex: useAccordionState dans autre UI)Lift State : Partage entre Siblings
Déplacer l'état dans un provider parent pour permettre aux composants frères (siblings) d'y accéder sans prop drilling.
1// ❌ PROBLÈME : Siblings ne peuvent pas partager l'état2function ParentComponent() {3 return (4 <div>5 <ComponentA /> {/* A l'état du panier */}6 <ComponentB /> {/* B veut accéder au panier */}7 </div>8 );9}10
11// ComponentA gère son état local12function ComponentA() {13 const [cart, setCart] = useState<Item[]>([]);14
15 const addItem = (item: Item) => {16 setCart([...cart, item]);17 };18
19 return <ProductList onAddItem={addItem} />;20}21
22// ComponentB ne peut pas accéder au panier de A23function ComponentB() {24 // Comment afficher le nombre d'items dans le panier ?25 return <CartButton itemCount={???} />;26}1// ✅ SOLUTION : Lift State dans un Provider2import { createContext, useContext, useState } from 'react';3
4type CartContextValue = {5 items: Item[];6 addItem: (item: Item) => void;7 removeItem: (id: string) => void;8 itemCount: number;9};10
11const CartContext = createContext<CartContextValue | null>(null);12
13export function CartProvider({ children }: { children: React.ReactNode }) {14 const [items, setItems] = useState<Item[]>([]);15
16 const addItem = (item: Item) => {17 setItems((prev) => [...prev, item]);18 };19
20 const removeItem = (id: string) => {21 setItems((prev) => prev.filter((item) => item.id !== id));22 };23
24 const itemCount = items.length;25
26 return (27 <CartContext.Provider value={{ items, addItem, removeItem, itemCount }}>28 {children}29 </CartContext.Provider>30 );31}32
33export function useCart() {34 const context = useContext(CartContext);35 if (!context) {36 throw new Error('useCart must be inside CartProvider');37 }38 return context;39}40
41// Utilisation : Wrapping parent42function ParentComponent() {43 return (44 <CartProvider>45 <ComponentA />46 <ComponentB />47 </CartProvider>48 );49}50
51// ComponentA : Ajoute au panier52function ComponentA() {53 const { addItem } = useCart(); // ✅ Accès au context54
55 return <ProductList onAddItem={addItem} />;56}57
58// ComponentB : Affiche le nombre d'items59function ComponentB() {60 const { itemCount } = useCart(); // ✅ Accès au context61
62 return <CartButton itemCount={itemCount} />;63}64
65// Avantages :66// ✅ État partagé entre siblings67// ✅ Pas de prop drilling68// ✅ Re-render sélectif (itemCount vs items)69// ✅ Testable (mock CartProvider)Récapitulatif : Choisir le Bon Pattern
Guide de décision pour choisir le pattern de composition adapté à votre cas d'usage.
Compound Components
Pour composants avec état partagé (Tabs, Accordion, Select, Dropdown). Les sous-composants doivent communiquer entre eux.
Children Composition
Pour layouts simples sans état partagé (Card, Modal, Panel). Composition libre sans logique complexe.
State-Context-Interface
Pour composants complexes avec logique métier réutilisable. Séparer état (testable) et interface (UI).
Lift State (Provider)
Pour partager état entre siblings ou composants distants. Éviter prop drilling tout en gardant l'état accessible.
À Éviter
- Props booléens multiples (showX, hasY, isZ)
- Render props excessifs (renderX pour chaque slot)
- Prop drilling sur plus de 2 niveaux
- Logique métier mélangée avec UI dans le composant
Comment structurer votre projet Next.js ?
Une architecture solide est la clé d'un projet maintenable, scalable et testable. Ce guide présente les principes SOLID, la Clean Architecture, et les patterns essentiels pour structurer une application Next.js professionnelle.
Principes SOLID
Les 5 principes fondamentaux de la programmation orientée objet, appliqués au développement React et Next.js moderne.
1// ❌ Violation de Single Responsibility - God Component2export default function UserProfile() {3 const [user, setUser] = useState(null);4 const [posts, setPosts] = useState([]);5 const [analytics, setAnalytics] = useState({});6
7 // Fetch user, posts, analytics, tout est mélangé8 // Logique d'affichage, de validation, de tracking...9 // Ce composant fait TROP de choses10}11
12// ✅ Respect du Single Responsibility13export default function UserProfile({ userId }: { userId: string }) {14 return (15 <div>16 <UserInfo userId={userId} />17 <UserPosts userId={userId} />18 <UserAnalytics userId={userId} />19 </div>20 );21}22
23// Chaque composant a une responsabilité claire et uniqueClean Architecture - 4 Layers
Organisation du code en couches concentriques pour séparer la logique métier de l'infrastructure technique.
1src/2├── features/ # Organisation par feature (recommandé)3│ ├── inspections/4│ │ ├── components/ # Composants UI spécifiques5│ │ │ ├── inspection-list.tsx6│ │ │ └── inspection-detail.tsx7│ │ ├── hooks/ # Hooks métier de la feature8│ │ │ └── use-inspections.ts9│ │ ├── server-actions/ # Mutations serveur10│ │ │ └── actions.ts11│ │ ├── schemas/ # Validation Zod12│ │ │ └── inspection.schema.ts13│ │ └── types/ # Types TypeScript14│ │ └── inspection.types.ts15│ │16│ └── users/17│ ├── components/18│ ├── hooks/19│ └── types/20│21├── lib/ # Utilitaires partagés22│ ├── db/ # Client base de données23│ ├── utils/ # Fonctions utilitaires24│ └── auth/ # Logique d'authentification25│26└── app/ # Next.js App Router27 ├── (dashboard)/28 └── api/29
30# Avantage : tout ce qui concerne "inspections" est dans un seul dossier31# Scalable, facile à maintenir, délimite clairement les responsabilitésDesign Patterns Essentiels
Patterns architecturaux adaptés à Next.js pour structurer efficacement votre application.
InspectionRepository encapsule toutes les requêtes Prisma liées aux inspections.FieldFactory retourne différents composants de formulaire selon le type de champ.PrismaUserAdapter transforme les modèles Prisma en objets conformes à l'interface frontend.useQuery('users') sont automatiquement re-rendus quand les données changent.Testing Strategy - Pyramide de Tests
Une stratégie de tests équilibrée garantit la qualité sans sacrifier la vélocité de développement.
1// Repository Pattern - Abstraction de la couche de données2
3// lib/repositories/inspection.repository.ts4import { prisma } from '@/lib/db';5import type { Inspection } from '@/types/inspection';6
7export class InspectionRepository {8 async findAll(): Promise<Inspection[]> {9 return await prisma.inspection.findMany({10 include: { user: true, site: true }11 });12 }13
14 async findById(id: string): Promise<Inspection | null> {15 return await prisma.inspection.findUnique({16 where: { id },17 include: { user: true, site: true }18 });19 }20
21 async create(data: CreateInspectionInput): Promise<Inspection> {22 return await prisma.inspection.create({23 data,24 include: { user: true, site: true }25 });26 }27
28 async delete(id: string): Promise<void> {29 await prisma.inspection.delete({ where: { id } });30 }31}32
33// Utilisation dans une Server Action34import { InspectionRepository } from '@/lib/repositories/inspection.repository';35
36export async function getInspections() {37 const repo = new InspectionRepository();38 return await repo.findAll();39}40
41// Avantage : Si vous changez de DB (Prisma → Drizzle),42// vous ne modifiez QUE le repository, pas toute l'applicationPièges à éviter
Comment rendre votre app Next.js accessible ?
L'accessibilité (a11y) garantit que votre application est utilisable par tous, y compris les personnes en situation de handicap. Elle améliore l'expérience utilisateur globale, le SEO, et réduit les risques légaux. Ce guide couvre les principes WCAG 2.1 et les techniques pratiques.
WCAG 2.1 - Les 4 Principes POUR
Web Content Accessibility Guidelines définit 4 principes fondamentaux pour rendre le contenu web accessible à tous.
lang="fr" pour spécifier la langue1// ❌ MAUVAIS - Div soup sans sémantique2<div className="header">3 <div className="nav">4 <div className="link">Home</div>5 <div className="link">About</div>6 </div>7</div>8<div className="main">9 <div className="article">10 <div className="title">Mon article</div>11 <div className="content">Contenu...</div>12 </div>13</div>14
15// ✅ BON - HTML sémantique16<header>17 <nav>18 <ul>19 <li><a href="/">Home</a></li>20 <li><a href="/about">About</a></li>21 </ul>22 </nav>23</header>24<main>25 <article>26 <h1>Mon article</h1>27 <p>Contenu...</p>28 </article>29</main>30
31// Avantages :32// - Screen readers comprennent la structure33// - SEO amélioré (moteurs de recherche comprennent mieux)34// - Navigation au clavier native (liens vs divs cliquables)35// - Maintenance plus facile (la sémantique documente l'intention)Boutons vs Liens - Quand utiliser quoi ?
Choisir le bon élément HTML selon le contexte est crucial pour l'accessibilité et l'expérience utilisateur.
- Action sur la page actuelle (submit form, ouvrir modal, toggle menu)
- Modification d'état (ajouter au panier, liker)
- Déclencher du JavaScript
- Navigation vers une autre page ou section
- Téléchargement de fichier
- Ancre vers une section (#section-id)
ARIA Attributes - Attributs d'Accessibilité
ARIA (Accessible Rich Internet Applications) enrichit la sémantique HTML pour les technologies d'assistance.
role="dialog" - Modal ou dialoguearia-modal="true" - Indique que c'est une vraie modal (bloque le reste de la page)role="alert" - Message important (erreur, succès)aria-label="Fermer" - Label invisible mais lu par les screen readersaria-labelledby="title-id" - Référence un autre élément comme labelaria-describedby="desc-id" - Ajoute une description supplémentairearia-expanded="true" - Menu déroulant ouvert/ferméaria-selected="true" - Item sélectionné dans une listearia-disabled="true" - Élément désactivéaria-live="polite" - Zone qui se met à jour dynamiquement (notifications)1// Exemple complet : Modal accessible avec ARIA2
3export function AccessibleModal({4 isOpen,5 onClose,6 title,7 children8}: ModalProps) {9 useEffect(() => {10 // Focus trap - empêcher la navigation au clavier en dehors de la modal11 if (isOpen) {12 document.body.style.overflow = 'hidden';13 // Focus sur le premier élément interactif14 const firstFocusable = modalRef.current?.querySelector('button, [href], input');15 (firstFocusable as HTMLElement)?.focus();16 } else {17 document.body.style.overflow = '';18 }19 }, [isOpen]);20
21 if (!isOpen) return null;22
23 return (24 <div25 className="fixed inset-0 bg-black/50 flex items-center justify-center"26 onClick={onClose}27 aria-label="Overlay de la modal"28 >29 <div30 ref={modalRef}31 role="dialog"32 aria-modal="true"33 aria-labelledby="modal-title"34 aria-describedby="modal-description"35 className="bg-white p-6 rounded-lg shadow-xl max-w-md"36 onClick={(e) => e.stopPropagation()} // Empêcher fermeture au clic sur le contenu37 >38 <h2 id="modal-title" className="text-xl font-bold mb-4">39 {title}40 </h2>41 <div id="modal-description">42 {children}43 </div>44 <button45 onClick={onClose}46 aria-label="Fermer la modal"47 className="mt-4 px-4 py-2 bg-primary text-white rounded"48 >49 Fermer50 </button>51 </div>52 </div>53 );54}Navigation au Clavier
Permettre la navigation complète au clavier est essentiel pour l'accessibilité. Beaucoup d'utilisateurs n'utilisent pas de souris.
button:focus-visible { outline: 2px solid hsl(var(--primary)) }<a href="#main-content" className="skip-link">Skip to main content</a>focus-trap-react.Color Contrast - Contraste des Couleurs
Un contraste insuffisant rend le texte illisible pour les personnes malvoyantes ou en situation de forte luminosité.
- 4.5:1 minimum pour le texte standard
- 3:1 minimum pour le texte large (18px+ ou 14px+ gras) et les éléments UI
- 7:1 minimum pour le texte standard
- 4.5:1 minimum pour le texte large
Screen Readers - Lecteurs d'Écran
Les screen readers vocalisent le contenu web pour les utilisateurs aveugles ou malvoyants.
Pièges à éviter en Accessibilité
<div onClick={...}> au lieu de <button>. Les divs ne sont pas focusables au clavier par défaut et ne communiquent pas leur rôle aux screen readers.alt aux images. Si l'image est purement décorative, utilisez alt="" (vide).<label> associé via l'attribut htmlFor. Les placeholders ne suffisent pas.Quels patterns avancés pour aller plus loin ?
Les patterns avancés de Next.js permettent de créer des expériences utilisateur exceptionnelles grâce au streaming, aux parallel routes, au middleware edge, aux optimistic updates, au real-time et à l'internationalisation.
1. Streaming avec Suspense
Afficher progressivement le contenu au fur et à mesure de son chargement plutôt que d'attendre tout le contenu. Améliore le Time to First Byte (TTFB) et l'expérience utilisateur.
Exemple de Base : Suspense Multi-level
1import { Suspense } from 'react';2
3export default function DashboardPage() {4 return (5 <div>6 {/* Header s'affiche immédiatement */}7 <h1>Dashboard</h1>8
9 {/* Suspense Level 1 : Stats principales */}10 <Suspense fallback={<StatsSkeleton />}>11 <ExpensiveStats /> {/* Chargé en streaming */}12 </Suspense>13
14 {/* Suspense Level 2 : Graphiques */}15 <Suspense fallback={<ChartSkeleton />}>16 <AnalyticsChart /> {/* Chargé en parallèle */}17 </Suspense>18
19 {/* Suspense Level 3 : Activité récente */}20 <Suspense fallback={<ActivitySkeleton />}>21 <RecentActivity /> {/* Chargé en dernier */}22 </Suspense>23 </div>24 );25}26
27// Composant avec fetch asynchrone28async function ExpensiveStats() {29 // Simule un fetch lent (3 secondes)30 const stats = await fetch('https://api.example.com/stats', {31 next: { revalidate: 60 }32 }).then(res => res.json());33
34 return (35 <div className="grid grid-cols-4 gap-4">36 {stats.map((stat) => (37 <div key={stat.id} className="p-4 bg-card rounded-lg">38 <div className="text-2xl font-bold">{stat.value}</div>39 <div className="text-sm text-muted-foreground">{stat.label}</div>40 </div>41 ))}42 </div>43 );44}Convention Next.js : loading.tsx
1// app/dashboard/loading.tsx2// Next.js génère automatiquement un Suspense boundary3export default function Loading() {4 return (5 <div className="space-y-4">6 <div className="h-8 w-48 bg-muted/50 rounded animate-pulse" />7 <div className="grid grid-cols-4 gap-4">8 {[1, 2, 3, 4].map((i) => (9 <div key={i} className="h-24 bg-muted/50 rounded animate-pulse" />10 ))}11 </div>12 </div>13 );14}15
16// Équivalent manuel :17// <Suspense fallback={<Loading />}>18// <DashboardPage />19// </Suspense>Timeline de Chargement
2. Parallel Routes (@slot)
Afficher plusieurs pages en parallèle dans le même layout. Parfait pour modals interceptées, dashboards multi-sections, ou A/B testing.
Structure du Projet
1app/2├── @modal/ # Slot "modal"3│ ├── default.tsx # Page par défaut (modal cachée)4│ ├── login/5│ │ └── page.tsx # Modal de login6│ └── signup/7│ └── page.tsx # Modal de signup8├── @sidebar/ # Slot "sidebar"9│ ├── default.tsx10│ └── page.tsx11├── layout.tsx # Layout utilisant les slots12└── page.tsx # Page principaleLayout Utilisant les Slots
1// app/layout.tsx2export default function Layout({3 children, // Page principale4 modal, // Slot @modal5 sidebar // Slot @sidebar6}: {7 children: React.ReactNode;8 modal: React.ReactNode;9 sidebar: React.ReactNode;10}) {11 return (12 <div className="flex">13 {/* Sidebar (chargée en parallèle) */}14 <aside className="w-64">{sidebar}</aside>15
16 {/* Contenu principal */}17 <main className="flex-1">{children}</main>18
19 {/* Modal (chargée en parallèle, conditionnellement visible) */}20 {modal}21 </div>22 );23}24
25// app/@modal/default.tsx26export default function ModalDefault() {27 return null; // Pas de modal par défaut28}29
30// app/@modal/login/page.tsx31export default function LoginModal() {32 return (33 <div className="fixed inset-0 bg-black/50 flex items-center justify-center">34 <div className="bg-background p-8 rounded-lg">35 <h2>Login</h2>36 <form>{/* ... */}</form>37 </div>38 </div>39 );40}Modals Interceptées
Afficher un produit en modal sur /products, en page complète sur /products/[id]
Dashboards
Sidebar avec navigation, contenu principal, et panneau de notifications
A/B Testing
Afficher deux variantes d'une page en parallèle selon cookie/segment
3. Middleware (Edge Runtime)
Exécuter du code avant qu'une requête soit traitée, directement sur le CDN (Edge). Parfait pour auth check, A/B testing, redirections, et i18n.
Middleware : Auth Check + A/B Testing + i18n
1// middleware.ts (à la racine du projet)2import { NextRequest, NextResponse } from 'next/server';3
4export function middleware(request: NextRequest) {5 const { pathname } = request.nextUrl;6
7 // 1. Auth Check : Rediriger vers /login si non authentifié8 const token = request.cookies.get('auth-token');9 if (pathname.startsWith('/dashboard') && !token) {10 return NextResponse.redirect(new URL('/login', request.url));11 }12
13 // 2. A/B Testing : Afficher variante selon cookie14 const variant = request.cookies.get('ab-variant') || 'A';15 const response = NextResponse.next();16 if (!request.cookies.has('ab-variant')) {17 // Assigner variant aléatoirement18 const randomVariant = Math.random() > 0.5 ? 'A' : 'B';19 response.cookies.set('ab-variant', randomVariant);20 }21
22 // 3. i18n : Rediriger selon langue navigateur23 const locale = request.headers.get('accept-language')?.split(',')[0] || 'en';24 if (pathname === '/' && !pathname.startsWith('/fr') && locale === 'fr') {25 return NextResponse.redirect(new URL('/fr', request.url));26 }27
28 // 4. Headers personnalisés29 response.headers.set('x-middleware-version', '1.0');30
31 return response;32}33
34// Configurer les paths où le middleware s'applique35export const config = {36 matcher: [37 '/',38 '/dashboard/:path*',39 '/((?!api|_next/static|_next/image|favicon.ico).*)'40 ]41};Limitations du Edge Runtime
- •Pas d'accès aux APIs Node.js (fs, crypto natif, etc.)
- •Pas de connexion directe à PostgreSQL/MySQL (utiliser API REST)
- •Timeout de 10 secondes maximum
- •Limité à 1 MB de code (middleware léger uniquement)
4. Optimistic Updates avec useOptimistic
Mettre à jour l'UI instantanément avant la réponse serveur, puis rollback si erreur. Hook React 19 natif pour une UX ultra-réactive.
Exemple : Todo List avec useOptimistic
1'use client';2
3import { useOptimistic, useTransition } from 'react';4import { addTodo } from './actions';5
6export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {7 const [isPending, startTransition] = useTransition();8
9 // State optimiste : UI mise à jour instantanément10 const [optimisticTodos, addOptimisticTodo] = useOptimistic(11 initialTodos,12 (state, newTodo: Todo) => [...state, newTodo]13 );14
15 const handleSubmit = async (formData: FormData) => {16 const text = formData.get('text') as string;17
18 // Créer un todo temporaire19 const tempTodo = {20 id: 'temp-' + Date.now(),21 text,22 completed: false,23 createdAt: new Date()24 };25
26 startTransition(async () => {27 // 1. UI mise à jour instantanément (optimiste)28 addOptimisticTodo(tempTodo);29
30 try {31 // 2. Server action en arrière-plan32 await addTodo(text);33 // 3. Revalidation automatique, vrai todo affiché34 } catch (error) {35 // 4. Rollback automatique si erreur36 console.error('Failed to add todo:', error);37 // useOptimistic gère le rollback automatiquement38 }39 });40 };41
42 return (43 <div>44 <form action={handleSubmit}>45 <input name="text" placeholder="Nouvelle tâche..." />46 <button type="submit" disabled={isPending}>47 Ajouter48 </button>49 </form>50
51 <ul className="space-y-2 mt-4">52 {optimisticTodos.map((todo) => (53 <li54 key={todo.id}55 className={todo.id.startsWith('temp-') ? 'opacity-50' : ''}56 >57 {todo.text}58 </li>59 ))}60 </ul>61 </div>62 );63}64
65// app/actions.ts66'use server';67
68import { revalidatePath } from 'next/cache';69
70export async function addTodo(text: string) {71 await db.todo.create({ data: { text } });72 revalidatePath('/todos');73}Quand Utiliser ?
- Feedback immédiat
- Meilleure UX
- Pas de spinner
- Complexité accrue
- Gestion rollback
- Pas pour tout
- •Likes/votes
- •Todos
- •Comments
- •Favoris
- Simple
- Fiable
- Pas de rollback
- Spinner visible
- Latence perceptible
- UX dégradée
- •Paiements
- •Mutations critiques
- •Opérations irréversibles
Optimistic Updates | Server Mutations Classiques |
|---|---|
UI mise à jour instantanément, rollback si erreur | Attendre la réponse serveur avant mise à jour UI |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
5. Real-time avec Supabase Realtime
Synchroniser l'UI en temps réel avec les changements de base de données. Supabase Realtime écoute les changements PostgreSQL et les diffuse via WebSocket.
Exemple : Inspections en Temps Réel
1'use client';2
3import { useEffect, useState } from 'react';4import { createClient } from '@/lib/supabase/client';5
6export function InspectionsLiveList() {7 const [inspections, setInspections] = useState<Inspection[]>([]);8 const supabase = createClient();9
10 useEffect(() => {11 // 1. Charger les données initiales12 async function loadInspections() {13 const { data } = await supabase14 .from('inspections')15 .select('*')16 .order('created_at', { ascending: false });17
18 if (data) setInspections(data);19 }20
21 loadInspections();22
23 // 2. Écouter les changements en temps réel24 const channel = supabase25 .channel('inspections-changes')26 .on(27 'postgres_changes',28 {29 event: '*', // INSERT, UPDATE, DELETE30 schema: 'public',31 table: 'inspections'32 },33 (payload) => {34 if (payload.eventType === 'INSERT') {35 // Nouvelle inspection ajoutée36 setInspections((prev) => [payload.new as Inspection, ...prev]);37 } else if (payload.eventType === 'UPDATE') {38 // Inspection modifiée39 setInspections((prev) =>40 prev.map((i) =>41 i.id === payload.new.id ? (payload.new as Inspection) : i42 )43 );44 } else if (payload.eventType === 'DELETE') {45 // Inspection supprimée46 setInspections((prev) =>47 prev.filter((i) => i.id !== payload.old.id)48 );49 }50 }51 )52 .subscribe();53
54 // 3. Cleanup : Unsubscribe au démontage55 return () => {56 supabase.removeChannel(channel);57 };58 }, [supabase]);59
60 return (61 <div className="space-y-4">62 {inspections.map((inspection) => (63 <div key={inspection.id} className="p-4 bg-card rounded-lg border">64 <h3 className="font-bold">{inspection.site_name}</h3>65 <p className="text-sm text-muted-foreground">66 {inspection.status}67 </p>68 </div>69 ))}70 </div>71 );72}Quand Utiliser
- • Dashboards multi-utilisateurs
- • Chat et messagerie
- • Notifications en temps réel
- • Suivi de livraison/statut
- • Éditeurs collaboratifs
Attention
- • Toujours cleanup avec removeChannel()
- • Limiter le nombre de subscriptions
- • Filtrer côté serveur (RLS Supabase)
- • Désactiver si utilisateur inactif
6. Internationalisation (i18n) avec next-intl
Gérer plusieurs langues avec traductions, formatage de dates/nombres, et SEO optimisé. next-intl s'intègre parfaitement avec l'App Router.
Structure du Projet
1messages/2├── en.json # Anglais3├── fr.json # Français4└── es.json # Espagnol5
6app/7├── [locale]/ # Dynamic segment pour langue8│ ├── layout.tsx # Layout par langue9│ ├── page.tsx # Page d'accueil traduite10│ └── about/11│ └── page.tsx # Page About traduite12└── i18n.ts # Configuration i18nFichiers de Traduction
1// messages/en.json2{3 "home": {4 "title": "Welcome to maxpaths",5 "subtitle": "Learn Next.js with interactive courses"6 },7 "nav": {8 "courses": "Courses",9 "about": "About"10 }11}12
13// messages/fr.json14{15 "home": {16 "title": "Bienvenue sur maxpaths",17 "subtitle": "Apprenez Next.js avec des guides interactifs"18 },19 "nav": {20 "courses": "Guides",21 "about": "À propos"22 }23}Utilisation dans un Composant
1// app/[locale]/page.tsx2import { useTranslations } from 'next-intl';3import { unstable_setRequestLocale } from 'next-intl/server';4
5export default function HomePage({ params: { locale } }: { params: { locale: string } }) {6 unstable_setRequestLocale(locale);7 const t = useTranslations('home');8
9 return (10 <div>11 <h1>{t('title')}</h1>12 <p>{t('subtitle')}</p>13 </div>14 );15}16
17// Générer les routes pour chaque locale18export function generateStaticParams() {19 return [{ locale: 'en' }, { locale: 'fr' }, { locale: 'es' }];20}SEO : hreflang + sitemap
1// app/[locale]/layout.tsx2export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) {3 const t = await getTranslations({ locale, namespace: 'metadata' });4
5 return {6 title: t('title'),7 description: t('description'),8 alternates: {9 canonical: `https://maxpaths.com/${locale}`,10 languages: {11 'en': 'https://maxpaths.com/en',12 'fr': 'https://maxpaths.com/fr',13 'es': 'https://maxpaths.com/es'14 }15 }16 };17}18
19// app/sitemap.ts20export default function sitemap() {21 const locales = ['en', 'fr', 'es'];22
23 return locales.flatMap((locale) => [24 {25 url: `https://maxpaths.com/${locale}`,26 lastModified: new Date(),27 changeFrequency: 'weekly',28 priority: 129 },30 {31 url: `https://maxpaths.com/${locale}/courses`,32 lastModified: new Date(),33 changeFrequency: 'daily',34 priority: 0.835 }36 ]);37}Pièges à Éviter avec les Patterns Avancés
Erreurs courantes lors de l'utilisation de patterns avancés et comment les éviter
Suspense partout
N'utilisez Suspense que pour les composants qui prennent plus de 200-300ms à charger. Trop de Suspense boundaries peut fragmenter le HTML et ralentir le rendu.
Parallel routes trop complexes
Limitez-vous à 2-3 slots maximum. Au-delà, le routing devient difficile à maintenir et peut créer des bugs de navigation.
Middleware lourd
Le middleware s'exécute sur chaque requête. Évitez les opérations lourdes (fetch API, parsing JSON volumineux). Pas d'accès aux APIs Node.js (fs, crypto, database drivers).
Optimistic updates sans rollback
useOptimistic gère le rollback automatiquement, mais vous devez gérer les erreurs utilisateur (toast, notification). Ne l'utilisez jamais pour des mutations critiques (paiements, suppressions définitives).
Real-time sans cleanup
Toujours unsubscribe dans le cleanup de useEffect. Oublier le cleanup crée des memory leaks et des subscriptions multiples.
i18n sans SEO
Toujours générer hreflang tags et sitemap.xml pour chaque langue. Sans cela, Google ne référencera qu'une seule version de votre site.
SSR vs SSG vs ISR : lequel choisir ?
Comprenez les différences entre les modes de rendu pour choisir la meilleure stratégie selon votre cas d'usage.
- Données toujours à jour
- Personnalisation par utilisateur
- SEO optimal
- Temps de réponse variable
- Charge serveur élevée
- Coûts de serveur
- •Dashboards utilisateur
- •Flux sociaux
- •Contenu personnalisé
- Vitesse maximale
- Hébergement économique
- Mise en cache CDN
- Données figées au build
- Rebuild nécessaire pour MAJ
- Pas de personnalisation
- •Blog & documentation
- •Landing pages
- •Sites marketing
- Vitesse du statique
- Données périodiquement fraîches
- Scalabilité
- Données légèrement obsolètes
- Configuration revalidate
- Complexité accrue
- •E-commerce (produits)
- •Actualités
- •Agrégateurs de contenu
- Interactivité totale
- Pas de serveur nécessaire
- Expérience fluide
- SEO limité
- Temps de chargement initial
- Bundle JavaScript lourd
- •Applications SPA
- •Outils interactifs
- •Dashboards temps réel
SSR | SSG | ISR | Client |
|---|---|---|---|
Rendu serveur à chaque requête | Génération statique au build | Régénération incrémentale | Rendu côté navigateur |
Avantages
| Avantages
| Avantages
| Avantages
|
Inconvenients
| Inconvenients
| Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
| Cas d'usage
| Cas d'usage
|
Comparez visuellement SSR, SSG, ISR, CSR et Streaming avec des timelines animees et des metriques Core Web Vitals detaillees.
Stratégie Hybride
La puissance de Next.js réside dans la possibilité de mixer plusieurs modes de rendu dans une même application.
Exemple d'architecture hybride :
- →Page d'accueil : SSG (contenu marketing)
- →Liste produits : ISR revalidate 300 (5 min)
- →Page produit : ISR revalidate 60 (1 min)
- →Panier : Client Component (interactivité)
- →Dashboard utilisateur : SSR (données personnelles)
1// Architecture hybride dans un même projet2
3// 1. Page d'accueil - SSG4// app/page.tsx5export default async function Home() {6 const features = await fetch('https://api.example.com/features', {7 cache: 'force-cache' // Statique8 }).then(res => res.json());9
10 return <FeaturesGrid features={features} />;11}12
13// 2. Liste produits - ISR14// app/products/page.tsx15export default async function Products() {16 const products = await fetch('https://api.example.com/products', {17 next: { revalidate: 300 } // ISR 5 minutes18 }).then(res => res.json());19
20 return <ProductList products={products} />;21}22
23// 3. Dashboard utilisateur - SSR24// app/dashboard/page.tsx25export default async function Dashboard() {26 const userData = await fetch('https://api.example.com/user', {27 cache: 'no-store' // SSR, toujours frais28 }).then(res => res.json());29
30 return <UserDashboard data={userData} />;31}32
33// 4. Panier - Client Component34// components/cart.tsx35'use client';36import { useState } from 'react';37
38export function Cart() {39 const [items, setItems] = useState([]);40 // Interactivité totale côté client41 return <CartUI items={items} />;42}Conclusion
Il n'y a pas de solution unique. Analysez vos besoins en termes de performance, fraîcheur des données, SEO et interactivité pour choisir le bon mode de rendu pour chaque partie de votre application. Next.js vous donne cette flexibilité.
Felicitations !
Vous avez termine ce guide.