React 19 Server Components : Guide Pratique Pour les Développeurs
Salut HaWkers, React 19 a apporté les Server Components comme fonctionnalité stable, changeant fondamentalement notre façon de penser le rendu dans React. Si vous n'avez pas encore bien compris comment ils fonctionnent ou quand les utiliser, ce guide pratique va tout clarifier.
Vous êtes-vous déjà demandé pourquoi vos bundles JavaScript sont si gros ou pourquoi la performance initiale de votre application est lente ? Les Server Components pourraient être la réponse.
Que Sont les Server Components
Les Server Components sont des composants React qui s'exécutent exclusivement sur le serveur. Contrairement au SSR traditionnel, ils n'envoient jamais de JavaScript au client.
Différence Fondamentale
Comparatif des approches :
| Aspect | Client Components | Server Components |
|---|---|---|
| Où s'exécute | Navigateur | Serveur |
| JavaScript côté client | Oui | Non |
| Accès à la base de données | Via API | Direct |
| État et événements | useState, onClick | Non disponible |
| Performance initiale | Plus lent | Plus rapide |
💡 Contexte : Les Server Components ne remplacent pas les Client Components. Ils travaillent ensemble, chacun dans ce qu'il fait de mieux.
Comment Ils Fonctionnent en Pratique
Voyons des exemples concrets de Server et Client Components :
Server Component Basique
// app/posts/page.tsx (Server Component par défaut)
import { db } from '@/lib/database';
// Ce composant ne va JAMAIS au navigateur
async function PostsPage() {
// Accès direct à la base de données
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<div className="posts-container">
<h1>Derniers Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
</article>
))}
</div>
);
}
export default PostsPage;Client Component
// components/LikeButton.tsx
'use client'; // Marqueur obligatoire
import { useState, useTransition } from 'react';
import { likePost } from '@/actions/posts';
interface LikeButtonProps {
postId: string;
initialLikes: number;
}
export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
const handleLike = () => {
startTransition(async () => {
const newLikes = await likePost(postId);
setLikes(newLikes);
});
};
return (
<button
onClick={handleLike}
disabled={isPending}
className="like-button"
>
{isPending ? '...' : `❤️ ${likes}`}
</button>
);
}Combiner Server et Client
// app/posts/[id]/page.tsx
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
import { CommentSection } from '@/components/CommentSection';
async function PostPage({ params }: { params: { id: string } }) {
const post = await db.posts.findUnique({
where: { id: params.id },
include: { author: true, comments: true },
});
if (!post) {
return <div>Post non trouvé</div>;
}
return (
<article>
<header>
<h1>{post.title}</h1>
<span>Par {post.author.name}</span>
</header>
<div className="content">
{post.content}
</div>
{/* Client Component avec interactivité */}
<LikeButton postId={post.id} initialLikes={post.likes} />
{/* Autre Client Component */}
<CommentSection
postId={post.id}
initialComments={post.comments}
/>
</article>
);
}
export default PostPage;
Patterns de Composition
La clé pour utiliser efficacement les Server Components est de comprendre les patterns de composition :
1. Passer des Server Components comme Children
// components/ClientWrapper.tsx
'use client';
import { useState } from 'react';
interface ClientWrapperProps {
children: React.ReactNode;
}
export function ClientWrapper({ children }: ClientWrapperProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className={isExpanded ? 'expanded' : 'collapsed'}>
<button onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Réduire' : 'Développer'}
</button>
{isExpanded && children}
</div>
);
}
// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper';
import { ExpensiveServerComponent } from '@/components/ExpensiveServerComponent';
export default function Page() {
return (
<ClientWrapper>
{/* Ce Server Component est passé comme children */}
<ExpensiveServerComponent />
</ClientWrapper>
);
}2. Pattern Slots
// components/Layout.tsx
'use client';
import { useState } from 'react';
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
content: React.ReactNode;
}
export function Layout({ header, sidebar, content }: LayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="layout">
<header>{header}</header>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
Toggle Sidebar
</button>
{sidebarOpen && <aside>{sidebar}</aside>}
<main>{content}</main>
</div>
);
}
// app/dashboard/page.tsx
import { Layout } from '@/components/Layout';
import { UserStats } from '@/components/UserStats';
import { Navigation } from '@/components/Navigation';
import { DashboardContent } from '@/components/DashboardContent';
export default function DashboardPage() {
return (
<Layout
header={<Navigation />}
sidebar={<UserStats />}
content={<DashboardContent />}
/>
);
}
Server Actions
React 19 a introduit les Server Actions pour les mutations de façon intégrée :
Créer des Server Actions
// actions/posts.ts
'use server';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
};
const validatedData = PostSchema.safeParse(rawData);
if (!validatedData.success) {
return { error: validatedData.error.flatten().fieldErrors };
}
try {
const post = await db.posts.create({
data: validatedData.data,
});
revalidatePath('/posts');
return { success: true, postId: post.id };
} catch (error) {
return { error: 'Échec de la création du post' };
}
}
export async function likePost(postId: string) {
const post = await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
revalidatePath(`/posts/${postId}`);
return post.likes;
}
export async function deletePost(postId: string) {
await db.posts.delete({ where: { id: postId } });
revalidatePath('/posts');
}Utiliser les Server Actions dans les Forms
// components/CreatePostForm.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '@/actions/posts';
export function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Titre</label>
<input
id="title"
name="title"
type="text"
required
disabled={isPending}
/>
{state?.error?.title && (
<span className="error">{state.error.title}</span>
)}
</div>
<div>
<label htmlFor="content">Contenu</label>
<textarea
id="content"
name="content"
rows={10}
required
disabled={isPending}
/>
{state?.error?.content && (
<span className="error">{state.error.content}</span>
)}
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Création...' : 'Créer Post'}
</button>
{state?.success && (
<div className="success">Post créé avec succès !</div>
)}
</form>
);
}
Optimisation et Performance
Les Server Components apportent des bénéfices significatifs de performance :
1. Streaming avec Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { SlowDataComponent } from '@/components/SlowDataComponent';
import { FastDataComponent } from '@/components/FastDataComponent';
export default function DashboardPage() {
return (
<div className="dashboard">
<h1>Dashboard</h1>
{/* Composant rapide se rend immédiatement */}
<FastDataComponent />
{/* Composant lent charge en streaming */}
<Suspense fallback={<div>Chargement des données...</div>}>
<SlowDataComponent />
</Suspense>
{/* Plusieurs Suspense pour streaming parallèle */}
<div className="widgets">
<Suspense fallback={<WidgetSkeleton />}>
<AnalyticsWidget />
</Suspense>
<Suspense fallback={<WidgetSkeleton />}>
<RevenueWidget />
</Suspense>
<Suspense fallback={<WidgetSkeleton />}>
<UsersWidget />
</Suspense>
</div>
</div>
);
}2. Preloading de Données
// lib/preload.ts
import { cache } from 'react';
export const getUser = cache(async (userId: string) => {
const user = await db.users.findUnique({ where: { id: userId } });
return user;
});
export const preloadUser = (userId: string) => {
void getUser(userId);
};
// components/UserProfile.tsx
import { getUser, preloadUser } from '@/lib/preload';
// Dans un composant parent
function ParentComponent({ userId }: { userId: string }) {
// Démarre le fetch à l'avance
preloadUser(userId);
return (
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
);
}
// Le composant enfant utilise les données pré-chargées
async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId); // Déjà en cache !
return (
<div className="profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
Quand Utiliser Chaque Type
Guide pratique pour décider entre Server et Client Components :
Utilisez les Server Components Pour
Cas idéaux :
- Fetch de données depuis la base
- Accès aux APIs privées
- Composants sans interactivité
- Contenu statique ou semi-statique
- Réduction de la taille du bundle
// Exemples de Server Components
// - Listes de données
// - Headers et footers
// - Navigation statique
// - Contenu de pages
// - Composants de layout
async function ProductList() {
const products = await db.products.findMany();
return (
<ul>
{products.map(product => (
<li key={product.id}>
<h3>{product.name}</h3>
<p>{product.price}</p>
</li>
))}
</ul>
);
}Utilisez les Client Components Pour
Cas idéaux :
- Interactivité (onClick, onChange)
- État local (useState)
- Effets (useEffect)
- APIs du navigateur (localStorage, geolocation)
- Bibliothèques client-only
// Exemples de Client Components
// - Formulaires
// - Modals et dialogs
// - Carousels et sliders
// - Tooltips et dropdowns
// - Composants avec animation
'use client';
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const search = async () => {
if (query.length > 2) {
const data = await fetch(`/api/search?q=${query}`);
setResults(await data.json());
}
};
const timer = setTimeout(search, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Rechercher..."
/>
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
</div>
);
}
Erreurs Courantes et Solutions
Évitez ces problèmes fréquents :
1. Importer un Client Component sans 'use client'
// ❌ Mauvais : utiliser des hooks sans 'use client'
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Erreur !
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ Correct : ajouter 'use client'
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}2. Passer des Fonctions Non Sérialisables
// ❌ Mauvais : passer une fonction du Server au Client
async function ServerComponent() {
const handleClick = () => console.log('clicked'); // Non sérialisable !
return <ClientButton onClick={handleClick} />; // Erreur !
}
// ✅ Correct : utiliser une Server Action
async function ServerComponent() {
return <ClientButton postId="123" />;
}
// Le Client Component appelle l'action
'use client';
import { likePost } from '@/actions/posts';
function ClientButton({ postId }: { postId: string }) {
return <button onClick={() => likePost(postId)}>Like</button>;
}3. Fetch dans un Client Component Inutilement
// ❌ Mauvais : fetching côté client quand ça pourrait être côté serveur
'use client';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ Correct : Server Component avec accès direct
async function UserList() {
const users = await db.users.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Migrer Vers les Server Components
Si vous avez une application React existante :
Stratégie de Migration
Étapes recommandées :
- Identifiez les composants sans interactivité
- Déplacez le fetch de données vers les Server Components
- Ajoutez 'use client' où nécessaire
- Refactorisez vers des patterns de composition
- Implémentez des Server Actions pour les mutations
Conclusion
React 19 Server Components représentent un changement fondamental dans la façon de construire des applications React. En exécutant les composants sur le serveur et en envoyant seulement du HTML au client, nous obtenons des applications plus rapides, des bundles plus petits, et un accès direct aux ressources serveur.
La clé est de comprendre que Server et Client Components travaillent ensemble. Utilisez les Server Components pour fetcher des données et rendre du contenu statique, et les Client Components pour l'interactivité. Avec cette division claire, vos applications seront plus performantes et plus faciles à maintenir.
Si vous voulez approfondir React et le développement frontend moderne, je recommande de jeter un œil à un autre article : Svelte 5 et Runes : Pourquoi le Framework Gagne du Terrain où vous découvrirez des alternatives intéressantes à React.

