React Server Components vs Server Actions : guide pratique 2026
Server Components, Server Actions, Client Components : la confusion règne. Voici un guide pratique 2026 pour savoir quand utiliser quoi, avec du vrai code.
On a auditer 12 codebases React/Next cette année. Dans 9 d'entre elles, RSC et Server Actions étaient mal utilisés. Composants client inutiles, mutations en route handlers, données fetched côté client par habitude. Le résultat : -300ms de TTI moyen une fois corrigé. Voici le guide qu'on aurait aimé avoir.
La confusion principale en 2 phrases
Server Components, c'est pour LIRE des données. Server Actions, c'est pour les ÉCRIRE (ou déclencher une mutation). Si vous gardez ça en tête, 80% des décisions deviennent évidentes.
Mais creusons, parce que la nuance compte.
React Server Components : le rendering serveur, repensé
Un RSC est un composant React qui s'exécute uniquement sur le serveur. Il n'envoie aucun JavaScript au client. Aucun. Zéro.
// app/products/page.tsx (RSC par défaut dans App Router)
import { db } from '@/db';
import { products } from '@/db/schema';
export default async function ProductsPage() {
const items = await db.select().from(products).limit(20);
return (
<ul>
{items.map((p) => (
<li key={p.id}>
<h2>{p.name}</h2>
<p>{p.price}€</p>
</li>
))}
</ul>
);
}
Ce code :
- Tape la base directement (pas de route API intermédiaire)
- Ne génère aucun bundle client
- Le HTML est streamé au navigateur
- Le user voit le contenu instantanément
C'est la fin du pattern useEffect + fetch + useState + loading pour afficher de la donnée.
Server Actions : les mutations sans REST
Une Server Action est une fonction async qui s'exécute sur le serveur, déclenchée depuis un composant client (ou un formulaire). Elle remplace les routes API pour les mutations.
// app/products/actions.ts
'use server';
import { db } from '@/db';
import { products } from '@/db/schema';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const CreateProductSchema = z.object({
name: z.string().min(2),
price: z.coerce.number().positive(),
});
export async function createProduct(formData: FormData) {
const parsed = CreateProductSchema.safeParse({
name: formData.get('name'),
price: formData.get('price'),
});
if (!parsed.success) {
return { error: 'Données invalides' };
}
await db.insert(products).values(parsed.data);
revalidatePath('/products');
return { success: true };
}
Utilisé depuis un composant client :
'use client';
import { useActionState } from 'react';
import { createProduct } from './actions';
export function ProductForm() {
const [state, action, pending] = useActionState(createProduct, null);
return (
<form action={action}>
<input name="name" required />
<input name="price" type="number" required />
<button disabled={pending}>{pending ? 'Création...' : 'Créer'}</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</form>
);
}
Pas de fetch, pas de JSON parsing, pas de gestion d'erreur HTTP. La fonction est appelée comme une fonction.
Le tableau de décision 2026
| Cas d'usage | Quoi utiliser | Pourquoi |
|-------------|---------------|----------|
| Afficher données DB | Server Component | Pas de waterfall, pas de bundle |
| Form submit | Server Action | Progressive enhancement gratuite |
| Bouton like async | Server Action | Pas besoin d'API REST |
| State local UI | Client Component (useState) | Réactivité immédiate |
| Animations | Client Component | Code doit tourner dans le navigateur |
| Upload de fichier | Server Action (FormData) | Native browser support |
| Search avec debounce | Client Component + Server Action | UX réactive + logique serveur |
| Webhook entrant | Route handler classique | Pas appelé par votre app |
| OAuth callback | Route handler | Standard HTTP nécessaire |
| API mobile | Route handler | Clients externes |
Les patterns qui marchent en prod
Pattern 1 : Le composant hybride
Un RSC qui contient un client component pour la partie interactive.
// app/products/[id]/page.tsx (RSC)
import { db } from '@/db';
import { AddToCartButton } from './add-to-cart-button';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.query.products.findFirst({
where: (p, { eq }) => eq(p.id, params.id),
});
if (!product) notFound();
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} price={product.price} />
</article>
);
}
Le contenu textuel et SEO est en RSC (HTML pur, instant). Le bouton interactif est un client component minimal.
Pattern 2 : L'optimistic update
'use client';
import { useOptimistic } from 'react';
import { likePost } from './actions';
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addLike] = useOptimistic(
initialLikes,
(state, increment: number) => state + increment
);
async function handleClick() {
addLike(1);
await likePost(postId);
}
return <button onClick={handleClick}>❤ {optimisticLikes}</button>;
}
L'UI réagit instantanément, l'action serveur tourne en arrière-plan.
Pattern 3 : Le streaming avec Suspense
import { Suspense } from 'react';
export default function Dashboard() {
return (
<main>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
</main>
);
}
async function Stats() {
const stats = await db.query.stats.findFirst(); // 200ms
return <StatsView data={stats} />;
}
async function Chart() {
const data = await db.query.chartData.findMany(); // 800ms
return <ChartView data={data} />;
}
Les deux composants fetch en parallèle. Stats apparaît à 200ms, Chart à 800ms. Total user-perceived : 800ms au lieu de 1000ms en séquentiel.
💡 Vous voulez qu'on architecture votre app React 19 avec RSC et Server Actions pour vous ? On en discute 15 minutes : rdv.lenobot.com.
Les anti-patterns à éviter
Anti-pattern 1 : Tout mettre en client
// MAUVAIS
'use client';
import { useEffect, useState } from 'react';
export default function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
C'est l'ancien monde. Vous payez : un round-trip réseau, un état de loading, du JS client, une route API à maintenir.
La version RSC fait la même chose en 4 lignes, sans JS client.
Anti-pattern 2 : Server Action pour de la lecture
// MAUVAIS
'use server';
export async function getProducts() {
return db.select().from(products);
}
Les Server Actions sont des POST HTTP. Pas de cache navigateur, pas de cache CDN. Pour de la lecture, utilisez un RSC ou une fonction 'use cache'.
Anti-pattern 3 : Passer des fonctions du serveur au client
// MAUVAIS
export default async function Page() {
const handler = () => console.log('hi');
return <ClientChild onClick={handler} />; // ne marche pas !
}
Les RSC ne peuvent pas passer de closures aux client components. Utilisez une Server Action si vous voulez une callback côté serveur.
Le mental model qui marche
Quand vous écrivez un nouveau composant en 2026, posez ces questions dans l'ordre :
- Lit-il des données ? RSC, par défaut.
- A-t-il besoin d'interactivité (state, event, hooks) ? Convertissez juste la partie interactive en client component.
- A-t-il une mutation ? Server Action, jamais une route API.
- Est-il consommé par un client externe (mobile, webhook) ? Route handler classique.
C'est tout. Cette discipline simple suffit à éviter 90% des erreurs d'architecture.
Prêt à structurer proprement votre app React 19 avec RSC et Server Actions sur votre stack en 2026 ? Notre équipe de devs seniors vous accompagne. Réservez votre appel découverte gratuit sur rdv.lenobot.com, 15 minutes pour évaluer votre projet, devis ferme sous 48h, sans engagement.
Article rédigé par L'équipe Lenobot.
Besoin d'aide avec votre projet ?
Nos experts sont prêts à vous accompagner dans votre transformation digitale.
Discutons de votre projet