⚡ Next.js
📋 Vue d'ensemble
Next.js est un framework React qui simplifie le développement d'applications web modernes. Next.js 13+ introduit l'App Router, une nouvelle architecture de routage basée sur les dossiers et les composants.
🏗️ App Router
Structure des dossiers
L'App Router utilise une structure de dossiers pour définir les routes :
app/
├── 📁 layout.tsx # Layout racine (toujours présent)
├── 📁 page.tsx # Page d'accueil (/)
├── 📁 globals.css # Styles globaux
├── 📁 shows/
│ ├── 📁 page.tsx # Liste des spectacles (/shows)
│ ├── 📁 [id]/
│ │ └── 📁 page.tsx # Détail d'un spectacle (/shows/[id])
│ └── 📁 new/
│ └── 📁 page.tsx # Créer un spectacle (/shows/new)
├── 📁 admin/
│ ├── 📁 layout.tsx # Layout admin (/admin)
│ └── 📁 page.tsx # Dashboard admin (/admin)
└── 📁 api/
├── 📁 shows/
│ └── 📁 route.ts # API spectacles (/api/shows)
└── 📁 users/
└── 📁 route.ts # API utilisateurs (/api/users)Layout racine
Le layout racine s'applique à toutes les pages de l'application :
tsx
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: {
default: 'LIPAIX - Théâtre d\'improvisation',
template: '%s | LIPAIX'
},
description: 'Découvrez nos spectacles et événements d\'improvisation',
keywords: ['théâtre', 'improvisation', 'spectacle', 'LIPAIX'],
authors: [{ name: 'LIPAIX Team' }],
creator: 'LIPAIX Team',
publisher: 'LIPAIX',
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL('https://www.lipaix.com'),
alternates: {
canonical: '/',
languages: {
'fr-FR': '/fr',
'en-US': '/en',
},
},
openGraph: {
type: 'website',
locale: 'fr_FR',
url: 'https://www.lipaix.com',
title: 'LIPAIX - Théâtre d\'improvisation',
description: 'Découvrez nos spectacles et événements d\'improvisation',
siteName: 'LIPAIX',
images: [
{
url: '/og-image.jpg',
width: 1200,
height: 630,
alt: 'LIPAIX - Théâtre d\'improvisation',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'LIPAIX - Théâtre d\'improvisation',
description: 'Découvrez nos spectacles et événements d\'improvisation',
images: ['/og-image.jpg'],
creator: '@lipaix',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: 'your-google-verification-code',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr" className="h-full">
<body className={`${inter.className} h-full`}>
<div className="min-h-full flex flex-col">
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</div>
</body>
</html>
);
}Layouts imbriqués
Les layouts peuvent être imbriqués pour créer des structures complexes :
tsx
// app/admin/layout.tsx
import { AdminSidebar } from '@/components/AdminSidebar';
import { AdminHeader } from '@/components/AdminHeader';
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50">
<AdminHeader />
<div className="flex">
<AdminSidebar />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
);
}
// app/admin/page.tsx
export default function AdminPage() {
return (
<div>
<h1 className="text-3xl font-bold mb-6">Tableau de bord administrateur</h1>
{/* Contenu de la page admin */}
</div>
);
}📱 Pages et composants
Page d'accueil
tsx
// app/page.tsx
import { Hero } from '@/components/Hero';
import { FeaturedEvents } from '@/components/FeaturedEvents';
import { AboutSection } from '@/components/AboutSection';
export default function HomePage() {
return (
<div>
<Hero />
<FeaturedEvents />
<AboutSection />
</div>
);
}Page dynamique avec paramètres
tsx
// app/shows/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getShowById } from '@/lib/shows';
import { EventDetails } from '@/components/EventDetails';
import { AvailabilityForm } from '@/components/AvailabilityForm';
interface ShowPageProps {
params: {
id: string;
};
}
export default async function ShowPage({ params }: ShowPageProps) {
const show = await getShowById(params.id);
if (!show) {
notFound();
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<EventDetails show={show} />
<AvailabilityForm showId={show.id} />
</div>
</div>
);
}
// Génération des métadonnées dynamiques
export async function generateMetadata({ params }: ShowPageProps) {
const show = await getShowById(params.id);
if (!show) {
return {
title: 'Spectacle non trouvé',
};
}
return {
title: show.title,
description: show.description,
openGraph: {
title: show.title,
description: show.description,
type: 'event',
startTime: show.date,
},
};
}Page avec recherche
tsx
// app/shows/page.tsx
import { SearchForm } from '@/components/SearchForm';
import { EventList } from '@/components/EventList';
import { getShows } from '@/lib/shows';
interface ShowsPageProps {
searchParams: {
q?: string;
status?: string;
date?: string;
};
}
export default async function ShowsPage({ searchParams }: ShowsPageProps) {
const shows = await getShows(searchParams);
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto">
<header className="mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Nos spectacles
</h1>
<p className="text-lg text-gray-600">
Découvrez tous nos événements d'improvisation
</p>
</header>
<SearchForm />
<div className="mt-8">
<EventList events={shows} />
</div>
</div>
</div>
);
}🔌 API Routes
Route API simple
tsx
// app/api/shows/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getShows, createShow } from '@/lib/shows';
import { validateShowData } from '@/lib/validation';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q') || '';
const status = searchParams.get('status') || '';
const limit = parseInt(searchParams.get('limit') || '10');
const shows = await getShows({ query, status, limit });
return NextResponse.json({
success: true,
data: shows,
pagination: {
total: shows.length,
limit,
hasMore: shows.length === limit
}
});
} catch (error) {
console.error('Erreur lors de la récupération des spectacles:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la récupération des spectacles'
},
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validation des données
const validationResult = validateShowData(body);
if (!validationResult.success) {
return NextResponse.json(
{
success: false,
error: 'Données invalides',
details: validationResult.errors
},
{ status: 400 }
);
}
// Vérification de l'authentification
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{
success: false,
error: 'Authentification requise'
},
{ status: 401 }
);
}
// Création du spectacle
const show = await createShow(body);
return NextResponse.json({
success: true,
data: show
}, { status: 201 });
} catch (error) {
console.error('Erreur lors de la création du spectacle:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la création du spectacle'
},
{ status: 500 }
);
}
}Route API avec paramètres dynamiques
tsx
// app/api/shows/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getShowById, updateShow, deleteShow } from '@/lib/shows';
interface ShowRouteProps {
params: {
id: string;
};
}
export async function GET(
request: NextRequest,
{ params }: ShowRouteProps
) {
try {
const show = await getShowById(params.id);
if (!show) {
return NextResponse.json(
{
success: false,
error: 'Spectacle non trouvé'
},
{ status: 404 }
);
}
return NextResponse.json({
success: true,
data: show
});
} catch (error) {
console.error('Erreur lors de la récupération du spectacle:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la récupération du spectacle'
},
{ status: 500 }
);
}
}
export async function PATCH(
request: NextRequest,
{ params }: ShowRouteProps
) {
try {
const body = await request.json();
// Vérification de l'authentification et des permissions
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{
success: false,
error: 'Authentification requise'
},
{ status: 401 }
);
}
// Mise à jour du spectacle
const updatedShow = await updateShow(params.id, body);
return NextResponse.json({
success: true,
data: updatedShow
});
} catch (error) {
console.error('Erreur lors de la mise à jour du spectacle:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour du spectacle'
},
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: ShowRouteProps
) {
try {
// Vérification de l'authentification et des permissions
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{
success: false,
error: 'Authentification requise'
},
{ status: 401 }
);
}
// Suppression du spectacle
await deleteShow(params.id);
return NextResponse.json({
success: true,
message: 'Spectacle supprimé avec succès'
});
} catch (error) {
console.error('Erreur lors de la suppression du spectacle:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la suppression du spectacle'
},
{ status: 500 }
);
}
}🎯 Server Components vs Client Components
Server Components (par défaut)
Les Server Components s'exécutent côté serveur et sont parfaits pour :
- Récupération de données - Accès direct aux bases de données
- Rendu statique - Contenu qui ne change pas souvent
- SEO - Contenu indexable par les moteurs de recherche
tsx
// app/shows/page.tsx (Server Component)
import { getShows } from '@/lib/shows';
import { EventList } from '@/components/EventList';
export default async function ShowsPage() {
// Cette fonction s'exécute côté serveur
const shows = await getShows();
return (
<div>
<h1>Nos spectacles</h1>
<EventList events={shows} />
</div>
);
}Client Components
Les Client Components s'exécutent côté navigateur et sont nécessaires pour :
- Interactivité - Gestion des événements utilisateur
- État local - useState, useEffect, etc.
- APIs du navigateur - localStorage, window, etc.
tsx
// components/EventForm.tsx (Client Component)
'use client';
import { useState } from 'react';
export function EventForm() {
const [formData, setFormData] = useState({
title: '',
description: '',
date: ''
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch('/api/shows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
// Gérer le succès
alert('Spectacle créé avec succès !');
}
} catch (error) {
console.error('Erreur:', error);
}
};
return (
<form onSubmit={handleSubmit}>
{/* Champs du formulaire */}
</form>
);
}Mélange Server et Client
tsx
// app/admin/page.tsx (Server Component)
import { getStats } from '@/lib/admin';
import { AdminDashboard } from '@/components/AdminDashboard';
export default async function AdminPage() {
// Récupération des données côté serveur
const stats = await getStats();
return (
<div>
<h1>Tableau de bord administrateur</h1>
{/* Passer les données au composant client */}
<AdminDashboard initialStats={stats} />
</div>
);
}
// components/AdminDashboard.tsx (Client Component)
'use client';
import { useState, useEffect } from 'react';
interface AdminDashboardProps {
initialStats: any;
}
export function AdminDashboard({ initialStats }: AdminDashboardProps) {
const [stats, setStats] = useState(initialStats);
const [loading, setLoading] = useState(false);
// Mise à jour en temps réel
useEffect(() => {
const interval = setInterval(async () => {
setLoading(true);
try {
const response = await fetch('/api/admin/stats');
const newStats = await response.json();
setStats(newStats.data);
} catch (error) {
console.error('Erreur lors de la mise à jour des stats:', error);
} finally {
setLoading(false);
}
}, 30000); // Mise à jour toutes les 30 secondes
return () => clearInterval(interval);
}, []);
return (
<div>
{loading && <div>Mise à jour...</div>}
{/* Affichage des statistiques */}
</div>
);
}🚀 Optimisations de performance
Génération statique
tsx
// app/shows/[id]/page.tsx
export async function generateStaticParams() {
// Générer les pages statiques pour les spectacles populaires
const popularShows = await getPopularShows();
return popularShows.map((show) => ({
id: show.id,
}));
}
// Revalidation toutes les heures
export const revalidate = 3600;Lazy loading des composants
tsx
// app/admin/page.tsx
import dynamic from 'next/dynamic';
// Charger le composant AdminPanel seulement quand nécessaire
const AdminPanel = dynamic(() => import('@/components/AdminPanel'), {
loading: () => <div>Chargement de l'admin...</div>,
ssr: false // Désactiver le rendu côté serveur
});
// Charger le composant Chart avec un délai
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div>Chargement du graphique...</div>,
ssr: false
});
export default function AdminPage() {
const [showAdmin, setShowAdmin] = useState(false);
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowAdmin(!showAdmin)}>
{showAdmin ? 'Masquer' : 'Afficher'} l'admin
</button>
{showAdmin && <AdminPanel />}
<button onClick={() => setShowChart(!showChart)}>
{showChart ? 'Masquer' : 'Afficher'} le graphique
</button>
{showChart && <Chart />}
</div>
);
}Optimisation des images
tsx
// components/EventImage.tsx
import Image from 'next/image';
interface EventImageProps {
event: {
id: string;
title: string;
imageUrl?: string;
};
}
export function EventImage({ event }: EventImageProps) {
return (
<div className="relative aspect-video overflow-hidden rounded-lg">
{event.imageUrl ? (
<Image
src={event.imageUrl}
alt={event.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={false}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-600 flex items-center justify-center">
<span className="text-white text-4xl">🎭</span>
</div>
)}
</div>
);
}🧪 Tests et qualité
Tests des pages
tsx
// __tests__/pages/shows.test.tsx
import { render, screen } from '@testing-library/react';
import ShowsPage from '@/app/shows/page';
// Mock de la fonction getShows
jest.mock('@/lib/shows', () => ({
getShows: jest.fn()
}));
describe('ShowsPage', () => {
it('should render shows list correctly', async () => {
const mockShows = [
{ id: '1', title: 'Show 1', description: 'Description 1' },
{ id: '2', title: 'Show 2', description: 'Description 2' }
];
// Mock de la réponse
const { getShows } = require('@/lib/shows');
getShows.mockResolvedValue(mockShows);
// Rendu de la page
const page = await ShowsPage();
render(page);
expect(screen.getByText('Nos spectacles')).toBeInTheDocument();
expect(screen.getByText('Show 1')).toBeInTheDocument();
expect(screen.getByText('Show 2')).toBeInTheDocument();
});
});🚀 Bonnes pratiques
Do's
- ✅ Utiliser les Server Components par défaut pour les données statiques
- ✅ Séparer les responsabilités entre Server et Client Components
- ✅ Optimiser les images avec le composant Image de Next.js
- ✅ Gérer les erreurs avec notFound() et error.tsx
- ✅ Utiliser la génération statique quand c'est possible
Don'ts
- ❌ Abuser des Client Components - Utiliser les Server Components quand possible
- ❌ Oublier la gestion d'erreurs - Toujours gérer les cas d'erreur
- ❌ Ignorer le SEO - Utiliser les métadonnées appropriées
- ❌ Négliger les performances - Optimiser avec le lazy loading
🚀 Prochaines étapes
- React Fundamentals - Bases de React 18
- TailwindCSS - Système de design
- Frontend Overview - Vue d'ensemble du frontend
- Backend - PayloadCMS et architecture
Next.js est le cœur de notre application. Maîtrisez l'App Router pour créer des expériences utilisateur exceptionnelles ! 🚀
