Maxpaths
Fondamentaux·Section 1/20

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
Structure du projettypescript
1// Structure d'un projet Next.js 16
2app/
3├── layout.tsx // Layout racine
4├── page.tsx // Page d'accueil
5├── about/
6│ └── page.tsx // Route /about
7└── blog/
8 ├── page.tsx // Liste des articles
9 └── [slug]/
10 └── page.tsx // Article individuel
11
12// Exemple de page simple
13export default function Home() {
14 return (
15 <main>
16 <h1>Bienvenue sur Next.js 16</h1>
17 </main>
18 );
19}
Modes de Rendu·Section 2/20

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.

1

Client demande une page

2

Serveur exécute le composant React

3

Données récupérées (API, DB)

4

HTML complet envoyé au client

app/users/[id]/page.tsxtypescript
1// Page SSR avec données dynamiques
2export default async function UserProfile({
3 params
4}: {
5 params: Promise<{ id: string }>
6}) {
7 const { id } = await params;
8
9 // Cette fonction s'exécute côté serveur à chaque requête
10 const user = await fetch(`https://api.example.com/users/${id}`, {
11 cache: 'no-store' // Pas de cache, données toujours fraîches
12 }).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 16
SSR

Démo SSR en temps réel

Cliquez pour simuler une requête SSR. Notez que l'heure change à chaque chargement.

Modes de Rendu·Section 3/20

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
app/blog/[slug]/page.tsxtypescript
1// Page statique générée au build
2export default async function BlogPost({
3 params
4}: {
5 params: Promise<{ slug: string }>
6}) {
7 const { slug } = await params;
8
9 // Cette fonction s'exécute UNE SEULE FOIS au build
10 const post = await fetch(`https://api.example.com/posts/${slug}`, {
11 cache: 'force-cache' // Cache permanent
12 }).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 build
23export 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}
SSG

Démo SSG

Simulez une page statique. L'heure reste fixe car elle a été générée au build.

Modes de Rendu·Section 4/20

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.

1

Première requête : génération au build

2

Requêtes suivantes : page statique en cache

3

Après X secondes : régénération en arrière-plan

4

Nouvelle version servie aux prochains visiteurs

app/products/[id]/page.tsxtypescript
1// Page ISR avec revalidation toutes les 60 secondes
2export default async function ProductPage({
3 params
4}: {
5 params: Promise<{ id: string }>
6}) {
7 const { id } = await params;
8
9 // Revalidation toutes les 60 secondes
10 const product = await fetch(`https://api.example.com/products/${id}`, {
11 next: { revalidate: 60 } // Clé magique ISR
12 }).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és
20 </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}
ISR

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.

Modes de Rendu·Section 5/20

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.

Carton plat = HTML minimal (~2KB)
Instructions = JavaScript bundle (~200KB)
Assemblage = Exécution dans le navigateur

Cycle de vie du CSR

Voici ce qui se passe réellement lorsqu'un utilisateur charge une page CSR dans Next.js.

1
Requête initiale
Navigateur demande la page au serveur Next.js
2
HTML minimal (~2KB)
Serveur renvoie un squelette avec <div id="root"></div>
3
Download JS (~200KB)
Téléchargement du bundle React + Next.js + votre code
4
Exécution JavaScript
React monte le composant et exécute useEffect
5
Fetch client
Appel API depuis le navigateur pour récupérer les données
6
Contenu visible (2-3 secondes)
L'utilisateur voit enfin le contenu final
app/users/page.tsxtsx
1'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 composant
16 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 render
31
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.

TTFB rapide - Le serveur répond instantanément avec du HTML vide
FCP lent - First Contentful Paint retardé par le téléchargement et l'exécution JS
SEO limité - Les robots voient un HTML vide (sauf si rendu dynamique activé)
Interactivité maximale - Toute la logique React est disponible immédiatement après hydratation

Quand utiliser le CSR ?

Le Client-Side Rendering brille dans des contextes spécifiques où l'interactivité prime sur le SEO.

Cas d'usage idéaux
  • 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

1. Tout mettre en CSR
Évitez de marquer toute votre application avec 'use client'. Privilégiez le rendu serveur par défaut et isolez uniquement les composants interactifs.
2. Oublier les états de loading
Toujours afficher un skeleton ou spinner pendant le fetch. L'utilisateur ne doit jamais voir une page blanche.
3. Abuser de useEffect
Évitez les chaînes de 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.
4. Ignorer les erreurs de fetch
Toujours gérer les cas d'erreur avec des try/catch et afficher un message clair à l'utilisateur. Ne laissez jamais l'interface dans un état indéfini.
Modes de Rendu·Section 6/20

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.

Base du gâteau = Server Components (HTML pré-rendu)
Glaçage = Client Components (interactivité JS)
Résultat = Page complète optimisée

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.

app/products/page.tsxtsx
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édiatement
7 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é client
46 setAdded(true);
47 // ... appel API pour ajouter au panier
48 };
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.

components/current-time.tsxtsx
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érent
14 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.

Mauvaise pratique
Marquer tout le layout avec 'use client' force l'envoi de tout le code au client, même les parties statiques.
Bonne pratique
Isoler uniquement le bouton interactif avec 'use client'. Le reste du layout reste en Server Component (HTML pur, 0 JS).
app/layout.tsxtsx
1// ❌ Mauvais : Tout le layout devient client
2'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 client
18import { 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.tsx
32'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.

Cas d'usage idéaux
  • 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

1. 'use client' trop haut dans l'arbre
Placer '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.
2. Passer des fonctions Server → Client
Vous ne pouvez pas passer de fonctions depuis un Server Component vers un Client Component comme props. Les fonctions ne sont pas sérialisables. Utilisez Server Actions à la place.
3. Variables d'environnement privées exposées
Les variables d'environnement utilisées dans des Client Components sont exposées au navigateur. Ne mettez jamais de secrets (clés API privées, tokens) dans des composants marqués 'use client'. Utilisez Route Handlers ou Server Actions pour les appels sécurisés.
4. Oublier les limites de serialization
Seuls les types sérialisables (JSON) peuvent transiter entre Server et Client Components : string, number, boolean, array, object. Les classes, fonctions, Date, Map, Set ne sont pas supportés directement.

Résumé : Quand utiliser quoi ?

Choisissez la bonne stratégie selon vos besoins.

Server Component
Contenu statique, fetch de données, SEO, zéro JavaScript envoyé au client
Client Component
Événements (onClick, onChange), hooks (useState, useEffect), interactivité, animations
Hybrid (recommandé)
Combiner les deux : Server Components pour la structure, Client Components isolés pour l'interaction
Modes de Rendu·Section 7/20

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...
components/counter.tsxtypescript
1'use client'; // Directive obligatoire en haut du fichier
2
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émenter
13 </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 its
21// 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.

Client

Démo Client Component

Ce composant s'exécute côté client et peut gérer l'interactivité en temps réel.

Optimisations·Section 8/20

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

app/dashboard/page.tsxtsx
1import dynamic from 'next/dynamic';
2
3// Chargement lazy d'un composant lourd
4const HeavyChart = dynamic(() => import('./components/heavy-chart'), {
5 loading: () => <div>Chargement du graphique...</div>,
6 ssr: false
7});
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.

Sans Dynamic Import
Bundle initial : 500 KB
Premier rendu : ~6 secondes
Tout chargé en une fois
Avec Dynamic Import
Bundle initial : 50 KB
Premier rendu : ~3 secondes
450 KB chargés à la demande
Amélioration du First Contentful Paint (FCP) de 3 secondes grâce au lazy loading des composants lourds.

Quand Utiliser le Dynamic Import

Scénarios d'Utilisation Optimaux

Le dynamic import est particulièrement efficace dans les situations suivantes.

Charts et visualisations lourdes
Bibliothèques comme Chart.js, D3.js (100-300 KB)
Éditeurs riches (WYSIWYG)
Quill, TinyMCE, Draft.js (200-500 KB)
Modals et dialogues
Chargés uniquement à l'ouverture
Contenu below-the-fold
Sections non visibles au chargement initial

Exemple Complet avec Options

app/articles/new/page.tsxtsx
1import dynamic from 'next/dynamic';
2import { Suspense } from 'react';
3
4// Composant lourd chargé dynamiquement
5const 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 navigateur
18 ssr: false
19 }
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 <RichTextEditor
29 placeholder="Commencez à écrire..."
30 onSave={(content) => console.log(content)}
31 />
32 </div>
33 );
34}

Pièges à Éviter

1. Lazy-load des composants trop petits
tsx
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 principal
5import { Button } from './button';
2. Oublier le loading state
tsx
1// ❌ MAUVAIS - Pas de feedback visuel pendant le chargement
2const Chart = dynamic(() => import('./chart'));
3
4// ✅ BON - Loading state pour UX fluide
5const Chart = dynamic(() => import('./chart'), {
6 loading: () => <Skeleton className="h-64 w-full" />
7});
3. SSR désactivé par défaut sans raison
tsx
1// ❌ MAUVAIS - Désactiver SSR sans raison (perte de SEO)
2const ProductCard = dynamic(() => import('./product-card'), {
3 ssr: false
4});
5
6// ✅ BON - Garder SSR activé si possible
7const ProductCard = dynamic(() => import('./product-card'));
8
9// ✅ BON - Désactiver SSR uniquement si nécessaire
10const BrowserOnlyMap = dynamic(() => import('./map'), {
11 ssr: false // Utilise window/navigator
12});

Points Clés à Retenir

Le dynamic import est un outil puissant d'optimisation, mais doit être utilisé avec discernement.

Réduire le bundle initial de 50-90% dans certains cas
Améliorer le FCP de 2-4 secondes sur connexions lentes
Toujours fournir un loading state pour une UX fluide
Réserver pour composants lourds (100+ KB), pas les petits
Optimisations·Section 9/20

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

app/_lib/server/actions.tstsx
1'use server';
2
3import { z } from 'zod';
4import { revalidatePath } from 'next/cache';
5
6// Schéma de validation
7const 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 Action
13export async function createPost(formData: FormData) {
14 // Validation côté serveur
15 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 cache
34 revalidatePath('/posts');
35
36 return { success: true };
37}

Appel depuis un Client Component

app/posts/create-post-form.tsxtsx
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 erreurs
15 console.error(result.errors);
16 } else {
17 // Succès
18 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.

Type-safety de bout en bout
TypeScript infère automatiquement les types entre client et serveur
Pas de route API nécessaire
Réduction du boilerplate et de la complexité
Progressive Enhancement
Fonctionne même sans JavaScript activé
Validation automatique double
Client (UX rapide) + Serveur (sécurité garantie)

Pattern Avancé avec enhanceAction

app/_lib/server/actions.tstsx
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 automatiques
13export const submitDemoFormAction = enhanceAction(
14 async (data, { user }) => {
15 // data est déjà validé selon FormSchema
16 // user est automatiquement vérifié (auth: true)
17
18 // Logique métier
19 await db.post.create({
20 data: {
21 ...data,
22 authorId: user.id,
23 },
24 });
25
26 // Revalidation
27 revalidatePath('/dashboard');
28
29 return { success: true };
30 },
31 {
32 schema: FormSchema,
33 auth: true, // Requiert authentification
34 }
35);
Note : Le helper 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.

1
Form Submit
L'utilisateur soumet le formulaire
2
Client Validation (optionnelle)
Feedback UX immédiat avant l'envoi
3
startTransition
React marque l'UI comme pending
4
RPC Call
Appel sécurisé vers le serveur
5
Server Validation
Validation Zod + vérification sécurité
6
Authentication Check
Vérification du token/session utilisateur
7
Exécution Métier
Database write, API calls, etc.
8
Revalidation
Mise à jour du cache Next.js

Server Actions vs API Routes

Utiliser Server Actions pour :
Mutations (POST, PUT, DELETE)
Soumission de formulaires
Actions utilisateur (like, vote, save)
Progressive enhancement requis
Utiliser API Routes pour :
Webhooks (Stripe, GitHub, etc.)
API publique (documentation OpenAPI)
Requêtes GET complexes avec cache
Endpoints pour apps mobiles/externes

Pièges à Éviter

1. Validation uniquement côté client
tsx
1// ❌ MAUVAIS - Aucune validation serveur
2'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 obligatoire
10'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}
2. Oublier 'use server' directive
tsx
1// ❌ MAUVAIS - Pas de 'use server' = exécution client
2export async function deletePost(id: string) {
3 await db.post.delete({ where: { id } });
4}
5
6// ✅ BON - 'use server' en haut du fichier
7'use server';
8
9export async function deletePost(id: string) {
10 await db.post.delete({ where: { id } });
11}
3. Retourner des données sensibles
tsx
1// ❌ MAUVAIS - Exposer des données privées au client
2'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écessaires
9'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 public
14 });
15 return user;
16}
4. Pas de revalidation après mutation
tsx
1// ❌ MAUVAIS - Cache non invalidé, UI désynchronisée
2'use server';
3export async function createPost(data: PostData) {
4 await db.post.create({ data });
5 return { success: true };
6}
7
8// ✅ BON - Revalidation du cache
9'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.

Type-safety de bout en bout sans configuration supplémentaire
Toujours valider côté serveur, même avec validation client
Utiliser revalidatePath/revalidateTag après mutations
Privilégier Server Actions pour mutations, API Routes pour webhooks
Optimisations·Section 10/20

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

0s
Requête envoyée
0.5s
Header chargé
1s
UserProfile chargé
3.5s
SlowDataTable chargé
3.5s
Page affichée (TOUT)
LCP : 3.5s - Perception : très lent
Avec Streaming

Shell visible en 0.5s

0s
Requête envoyée
0.5s
Header affiché
1s
UserProfile affiché
1-3.5s
Skeleton visible
3.5s
SlowDataTable affiché
LCP : 0.5s - Perception : instantané

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.

Suspense Unique
Un seul Suspense wrappant toute la page
Avantages
  • Simple à implémenter
  • Moins de code
Inconvenients
  • Page blanche jusqu'à tout charger
  • Mauvaise UX
  • LCP élevé
Cas d'usage
  • À éviter en production
Suspense Granulaires
Multiples Suspense pour chaque composant lent
Avantages
  • Shell HTML instantané
  • Streaming parallèle
  • LCP optimal
Inconvenients
  • Plus de code
  • Complexité accrue
Cas d'usage
  • Dashboard
  • Pages avec données multiples
  • Production
Partial Prerendering (PPR)
Shell statique + slots dynamiques (Next.js 16+)
Avantages
  • Meilleur des deux mondes
  • Shell instantané
  • Données à jour
Inconvenients
  • Experimental
  • Next.js 16+ uniquement
Cas d'usage
  • E-commerce
  • Pages produit
  • Blog avec commentaires
app/dashboard/page.tsxtsx
1// ❌ MAUVAIS : Un seul Suspense global
2import { 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 SlowDataTable
18// - LCP : 3.5s (très mauvais)
19// - Perception : lent et frustrant
app/dashboard/page.tsxtsx
1// ✅ 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 progressif

Pattern #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.

app/dashboard/loading.tsxtsx
1// app/dashboard/loading.tsx
2export 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.tsx
13export 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 Component
20
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 granulaires
49// ❌ É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.

app/product/[id]/page.tsxtsx
1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 experimental: {
5 ppr: 'incremental', // Active PPR de manière incrémentale
6 },
7};
8
9export default nextConfig;
10
11// app/product/[id]/page.tsx
12import { Suspense } from 'react';
13
14export const experimental_ppr = true; // Active PPR pour cette page
15
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 CDN
46// 2. ProductPrice, ProductStock, ProductReviews streamés en parallèle
47// 3. TTFB : 10ms (shell) + streaming progressif (données fraîches)
48// 4. Meilleur des deux mondes : vitesse statique + données dynamiques

Amélioration Mesurable des Core Web Vitals

Impact concret du streaming sur les métriques de performance et l'expérience utilisateur.

Sans Streaming

LCP3.5s

Page blanche jusqu'au chargement complet

INP350ms

Hydratation bloquante de tout le DOM

PerceptionLent

Utilisateur attend 3.5s sans feedback visuel

Avec Streaming

LCP0.5s

-86% grâce au shell HTML instantané

INP120ms

-66% grâce à l'hydratation sélective

PerceptionInstantané

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é)
Optimisations·Section 11/20

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

Objectif : < 2.5s

INP - Interaction to Next Paint

Réactivité aux interactions utilisateur

Objectif : < 200ms

CLS - Cumulative Layout Shift

Stabilité visuelle (éléments qui bougent)

Objectif : < 0.1

TTFB - Time to First Byte

Temps de réponse initial du serveur

Objectif : < 600ms

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

app/components/hero.tsxtsx
1import Image from 'next/image';
2
3export function HeroSection() {
4 return (
5 <Image
6 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 chargement
12 blurDataURL="data:image/..." // Base64 de l'image floue
13 quality={85} // Compression optimale (par défaut 75)
14 />
15 );
16}

Optimiser INP avec Server Actions

app/actions.tstsx
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ée
12 revalidatePath('/profile');
13
14 return { success: true };
15}
16
17// Côté client : useOptimistic pour UI instantanée
18'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ée
28 await updateProfile(formData); // Serveur en arrière-plan
29 }}>
30 <input name="name" defaultValue={optimisticName} />
31 </form>
32 );
33}

Optimiser CLS avec dimensions explicites

app/components/product-card.tsxtsx
1// ❌ MAUVAIS : Provoque un Layout Shift
2<img src="/banner.jpg" alt="Banner" />
3
4// ✅ BON : Dimensions explicites
5<Image
6 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 Loader
14export 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

app/api/data/route.tstsx
1// Edge Runtime : Déployé sur CDN global
2export 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ées
20}
21
22// Streaming avec Suspense : HTML partiel envoyé immédiatement
23export 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é

Moment.js
Librairie de dates populaire mais volumineuse
Avantages
  • API riche
  • Documentation complète
Inconvenients
  • 291 KB minifiée
  • Import tout ou rien
  • Non tree-shakable
Cas d'usage
  • Applications legacy
  • Besoins complets de manipulation de dates
date-fns
Alternative moderne et modulaire
Avantages
  • 12 KB par fonction
  • Tree-shakable
  • Import sélectif
Inconvenients
  • API différente de Moment
  • Migration nécessaire
Cas d'usage
  • Applications modernes
  • Optimisation bundle
  • Next.js recommandé

2. Dynamic Imports : Code Splitting automatique

app/dashboard/page.tsxtsx
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 navigateur
10});
11
12export function Dashboard() {
13 const [showChart, setShowChart] = useState(false);
14
15 return (
16 <div>
17 <button onClick={() => setShowChart(true)}>
18 Afficher le graphique
19 </button>
20
21 {showChart && <HeavyChart />} {/* Chargé uniquement si affiché */}
22 </div>
23 );
24}
25
26// Impact : TTI réduit de 40% sur /dashboard
27// Bundle initial : 450 KB → 180 KB
28// Bundle HeavyChart : 270 KB (chargé à la demande)

3. Server Components : Zéro JavaScript par défaut

app/blog/[slug]/page.tsxtsx
1// ✅ Server Component (par défaut dans App Router)
2// Rendu côté serveur, zéro JS envoyé au client
3export 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.tsx
18'use client'; // ⚠️ Ajoute du JS au bundle client
19
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'aime
28 </button>
29 );
30}
31
32// Impact : Réduction de 70% du JavaScript client
33// Avant (tout en client) : 250 KB de JS
34// 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%)
app/components/product-gallery.tsxtsx
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 <Image
8 key={product.id}
9 src={product.image}
10 alt={product.name}
11 width={400}
12 height={400}
13 quality={85} // 85 = bon compromis qualité/poids
14 loading="lazy" // Par défaut (sauf si priority={true})
15 placeholder="blur" // Placeholder flou pendant chargement
16 blurDataURL={product.blurHash}
17 />
18 ))}
19 </div>
20 );
21}
22
23// Next.js génère automatiquement :
24// - /product.webp?w=640&q=85
25// - /product.webp?w=750&q=85
26// - /product.webp?w=828&q=85
27// - /product.webp?w=1080&q=85
28// Le navigateur choisit la taille adaptée

Polices : next/font élimine le Flash of Unstyled Text

app/layout.tsxtsx
1import { Inter, Roboto_Mono } from 'next/font/google';
2
3// Chargement optimisé avec preload et display:swap
4const inter = Inter({
5 subsets: ['latin'],
6 display: 'swap', // Affiche texte immédiatement, swap dès que police chargée
7 preload: true, // Précharge dans <head>
8 variable: '--font-inter', // Variable CSS
9});
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

LCP4.2s
INP380ms
CLS0.18
Bundle JS450 KB

Après Optimisation

LCP1.8s

-57% grâce à next/image priority

INP120ms

-68% grâce à Server Actions + useOptimistic

CLS0.05

-72% grâce à dimensions explicites + skeletons

Bundle JS135 KB

-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
Testez ces optimisations en temps reelInteractif

Ouvrez le simulateur pour comparer React.memo, useMemo et useCallback avec des mesures reelles de performance.

Optimisations·Section 12/20

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

SELECT * FROM posts LIMIT 100
SELECT * FROM users WHERE id = 1
SELECT * FROM users WHERE id = 2
... x100
SELECT * FROM users WHERE id = 100

101 queries • 1250ms

Query avec Include

SELECT posts.*, users.*
FROM posts
LEFT JOIN users ON posts.user_id = users.id
LIMIT 100

1 query • 45ms

Exemple avec Prisma

app/lib/queries.tstsx
1// ❌ MAUVAIS : N+1 Problem
2export 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, 1250ms
16
17// ✅ BON : Include
18export async function getPosts() {
19 const posts = await db.post.findMany({
20 take: 100,
21 include: {
22 author: true, // JOIN automatique
23 comments: {
24 include: {
25 author: true // Nested include
26 }
27 }
28 }
29 });
30
31 return posts;
32}
33// Résultat : 1 query, 45ms
34
35// Impact : Amélioration de 27x (1250ms → 45ms)
N+1 Queries
Requêtes séquentielles pour chaque relation
Avantages
  • Code simple
  • Facile à comprendre
Inconvenients
  • 101 queries SQL
  • Temps : 1250ms
  • Scalabilité impossible
Cas d'usage
  • Jamais en production
  • Prototype rapide uniquement
Query avec Include
Une seule requête avec JOIN
Avantages
  • 1 query SQL
  • Temps : 45ms
  • Amélioration 20x-100x
Inconvenients
  • Nécessite planification
  • Schéma relationnel requis
Cas d'usage
  • Production recommandée
  • Toute relation parent-enfant

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

B-tree Index
Index standard pour égalité et ranges
Avantages
  • O(log n) vs O(n)
  • Supporte <, >, =, BETWEEN
  • Amélioration 100x-1000x
Inconvenients
  • Espace disque
  • Ralentit INSERT/UPDATE
Cas d'usage
  • Colonnes WHERE fréquentes
  • Foreign keys
  • ORDER BY
Partial Index
Index conditionnel sur sous-ensemble de données
Avantages
  • Moins d'espace disque
  • Plus rapide
  • Ciblé sur cas fréquents
Inconvenients
  • Requiert prédiction du pattern
  • Pas utilisé si WHERE différent
Cas d'usage
  • status = "active"
  • deleted_at IS NULL
  • Filtres récurrents
Composite Index
Index sur plusieurs colonnes (ordre important)
Avantages
  • Optimise queries multi-colonnes
  • Couvre plusieurs cas
Inconvenients
  • Ordre critique
  • Taille importante
  • Maintenance complexe
Cas d'usage
  • WHERE a = ? AND b = ?
  • ORDER BY a, b
  • Groupes fréquents

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%
migrations/add-index.sqlsql
1-- Exemple : Recherche d'inspections par compte
2-- Sans index :
3SELECT * FROM property_inspections
4WHERE account_id = '123';
5-- Seq Scan sur 100,000 rows : 450ms
6
7-- Créer un index B-tree
8CREATE INDEX idx_inspections_account_id
9ON property_inspections(account_id);
10
11-- Avec index :
12SELECT * FROM property_inspections
13WHERE account_id = '123';
14-- Index Scan sur 45 rows : 3ms
15
16-- Amélioration : 150x plus rapide

2. Partial Index : Index Conditionnel

migrations/add-partial-index.sqlsql
1-- Cas d'usage : Filtrer uniquement les inspections actives
2-- 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_status
7ON property_inspections(account_id, status);
8-- Taille : 12 MB
9
10-- ✅ BON : Partial Index (indexe uniquement les actives)
11CREATE INDEX idx_inspections_active
12ON 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 automatiquement
18SELECT * FROM property_inspections
19WHERE account_id = '123' AND status = 'draft';
20-- Index Scan sur idx_inspections_active : 1ms
21
22-- ⚠️ Requête NON optimisée par cet index
23SELECT * FROM property_inspections
24WHERE account_id = '123' AND status = 'completed';
25-- Seq Scan (l'index ne couvre pas 'completed')

3. Composite Index : L'Ordre est CRITIQUE

migrations/composite-index.sqlsql
1-- ⚠️ L'ORDRE DES COLONNES EST CRUCIAL
2
3-- Cas 1 : Index (account_id, created_at)
4CREATE INDEX idx_inspections_account_date
5ON property_inspections(account_id, created_at);
6
7-- ✅ Optimise ces requêtes :
8SELECT * FROM property_inspections
9WHERE account_id = '123'; -- Utilise l'index
10
11SELECT * FROM property_inspections
12WHERE account_id = '123' AND created_at > '2024-01-01'; -- Utilise l'index
13
14SELECT * FROM property_inspections
15WHERE account_id = '123' ORDER BY created_at DESC; -- Utilise l'index
16
17-- ❌ N'optimise PAS ces requêtes :
18SELECT * FROM property_inspections
19WHERE 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écessaire
31CREATE INDEX idx_inspections_date
32ON property_inspections(created_at); -- Pour requêtes sans account_id

Hié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()
~ms

Cache mémoire pendant le rendu. Déduplication automatique des requêtes identiques.

2. Next.js Data Cache
secondes/minutes

Cache serveur persistant. Revalidation par temps ou tag.

3. Redis / KV
minutes/heures

Cache distribué. Partagé entre instances. Idéal pour sessions, rate limiting.

4. CDN Cache
heures/jours

Cache global au plus près de l'utilisateur. Pour assets statiques et pages SSG.

4

niveaux de cache

Du plus rapide (ms)

au plus lent (jours)

1. React cache() : Déduplication de Requêtes

app/lib/queries.tstsx
1import { cache } from 'react';
2
3// Sans cache : 3 requêtes identiques
4export 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 fois
11 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 DB
17
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 4

2. Next.js Data Cache : Revalidation Granulaire

app/lib/api.tstsx
1// Revalidation par temps
2export async function getPosts() {
3 const res = await fetch('https://api.example.com/posts', {
4 next: { revalidate: 60 } // Cache 60 secondes
5 });
6 return res.json();
7}
8
9// Revalidation par tag
10export 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 manuellement
18import { 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 posts
24 revalidateTag('posts');
25
26 // Invalide un post spécifique
27 revalidateTag(`post-${id}`);
28
29 // Invalide une route complète
30 revalidatePath('/blog');
31}
32
33// Impact : TTFB réduit de 800ms à 50ms sur hits de cache

3. Redis / KV : Cache Distribué

app/lib/cache.tstsx
1import { kv } from '@vercel/kv';
2
3// Cache distribué pour sessions utilisateur
4export async function getUserSession(userId: string) {
5 // Vérifier le cache d'abord
6 const cached = await kv.get(`session:${userId}`);
7 if (cached) return cached;
8
9 // Si pas en cache, query DB
10 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 Redis
19export async function checkRateLimit(ip: string) {
20 const key = `rate-limit:${ip}`;
21 const count = await kv.incr(key);
22
23 // Expire après 1 minute
24 if (count === 1) {
25 await kv.expire(key, 60);
26 }
27
28 // Max 100 requêtes par minute
29 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

optimized-queries.sqlsql
1-- ❌ MAUVAIS : SELECT * (transfert inutile de données)
2SELECT * FROM property_inspections
3WHERE account_id = '123';
4-- Transfert : 5.2 MB pour 1000 rows
5
6-- ✅ BON : SELECT colonnes nécessaires
7SELECT id, address, status, created_at
8FROM property_inspections
9WHERE account_id = '123';
10-- Transfert : 180 KB pour 1000 rows (-97%)
11
12-- Avec Prisma
13// ❌ MAUVAIS
14const inspections = await db.propertyInspection.findMany({
15 where: { accountId: '123' }
16});
17
18// ✅ BON
19const inspections = await db.propertyInspection.findMany({
20 where: { accountId: '123' },
21 select: {
22 id: true,
23 address: true,
24 status: true,
25 createdAt: true
26 }
27});

2. Pagination avec Cursor (pas OFFSET)

pagination.sqlsql
1-- ❌ MAUVAIS : OFFSET (scanne toutes les rows précédentes)
2SELECT * FROM posts
3ORDER BY created_at DESC
4LIMIT 20 OFFSET 10000;
5-- Scan : 10,020 rows pour retourner 20 rows
6-- Temps : 850ms sur page 500
7
8-- ✅ BON : Cursor-based pagination
9SELECT * FROM posts
10WHERE created_at < '2024-01-15T10:30:00Z'
11ORDER BY created_at DESC
12LIMIT 20;
13-- Scan : 20 rows
14-- Temps : 5ms sur page 500 (170x plus rapide)
15
16-- Avec Prisma
17// ✅ Cursor pagination
18const 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 suivante
25return {
26 posts,
27 nextCursor: posts[posts.length - 1]?.id
28};

3. Aggregations en SQL (pas en application)

aggregations.tstsx
1// ❌ MAUVAIS : Aggregation en JavaScript
2const inspections = await db.propertyInspection.findMany({
3 where: { accountId: '123' }
4}); // Transfert de 10,000 rows
5
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.length
10};
11// Temps : 450ms, Transfert : 8 MB
12
13// ✅ BON : Aggregation en SQL
14const 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 bytes
24
25// SQL généré :
26-- SELECT COUNT(id), AVG(score)
27-- FROM property_inspections
28-- WHERE account_id = '123';
29--
30-- SELECT COUNT(*)
31-- FROM property_inspections
32-- WHERE account_id = '123' AND status = 'completed';
33
34// Impact : 37x plus rapide, 40,000x moins de données transférées

EXPLAIN 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

explain-analyze.sqlsql
1-- Analyser une requête
2EXPLAIN ANALYZE
3SELECT * FROM property_inspections
4WHERE 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: 99955
11Planning Time: 0.345 ms
12Execution Time: 450.567 ms
13
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 rien
17-- 3. "Execution Time: 450.567 ms" : Très lent
18
19-- Solution : Créer un index
20CREATE INDEX idx_inspections_account_status
21ON property_inspections(account_id, status);
22
23-- Résultat AVEC index :
24Index Scan using idx_inspections_account_status on property_inspections
25 (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 ms
29Execution Time: 2.567 ms
30
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 rapide

Signes 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

1

Identifier la requête lente

Logs, monitoring, profiler Next.js

2

Lancer EXPLAIN ANALYZE

Analyser le plan d'exécution

3

Créer les index nécessaires

B-tree, partial, ou composite selon le cas

4

Re-lancer EXPLAIN ANALYZE

Vérifier l'amélioration

5

Mesurer en production

Confirmer les gains réels

Optimisations·Section 13/20

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 :

profiler-usage.tsxtypescript
1import { Profiler, ProfilerOnRenderCallback } from 'react';
2
3function MyComponent() {
4 const onRender: ProfilerOnRenderCallback = (
5 id, // "id" du Profiler qui vient de commit
6 phase, // "mount" ou "update"
7 actualDuration, // Temps passe a render cette mise a jour
8 baseDuration, // Temps estime sans memoization
9 startTime, // Quand React a commence a render
10 commitTime // Quand React a commite cette mise a jour
11 ) => {
12 console.log(`${id} (${phase}): ${actualDuration.toFixed(2)}ms`);
13
14 // Envoyez ces metriques a votre service d'analytics
15 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.

Ouvrir le simulateur de performance

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 :

dev-vs-prod.shbash
1// Mode Developpement
2- React inclut des warnings et checks supplementaires
3- Source maps et stack traces detaillees
4- Hot Module Replacement actif
5- Double render en Strict Mode (React 18+)
6→ 2-3x plus lent que production
7
8// Mode Production
9- Code minifie et optimise
10- Tree-shaking applique
11- Pas de dev warnings
12- Un seul render par update
13→ Performance reelle pour les utilisateurs
14
15// Toujours tester en production !
16npm run build
17npm run start

Best 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
Bonnes Pratiques·Section 14/20

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)

app/actions/inspection.tstsx
1// ❌ VULNÉRABLE : Vérification client uniquement
2'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 Supabase
16'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'ownership
28 const { error } = await supabase
29 .from('property_inspections')
30 .delete()
31 .eq('id', inspectionId);
32 // .eq('user_id', user.id); // ✅ RLS vérifie automatiquement
33
34 if (error) throw error;
35}

2. Cryptographic Failures

supabase/migrations/encryption.sqlsql
1// ✅ Supabase Auth : Hashing sécurisé avec bcrypt
2// Jamais stocker de mots de passe en clair !
3
4// Inscription utilisateur
5const { data, error } = await supabase.auth.signUp({
6 email: 'user@example.com',
7 password: 'SuperSecretPassword123!', // ✅ Hashé automatiquement avec bcrypt
8});
9
10// Supabase utilise :
11// - bcrypt pour hasher les mots de passe
12// - 10 rounds de salting (coût optimal)
13// - Stockage dans auth.users (table protégée)
14
15// ✅ Chiffrement des données sensibles avec pgcrypto
16// Exemple : Chiffrer les numéros de carte bancaire
17CREATE 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_number
26FROM payments WHERE user_id = '123';

3. Injection (SQL, XSS, Command)

app/components/user-comment.tsxtsx
1// ❌ SQL INJECTION VULNERABLE
2const { data } = await supabase
3 .from('users')
4 .select('*')
5 .eq('email', userInput); // ⚠️ Si userInput = "' OR '1'='1", retourne TOUS les users
6
7// ✅ SÉCURISÉ : Parameterized Queries (Supabase utilise automatiquement)
8const { data } = await supabase
9 .from('users')
10 .select('*')
11 .eq('email', userInput); // ✅ userInput est automatiquement échappé
12
13// ❌ XSS VULNERABLE : Injection JavaScript
14export 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 automatiquement
20export function UserComment({ comment }: { comment: string }) {
21 return <div>{comment}</div>; // ✅ <script> est affiché comme texte
22}
23
24// ✅ Si HTML nécessaire : Sanitize avec DOMPurify
25import 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)

app/api/inspections/[id]/route.tstsx
1// ❌ IDOR VULNERABLE : IDs prévisibles séquentiels
2// 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évisible
5 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 ownership
10// URL : /api/inspections/550e8400-e29b-41d4-a716-446655440000
11import { 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 ownership
20 const { data: inspection, error } = await supabase
21 .from('property_inspections')
22 .select('*')
23 .eq('id', params.id) // ✅ UUID : 550e8400-e29b-41d4-a716-446655440000
24 .eq('user_id', user.id) // ✅ Vérification ownership
25 .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

app/actions/admin.tstsx
1// ❌ VULNÉRABLE : Exposer des secrets côté client
2// .env
3NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
4NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ✅ OK (publique)
5SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ⚠️ NE JAMAIS exposer !
6
7// ❌ Code vulnérable
8'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 JS
11
12// ✅ SÉCURISÉ : Service key uniquement serveur
13'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 serveur
20 );
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.

supabase/migrations/rls_policies.sqlsql
1-- Activer RLS sur la table
2ALTER TABLE property_inspections ENABLE ROW LEVEL SECURITY;
3
4-- Politique : Users accèdent uniquement à leurs propres inspections
5CREATE POLICY "Users access own inspections"
6ON property_inspections
7FOR ALL -- SELECT, INSERT, UPDATE, DELETE
8USING (auth.uid() = user_id) -- Vérifie que l'user authentifié = user_id de la row
9WITH CHECK (auth.uid() = user_id); -- Vérifie lors d'INSERT/UPDATE
10
11-- Test de sécurité : Un user ne peut PAS accéder aux données d'un autre
12-- 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 admins
17CREATE POLICY "Admins access all inspections"
18ON property_inspections
19FOR ALL
20USING (
21 EXISTS (
22 SELECT 1 FROM user_roles
23 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.

Validation Client Uniquement
Validation côté navigateur avec JavaScript
Avantages
  • UX réactive
  • Feedback immédiat
Inconvenients
  • Contournable (DevTools)
  • Pas de protection réelle
  • Faille de sécurité critique
Cas d'usage
  • Jamais en production
  • Amélioration UX seulement
Validation Client + Serveur
Double validation (UX + sécurité)
Avantages
  • UX réactive
  • Sécurité garantie
  • Protection OWASP
Inconvenients
  • Duplication du code de validation
Cas d'usage
  • Production (obligatoire)
  • Applications sécurisées
  • APIs publiques
app/actions/inspection.tstsx
1import { z } from 'zod';
2
3// Schéma de validation réutilisable
4const 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() // Normalisation
13 .trim(), // Supprime espaces
14
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é automatiquement
28type Inspection = z.infer<typeof InspectionSchema>;
29
30// Utilisation côté serveur
31'use server';
32
33export async function createInspection(formData: FormData) {
34 // Validation avec Zod
35 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 échoue
47 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ées
55 const validatedData = result.data; // ✅ Type-safe
56
57 // Insertion en DB
58 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.

next.config.jsjavascript
1// next.config.js
2module.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 HTTPS
19 },
20 {
21 key: 'Content-Security-Policy',
22 value: [
23 "default-src 'self'", // ✅ Ressources uniquement du même domaine
24 "script-src 'self' 'unsafe-inline' https://vercel.live", // Scripts autorisés
25 "style-src 'self' 'unsafe-inline'", // Styles autorisés
26 "img-src 'self' data: https:", // Images autorisées
27 "font-src 'self' data:", // Polices autorisées
28 "connect-src 'self' https://*.supabase.co", // APIs autorisées
29 "frame-ancestors 'none'", // ✅ Équivalent X-Frame-Options
30 ].join('; '),
31 },
32 {
33 key: 'Referrer-Policy',
34 value: 'strict-origin-when-cross-origin', // ✅ Limite info envoyée aux sites externes
35 },
36 {
37 key: 'Permissions-Policy',
38 value: 'camera=(), microphone=(), geolocation=()', // ✅ Désactive APIs sensibles
39 },
40 ],
41 },
42 ];
43 },
44};
45
46// Vérifier les headers : https://securityheaders.com
47// 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.

RLS activé sur toutes les tables Supabase
Validation Zod sur toutes les Server Actions
Secrets sans NEXT_PUBLIC_ (sauf ANON_KEY)
Security Headers configurés (CSP, HSTS, X-Frame-Options)
DOMPurify pour tout HTML utilisateur
UUIDs au lieu d'IDs séquentiels
Rate limiting sur APIs publiques
HTTPS forcé (Strict-Transport-Security)
Tests de sécurité (OWASP ZAP ou Burp Suite)
Audit npm (npm audit fix)
Bonnes Pratiques·Section 15/20

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

components/product-list.tsxtsx
1// ❌ ANTI-PATTERN : useEffect inutile
2function 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 directe
14function 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 : useMemo
22function ProductList({ products }: { products: Product[] }) {
23 const filteredProducts = useMemo(
24 () => products.filter(p => p.inStock && expensiveCalculation(p)),
25 [products] // Re-calcule uniquement si products change
26 );
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

components/checkout-form.tsxtsx
1// ❌ ANTI-PATTERN : Multiples useState liés
2function 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 setters
14 };
15}
16
17// ✅ SOLUTION : useReducer pour état complexe
18type 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 <input
65 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

components/user-context.tsxtsx
1// ❌ ANTI-PATTERN : Prop drilling sur 5 niveaux
2<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// Utilisation
28function 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 direct
44 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 change
59 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 change
64 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

components/card.tsxtsx
1// ✅ Pattern : Children pour composition flexible
2function 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 libre
11<Card>
12 <h2>Titre</h2>
13 <p>Description</p>
14 <button>Action</button>
15</Card>
16
17// Alternative : Slots nommés
18function Card({
19 header,
20 content,
21 footer
22}: {
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<Card
37 header={<h2>Titre</h2>}
38 content={<p>Description</p>}
39 footer={<button>Action</button>}
40/>

2. Render Props : Logique Réutilisable

components/mouse-tracker.tsxtsx
1// ✅ Pattern : Render Props pour partager logique
2interface 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 props
23<MouseTracker
24 render={({ x, y }) => (
25 <div>
26 Position : {x}, {y}
27 </div>
28 )}
29/>
30
31// Alternative moderne : Custom Hook
32function 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

components/tabs.tsxtsx
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 <button
31 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 namespace
49export const TabsComponent = {
50 Root: Tabs,
51 List: TabsList,
52 Trigger: TabsTrigger,
53 Content: TabsContent,
54};
55
56// Utilisation : API déclarative et flexible
57import { 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

components/with-auth.tsxtsx
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// Utilisation
14function 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 Props
23// - Compound Components
24
25// Alternative moderne : Middleware Route
26// app/dashboard/layout.tsx
27import { 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é serveur
35
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

components/product-card.tsxtsx
1// ❌ PROBLÈME : Re-render à chaque fois que parent re-render
2function 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 identiques
20const ProductCard = React.memo(function ProductCard({ product }: { product: Product }) {
21 console.log('ProductCard render'); // ✅ Ne log que si product change
22 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 equality
29const ProductCard = React.memo(
30 function ProductCard({ product }: { product: Product }) {
31 return <div>{product.name}</div>;
32 },
33 (prevProps, nextProps) => prevProps.product.id === nextProps.product.id
34);
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

components/product-list.tsxtsx
1// ❌ PROBLÈME : Calcul coûteux à chaque render
2function ProductList({ products }: { products: Product[] }) {
3 const [filter, setFilter] = useState('');
4
5 // ⚠️ Re-calcule à chaque render (même si products/filter identiques)
6 const filteredProducts = products
7 .filter(p => p.name.includes(filter))
8 .sort((a, b) => expensiveSort(a, b)); // Calcul coûteux
9
10 return <div>{filteredProducts.map(...)}</div>;
11}
12
13// ✅ SOLUTION : useMemo pour memoizer le résultat
14function ProductList({ products }: { products: Product[] }) {
15 const [filter, setFilter] = useState('');
16
17 const filteredProducts = useMemo(
18 () => products
19 .filter(p => p.name.includes(filter))
20 .sort((a, b) => expensiveSort(a, b)),
21 [products, filter] // ✅ Re-calcule uniquement si products ou filter change
22 );
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 deps
30// 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
Règle pratique : Si le calcul prend >10ms (vérifiable avec console.time), utilisez useMemo

3. useCallback : Memoization de Fonction

components/product-list.tsxtsx
1// ❌ PROBLÈME : Nouvelle fonction à chaque render
2function ProductList({ products }: { products: Product[] }) {
3 const [count, setCount] = useState(0);
4
5 // ⚠️ handleClick est une NOUVELLE fonction à chaque render
6 // 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 stable
20function ProductList({ products }: { products: Product[] }) {
21 const [count, setCount] = useState(0);
22
23 // ✅ handleClick garde la même référence entre renders
24 const handleClick = useCallback((id: string) => {
25 console.log('Clicked', id);
26 }, []); // Deps vide = fonction jamais re-créée
27
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 closure
39}, [count]); // ✅ Re-créée uniquement si count change
Cas 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
Attention : useCallback sans React.memo n'a pas d'impact sur la performance !

4. Virtualization : Listes Longues

components/product-list.tsxtsx
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 <FixedSizeList
16 height={600} // Hauteur container
17 itemCount={products.length} // Nombre total items
18 itemSize={100} // Hauteur d'un item
19 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 nodes
32// Après : ~10 DOM nodes (seulement les visibles)
33// Performance : 60 FPS vs 5 FPS

State Management : Choisir le Bon Outil

Guide de décision pour choisir entre useState, Context, Zustand et React Query selon le cas d'usage.

useState/useReducer
État local au composant React
Avantages
  • Simple
  • Pas de dépendance externe
  • Performant
Inconvenients
  • Prop drilling
  • Difficile à partager
Cas d'usage
  • État formulaire
  • Toggle UI
  • État composant isolé
Context API
Partage d'état global sans prop drilling
Avantages
  • Natif React
  • Évite prop drilling
  • Simple pour état global
Inconvenients
  • Re-render de tous consumers
  • Pas de devtools
Cas d'usage
  • Theme
  • Langue
  • Auth user
  • Données rarement modifiées
Zustand
State management client minimaliste
Avantages
  • 2 KB
  • API simple
  • Performant (re-render sélectif)
  • Devtools
Inconvenients
  • Client-only
  • Une librairie de plus
Cas d'usage
  • UI state complexe
  • Panier
  • Filtres
  • Modals
React Query / TanStack Query
Server state management (cache API)
Avantages
  • Cache intelligent
  • Refetch auto
  • Optimistic updates
  • Devtools
Inconvenients
  • Courbe d'apprentissage
  • Inutile sans API
Cas d'usage
  • Données API
  • Server state
  • Synchronisation serveur
Règle de Décision
Client state (UI) : useState, Zustand
Server state (API) : React Query, SWR
Global rarement modifié : Context API
Global fréquemment modifié : Zustand

Error Boundaries : Gérer les Erreurs React

Capturer les erreurs React et afficher un fallback UI sans crasher toute l'application.

app/layout.tsxtsx
1// ✅ Error Boundary avec react-error-boundary
2import { 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 <ErrorBoundary
17 FallbackComponent={ErrorFallback}
18 onReset={() => {
19 // Reset app state (ex: clear cache)
20 }}
21 onError={(error, errorInfo) => {
22 // Log error to Sentry/Bugsnag
23 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 rendering
35// - Errors dans Error Boundary lui-même
36
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.

Bonnes Pratiques·Section 16/20

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

showHeader: boolean
showFooter: boolean
showSidebar: boolean
showCloseButton: boolean
= 16 combinaisons possibles
Solution

Composition avec children et slots

Modal.Header
Modal.Body
Modal.Footer
Modal.CloseButton
= Flexibilité infinie
components/modal.tsxtsx
1// ❌ ANTI-PATTERN : Boolean props proliferation
2interface 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 children
26}: ModalProps) {
27 // Logique complexe avec 10+ props booléens
28 // Difficile à tester, maintenir et étendre
29
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 complexes
48<Modal
49 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 Content
59</Modal>
components/modal.tsxtsx
1// ✅ SOLUTION : Composition avec Compound Components
2import { 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 namespace
51Modal.Header = ModalHeader;
52Modal.Body = ModalBody;
53Modal.Footer = ModalFooter;
54Modal.CloseButton = ModalCloseButton;
55
56export { Modal };
57
58// Utilisation : Composition déclarative et flexible
59<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éens
93// ✅ API déclarative et lisible
94// ✅ Facile à tester et maintenir

Compound 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.

components/tabs.tsxtsx
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 <button
37 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 namespace
64Tabs.List = TabsList;
65Tabs.Trigger = TabsTrigger;
66Tabs.Content = TabsContent;
67
68export { Tabs };
69
70// Utilisation : API déclarative
71<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-documenting
97// ✅ Type-safe avec TypeScript
98// ✅ 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.

components/card.tsxtsx
1// ❌ ÉVITER : Render Props proliferation
2interface 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 lisible
21<Card
22 renderHeader={() => <h2>Titre</h2>}
23 renderBody={() => <p>Contenu</p>}
24 renderFooter={() => <button>Action</button>}
25/>
components/card.tsxtsx
1// ✅ PRÉFÉRER : Children pour composition
2function 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 namespace
19Card.Header = CardHeader;
20Card.Body = CardBody;
21Card.Footer = CardFooter;
22
23export { Card };
24
25// Utilisation : Simple et déclaratif
26<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 flexible
41<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 automatiquement
50// ✅ Permet composition libre
51// ✅ Idiomatique React

State-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.

components/accordion.tsxtsx
1// ✅ Pattern : State-Context-Interface
2// 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 <button
61 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// Export
81Accordion.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 1
92 </Accordion.Trigger>
93 <Accordion.Content value="item-1">
94 Réponse 1
95 </Accordion.Content>
96 </Accordion.Item>
97
98 <Accordion.Item value="item-2">
99 <Accordion.Trigger value="item-2">
100 Question 2
101 </Accordion.Trigger>
102 <Accordion.Content value="item-2">
103 Réponse 2
104 </Accordion.Content>
105 </Accordion.Item>
106</Accordion>
107
108// Avantages :
109// ✅ État testable indépendamment (useAccordionState)
110// ✅ Context pour dependency injection
111// ✅ Composants UI découplés de la logique
112// ✅ 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.

components/parent.tsxtsx
1// ❌ PROBLÈME : Siblings ne peuvent pas partager l'état
2function 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 local
12function 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 A
23function ComponentB() {
24 // Comment afficher le nombre d'items dans le panier ?
25 return <CartButton itemCount={???} />;
26}
components/cart-provider.tsxtsx
1// ✅ SOLUTION : Lift State dans un Provider
2import { 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 parent
42function ParentComponent() {
43 return (
44 <CartProvider>
45 <ComponentA />
46 <ComponentB />
47 </CartProvider>
48 );
49}
50
51// ComponentA : Ajoute au panier
52function ComponentA() {
53 const { addItem } = useCart(); // ✅ Accès au context
54
55 return <ProductList onAddItem={addItem} />;
56}
57
58// ComponentB : Affiche le nombre d'items
59function ComponentB() {
60 const { itemCount } = useCart(); // ✅ Accès au context
61
62 return <CartButton itemCount={itemCount} />;
63}
64
65// Avantages :
66// ✅ État partagé entre siblings
67// ✅ Pas de prop drilling
68// ✅ 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
Bonnes Pratiques·Section 17/20

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.

S
Single Responsibility
Un composant = une responsabilité unique. Évitez les "God Components" qui font tout.
O
Open/Closed
Ouvert à l'extension, fermé à la modification. Utilisez CVA (Class Variance Authority) et les props pour étendre le comportement.
L
Liskov Substitution
Les composants enfants doivent pouvoir remplacer leurs parents sans casser l'interface. Interfaces cohérentes et prévisibles.
I
Interface Segregation
Créez des interfaces ciblées plutôt qu'une seule interface universelle. Évitez les props inutilisées.
D
Dependency Inversion
Dépendez d'abstractions, pas d'implémentations. Utilisez des interfaces pour découpler le code.
components/user-profile.tsxtsx
1// ❌ Violation de Single Responsibility - God Component
2export 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 choses
10}
11
12// ✅ Respect du Single Responsibility
13export 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 unique

Clean Architecture - 4 Layers

Organisation du code en couches concentriques pour séparer la logique métier de l'infrastructure technique.

1
Domain - Business Logic
Entités et règles métier pures. Indépendant de toute technologie (base de données, UI, framework).
2
Use Cases - Application Logic
Orchestration des règles métier. Coordonne les entités pour accomplir des cas d'usage spécifiques.
3
Adapters - Controllers, Gateways
Convertit les données entre le monde extérieur (DB, API) et les use cases. Server Actions, API Routes.
4
Infrastructure - DB, API, UI
Détails techniques. Prisma, PostgreSQL, React components. Facilement remplaçables sans changer la logique métier.
Structure recommandéeplaintext
1src/
2├── features/ # Organisation par feature (recommandé)
3│ ├── inspections/
4│ │ ├── components/ # Composants UI spécifiques
5│ │ │ ├── inspection-list.tsx
6│ │ │ └── inspection-detail.tsx
7│ │ ├── hooks/ # Hooks métier de la feature
8│ │ │ └── use-inspections.ts
9│ │ ├── server-actions/ # Mutations serveur
10│ │ │ └── actions.ts
11│ │ ├── schemas/ # Validation Zod
12│ │ │ └── inspection.schema.ts
13│ │ └── types/ # Types TypeScript
14│ │ └── inspection.types.ts
15│ │
16│ └── users/
17│ ├── components/
18│ ├── hooks/
19│ └── types/
20
21├── lib/ # Utilitaires partagés
22│ ├── db/ # Client base de données
23│ ├── utils/ # Fonctions utilitaires
24│ └── auth/ # Logique d'authentification
25
26└── app/ # Next.js App Router
27 ├── (dashboard)/
28 └── api/
29
30# Avantage : tout ce qui concerne "inspections" est dans un seul dossier
31# Scalable, facile à maintenir, délimite clairement les responsabilités

Design Patterns Essentiels

Patterns architecturaux adaptés à Next.js pour structurer efficacement votre application.

Repository Pattern
Abstraction de la couche de données. Sépare la logique d'accès aux données de la logique métier.
Exemple : InspectionRepository encapsule toutes les requêtes Prisma liées aux inspections.
Factory Pattern
Crée dynamiquement des composants ou des instances selon un paramètre. Très utile pour les composants polymorphes.
Exemple : FieldFactory retourne différents composants de formulaire selon le type de champ.
Adapter Pattern
Convertit une interface en une autre. Crucial pour faire communiquer la DB avec le frontend.
Exemple : PrismaUserAdapter transforme les modèles Prisma en objets conformes à l'interface frontend.
Observer Pattern
Un sujet notifie ses observateurs des changements d'état. Implémenté nativement par React Query.
Exemple : Tous les composants utilisant 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.

70% Unit Tests (Vitest)
Testent des fonctions, hooks, utilitaires isolés. Rapides, faciles à maintenir.
20% Integration Tests (RTL)
Testent l'interaction entre composants. React Testing Library pour simuler les actions utilisateur.
10% E2E Tests (Playwright)
Testent les parcours critiques de bout en bout dans un vrai navigateur. Plus lents, mais indispensables.
lib/repositories/inspection.repository.tstypescript
1// Repository Pattern - Abstraction de la couche de données
2
3// lib/repositories/inspection.repository.ts
4import { 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 Action
34import { 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'application

Pièges à éviter

1. Over-engineering
N'ajoutez pas d'abstraction avant d'en avoir vraiment besoin. Commencez simple, refactorez quand la complexité augmente.
2. God Components
Si un composant dépasse 200 lignes, c'est probablement qu'il fait trop de choses. Découpez en sous-composants.
3. Coupling fort
Évitez les dépendances directes entre features. Utilisez des événements, des hooks partagés ou un state manager global si nécessaire.
4. Pas de tests
Écrire des tests APRÈS le code est 10x plus difficile. Testez au fur et à mesure, ou pratiquez le TDD (Test-Driven Development).
5. Tech-based folders
Organiser par technologie (components/, hooks/, utils/) devient vite ingérable. Privilégiez l'organisation par feature.
Bonnes Pratiques·Section 18/20

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.

P
Perceptible
• Alt text sur toutes les images
• Contraste minimum 4.5:1 (texte standard) ou 3:1 (texte large/UI)
• Design responsive adapté à tous les écrans
• Ne pas utiliser la couleur seule pour transmettre l'information (ajouter des icônes, du texte)
O
Opérable
• Navigation au clavier (Tab, Enter, Escape)
• Focus visible sur tous les éléments interactifs
• Éviter les timeouts trop courts (laisser le temps de lire/agir)
• Pas de contenu clignotant plus de 3 fois par seconde (risque d'épilepsie)
U
Understandable (Compréhensible)
• Attribut lang="fr" pour spécifier la langue
• Texte clair, sans jargon inutile
• Labels explicites sur les formulaires
• Messages d'erreur descriptifs
R
Robust (Robuste)
• HTML valide et sémantique
• ARIA utilisé uniquement quand nécessaire (ne pas abuser)
• Compatible avec les technologies d'assistance (screen readers, etc.)
• Code qui fonctionne sur tous les navigateurs
Comparaison HTML sémantiquetsx
1// ❌ MAUVAIS - Div soup sans sémantique
2<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émantique
16<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 structure
33// - 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.

Utilisez un <button>
  • Action sur la page actuelle (submit form, ouvrir modal, toggle menu)
  • Modification d'état (ajouter au panier, liker)
  • Déclencher du JavaScript
Utilisez un <a>
  • 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.

Roles
Définissent le type d'élément pour les screen readers.
role="dialog" - Modal ou dialogue
aria-modal="true" - Indique que c'est une vraie modal (bloque le reste de la page)
role="alert" - Message important (erreur, succès)
Labels
Donnent un nom accessible à un élément.
aria-label="Fermer" - Label invisible mais lu par les screen readers
aria-labelledby="title-id" - Référence un autre élément comme label
aria-describedby="desc-id" - Ajoute une description supplémentaire
States
Communiquent l'état dynamique d'un composant.
aria-expanded="true" - Menu déroulant ouvert/fermé
aria-selected="true" - Item sélectionné dans une liste
aria-disabled="true" - Élément désactivé
aria-live="polite" - Zone qui se met à jour dynamiquement (notifications)
components/accessible-modal.tsxtsx
1// Exemple complet : Modal accessible avec ARIA
2
3export function AccessibleModal({
4 isOpen,
5 onClose,
6 title,
7 children
8}: ModalProps) {
9 useEffect(() => {
10 // Focus trap - empêcher la navigation au clavier en dehors de la modal
11 if (isOpen) {
12 document.body.style.overflow = 'hidden';
13 // Focus sur le premier élément interactif
14 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 <div
25 className="fixed inset-0 bg-black/50 flex items-center justify-center"
26 onClick={onClose}
27 aria-label="Overlay de la modal"
28 >
29 <div
30 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 contenu
37 >
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 <button
45 onClick={onClose}
46 aria-label="Fermer la modal"
47 className="mt-4 px-4 py-2 bg-primary text-white rounded"
48 >
49 Fermer
50 </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.

Focus Management
Tous les éléments interactifs doivent avoir un style de focus visible.button:focus-visible { outline: 2px solid hsl(var(--primary)) }
Skip Link
Lien invisible qui apparaît au focus pour sauter directement au contenu principal.<a href="#main-content" className="skip-link">Skip to main content</a>
Focus Trap (Modal)
Quand une modal est ouverte, le focus doit rester piégé dans la modal jusqu'à sa fermeture. Utilisez une librairie comme 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é.

AA (Standard)
  • 4.5:1 minimum pour le texte standard
  • 3:1 minimum pour le texte large (18px+ ou 14px+ gras) et les éléments UI
AAA (Enhanced)
  • 7:1 minimum pour le texte standard
  • 4.5:1 minimum pour le texte large
Outils de vérification : Chrome DevTools Lighthouse, WebAIM Contrast Checker, axe DevTools

Screen Readers - Lecteurs d'Écran

Les screen readers vocalisent le contenu web pour les utilisateurs aveugles ou malvoyants.

VoiceOver (macOS/iOS) - Natif sur Mac et iPhone. Testez avec Cmd+F5
NVDA (Windows) - Gratuit et open source. Le plus utilisé sur Windows
JAWS (Windows) - Payant, très complet. Standard en entreprise

Pièges à éviter en Accessibilité

1. outline: none sur :focus
Ne jamais supprimer le outline sans le remplacer par un style de focus visible. Les utilisateurs au clavier doivent savoir où ils sont.
2. Div/span cliquables
Utiliser <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.
3. Images sans alt
Toujours ajouter un attribut alt aux images. Si l'image est purement décorative, utilisez alt="" (vide).
4. Formulaires sans labels
Chaque input doit avoir un <label> associé via l'attribut htmlFor. Les placeholders ne suffisent pas.
5. Contraste insuffisant
Texte gris clair sur fond blanc (ou vice versa). Utilisez toujours un outil de vérification du contraste.
6. Pas de focus trap dans les modals
Si l'utilisateur peut Tab en dehors de la modal, il perd le contexte. Implémentez un focus trap avec une librairie dédiée.
7. ARIA en excès
Trop d'ARIA peut nuire à l'accessibilité. Utilisez ARIA uniquement quand le HTML sémantique ne suffit pas. Règle d'or : "No ARIA is better than bad ARIA".
Avance·Section 19/20

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

app/dashboard/page.tsxtsx
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 asynchrone
28async 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

app/dashboard/loading.tsxtsx
1// app/dashboard/loading.tsx
2// Next.js génère automatiquement un Suspense boundary
3export 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
t=0msHTML initial + Skeleton affiché
t=500msExpensiveStats complètes, skeleton remplacé
t=800msAnalyticsChart affiché
t=1200msRecentActivity affiché, page complète

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

app/ structurebash
1app/
2├── @modal/ # Slot "modal"
3│ ├── default.tsx # Page par défaut (modal cachée)
4│ ├── login/
5│ │ └── page.tsx # Modal de login
6│ └── signup/
7│ └── page.tsx # Modal de signup
8├── @sidebar/ # Slot "sidebar"
9│ ├── default.tsx
10│ └── page.tsx
11├── layout.tsx # Layout utilisant les slots
12└── page.tsx # Page principale

Layout Utilisant les Slots

app/layout.tsxtsx
1// app/layout.tsx
2export default function Layout({
3 children, // Page principale
4 modal, // Slot @modal
5 sidebar // Slot @sidebar
6}: {
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.tsx
26export default function ModalDefault() {
27 return null; // Pas de modal par défaut
28}
29
30// app/@modal/login/page.tsx
31export 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

middleware.tstsx
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 cookie
14 const variant = request.cookies.get('ab-variant') || 'A';
15 const response = NextResponse.next();
16 if (!request.cookies.has('ab-variant')) {
17 // Assigner variant aléatoirement
18 const randomVariant = Math.random() > 0.5 ? 'A' : 'B';
19 response.cookies.set('ab-variant', randomVariant);
20 }
21
22 // 3. i18n : Rediriger selon langue navigateur
23 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és
29 response.headers.set('x-middleware-version', '1.0');
30
31 return response;
32}
33
34// Configurer les paths où le middleware s'applique
35export 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

app/todos/todo-list.tsxtsx
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ément
10 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 temporaire
19 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-plan
32 await addTodo(text);
33 // 3. Revalidation automatique, vrai todo affiché
34 } catch (error) {
35 // 4. Rollback automatique si erreur
36 console.error('Failed to add todo:', error);
37 // useOptimistic gère le rollback automatiquement
38 }
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 Ajouter
48 </button>
49 </form>
50
51 <ul className="space-y-2 mt-4">
52 {optimisticTodos.map((todo) => (
53 <li
54 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.ts
66'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 ?

Optimistic Updates
UI mise à jour instantanément, rollback si erreur
Avantages
  • Feedback immédiat
  • Meilleure UX
  • Pas de spinner
Inconvenients
  • Complexité accrue
  • Gestion rollback
  • Pas pour tout
Cas d'usage
  • Likes/votes
  • Todos
  • Comments
  • Favoris
Server Mutations Classiques
Attendre la réponse serveur avant mise à jour UI
Avantages
  • Simple
  • Fiable
  • Pas de rollback
Inconvenients
  • Spinner visible
  • Latence perceptible
  • UX dégradée
Cas d'usage
  • Paiements
  • Mutations critiques
  • Opérations irréversibles

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

app/inspections/live-list.tsxtsx
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 initiales
12 async function loadInspections() {
13 const { data } = await supabase
14 .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éel
24 const channel = supabase
25 .channel('inspections-changes')
26 .on(
27 'postgres_changes',
28 {
29 event: '*', // INSERT, UPDATE, DELETE
30 schema: 'public',
31 table: 'inspections'
32 },
33 (payload) => {
34 if (payload.eventType === 'INSERT') {
35 // Nouvelle inspection ajoutée
36 setInspections((prev) => [payload.new as Inspection, ...prev]);
37 } else if (payload.eventType === 'UPDATE') {
38 // Inspection modifiée
39 setInspections((prev) =>
40 prev.map((i) =>
41 i.id === payload.new.id ? (payload.new as Inspection) : i
42 )
43 );
44 } else if (payload.eventType === 'DELETE') {
45 // Inspection supprimée
46 setInspections((prev) =>
47 prev.filter((i) => i.id !== payload.old.id)
48 );
49 }
50 }
51 )
52 .subscribe();
53
54 // 3. Cleanup : Unsubscribe au démontage
55 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

Structure i18nbash
1messages/
2├── en.json # Anglais
3├── fr.json # Français
4└── es.json # Espagnol
5
6app/
7├── [locale]/ # Dynamic segment pour langue
8│ ├── layout.tsx # Layout par langue
9│ ├── page.tsx # Page d'accueil traduite
10│ └── about/
11│ └── page.tsx # Page About traduite
12└── i18n.ts # Configuration i18n

Fichiers de Traduction

messages/en.json + fr.jsonjson
1// messages/en.json
2{
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.json
14{
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

app/[locale]/page.tsxtsx
1// app/[locale]/page.tsx
2import { 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 locale
18export function generateStaticParams() {
19 return [{ locale: 'en' }, { locale: 'fr' }, { locale: 'es' }];
20}

SEO : hreflang + sitemap

app/[locale]/layout.tsxtsx
1// app/[locale]/layout.tsx
2export 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.ts
20export 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: 1
29 },
30 {
31 url: `https://maxpaths.com/${locale}/courses`,
32 lastModified: new Date(),
33 changeFrequency: 'daily',
34 priority: 0.8
35 }
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.

Avance·Section 20/20

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.

SSR
Rendu serveur à chaque requête
Avantages
  • Données toujours à jour
  • Personnalisation par utilisateur
  • SEO optimal
Inconvenients
  • Temps de réponse variable
  • Charge serveur élevée
  • Coûts de serveur
Cas d'usage
  • Dashboards utilisateur
  • Flux sociaux
  • Contenu personnalisé
SSG
Génération statique au build
Avantages
  • Vitesse maximale
  • Hébergement économique
  • Mise en cache CDN
Inconvenients
  • Données figées au build
  • Rebuild nécessaire pour MAJ
  • Pas de personnalisation
Cas d'usage
  • Blog & documentation
  • Landing pages
  • Sites marketing
ISR
Régénération incrémentale
Avantages
  • Vitesse du statique
  • Données périodiquement fraîches
  • Scalabilité
Inconvenients
  • Données légèrement obsolètes
  • Configuration revalidate
  • Complexité accrue
Cas d'usage
  • E-commerce (produits)
  • Actualités
  • Agrégateurs de contenu
Client
Rendu côté navigateur
Avantages
  • Interactivité totale
  • Pas de serveur nécessaire
  • Expérience fluide
Inconvenients
  • SEO limité
  • Temps de chargement initial
  • Bundle JavaScript lourd
Cas d'usage
  • Applications SPA
  • Outils interactifs
  • Dashboards temps réel
Ouvrir le simulateur de rendering

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)
Architecture hybridetypescript
1// Architecture hybride dans un même projet
2
3// 1. Page d'accueil - SSG
4// app/page.tsx
5export default async function Home() {
6 const features = await fetch('https://api.example.com/features', {
7 cache: 'force-cache' // Statique
8 }).then(res => res.json());
9
10 return <FeaturesGrid features={features} />;
11}
12
13// 2. Liste produits - ISR
14// app/products/page.tsx
15export default async function Products() {
16 const products = await fetch('https://api.example.com/products', {
17 next: { revalidate: 300 } // ISR 5 minutes
18 }).then(res => res.json());
19
20 return <ProductList products={products} />;
21}
22
23// 3. Dashboard utilisateur - SSR
24// app/dashboard/page.tsx
25export default async function Dashboard() {
26 const userData = await fetch('https://api.example.com/user', {
27 cache: 'no-store' // SSR, toujours frais
28 }).then(res => res.json());
29
30 return <UserDashboard data={userData} />;
31}
32
33// 4. Panier - Client Component
34// components/cart.tsx
35'use client';
36import { useState } from 'react';
37
38export function Cart() {
39 const [items, setItems] = useState([]);
40 // Interactivité totale côté client
41 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.