# React 19 : Server Components en production, le guide complet
> Maîtrisez les Server Components de React 19 en production. Architecture, patterns, streaming, cache et optimisations pour des applications performantes.
- Published: 2026-01-08
- Updated: 2026-03-31
- Author: SharpSkill
- Tags: react 19, server components, rsc, performance, next.js
- Reading time: 14 min
---
Les Server Components représentent l'évolution la plus significative de React depuis les Hooks. Avec React 19, cette architecture devient mature et prête pour la production, permettant d'exécuter des composants directement sur le serveur tout en conservant l'interactivité côté client.
> **Prérequis**
>
> Ce guide suppose une connaissance de React et de Next.js App Router. Les exemples utilisent Next.js 14+ qui implémente nativement les React Server Components.
## Comprendre l'architecture Server Components
Les Server Components (RSC) introduisent un nouveau paradigme : certains composants s'exécutent exclusivement sur le serveur, d'autres sur le client, et les deux peuvent coexister dans la même arborescence. Cette séparation permet d'optimiser les performances en réduisant drastiquement le JavaScript envoyé au navigateur.
L'idée fondamentale repose sur le fait que de nombreux composants n'ont pas besoin d'interactivité. Un composant qui affiche une liste d'articles depuis une base de données, par exemple, peut entièrement s'exécuter côté serveur. Seuls les éléments interactifs (boutons, formulaires, animations) nécessitent du JavaScript client.
```tsx
// app/articles/page.tsx
// Ce composant s'exécute uniquement sur le serveur
// Aucun JavaScript n'est envoyé au client pour ce composant
import { getArticles } from '@/lib/articles'
import ArticleCard from './ArticleCard'
import LikeButton from './LikeButton'
// async/await directement dans le composant
// Possible uniquement avec les Server Components
export default async function ArticlesPage() {
// Appel base de données direct (pas d'API REST nécessaire)
const articles = await getArticles()
return (
Articles récents
{articles.map((article) => (
// ArticleCard est aussi un Server Component
{/* LikeButton est un Client Component (interactif) */}
))}
)
}
```
La directive `"use client"` marque explicitement les composants qui nécessitent le JavaScript du navigateur.
```tsx
// app/articles/LikeButton.tsx
'use client'
// useState et les hooks interactifs nécessitent "use client"
import { useState, useTransition } from 'react'
import { likeArticle } from '@/actions/articles'
interface LikeButtonProps {
articleId: string
initialLikes?: number
}
export default function LikeButton({ articleId, initialLikes = 0 }: LikeButtonProps) {
// État local pour l'UI optimiste
const [likes, setLikes] = useState(initialLikes)
const [isPending, startTransition] = useTransition()
const handleLike = () => {
// Mise à jour optimiste immédiate
setLikes((prev) => prev + 1)
// Server Action pour persister
startTransition(async () => {
await likeArticle(articleId)
})
}
return (
)
}
```
Cette architecture réduit considérablement le bundle JavaScript : seul le code de `LikeButton` est envoyé au client, pas celui de `ArticlesPage` ni de `ArticleCard`.
## Patterns de composition Server/Client
La composition entre Server et Client Components suit des règles précises. Un Server Component peut importer et rendre des Client Components, mais l'inverse n'est pas possible directement. Pour passer du contenu serveur à un composant client, le pattern `children` est la solution.
```tsx
// components/InteractiveWrapper.tsx
'use client'
import { useState, ReactNode } from 'react'
interface InteractiveWrapperProps {
children: ReactNode
expandable?: boolean
}
// Client Component qui encapsule du contenu serveur
export function InteractiveWrapper({ children, expandable = false }: InteractiveWrapperProps) {
const [isExpanded, setIsExpanded] = useState(!expandable)
if (!expandable) {
return
{children}
}
return (
{isExpanded && (
{/* children peut contenir des Server Components */}
{children}
)}
)
}
```
```tsx
// app/dashboard/page.tsx
// Server Component qui utilise le wrapper client
import { InteractiveWrapper } from '@/components/InteractiveWrapper'
import { getStats, getRecentActivity } from '@/lib/dashboard'
export default async function DashboardPage() {
// Requêtes parallèles côté serveur
const [stats, activity] = await Promise.all([
getStats(),
getRecentActivity()
])
return (
{/* Stats dans un wrapper expansible */}
{/* Ce contenu est rendu côté serveur puis passé au client */}
{/* Activité récente */}
)
}
```
Ce pattern permet de combiner l'interactivité client avec des données rendues côté serveur sans dupliquer la logique.
## Data fetching et mise en cache
> **Fetch étendu par React**
>
> React 19 étend automatiquement l'API `fetch` native pour ajouter la déduplication et la mise en cache. Les requêtes identiques dans le même rendu ne sont exécutées qu'une seule fois.
La récupération de données dans les Server Components se fait directement avec `async/await`. React gère automatiquement la déduplication des requêtes identiques.
```tsx
// lib/api.ts
// Configuration centralisée des requêtes avec cache
const API_BASE = process.env.API_URL
// Requête avec revalidation temporelle
export async function getProducts() {
const response = await fetch(`${API_BASE}/products`, {
// Revalider toutes les heures
next: { revalidate: 3600 }
})
if (!response.ok) {
throw new Error('Failed to fetch products')
}
return response.json()
}
// Requête sans cache (données en temps réel)
export async function getCurrentUser() {
const response = await fetch(`${API_BASE}/me`, {
// Pas de cache, toujours frais
cache: 'no-store'
})
if (!response.ok) {
return null
}
return response.json()
}
// Requête avec tag pour invalidation ciblée
export async function getProduct(id: string) {
const response = await fetch(`${API_BASE}/products/${id}`, {
next: {
tags: [`product-${id}`],
revalidate: 3600
}
})
if (!response.ok) {
throw new Error('Product not found')
}
return response.json()
}
```
Pour les accès base de données directs (Prisma, Drizzle), le cache React avec `unstable_cache` offre les mêmes capacités.
```tsx
// lib/db-queries.ts
import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'
// Cache des catégories (rarement modifiées)
export const getCategories = unstable_cache(
async () => {
return prisma.category.findMany({
orderBy: { name: 'asc' }
})
},
['categories'], // Clé de cache
{
revalidate: 86400, // 24 heures
tags: ['categories']
}
)
// Cache des produits par catégorie
export const getProductsByCategory = unstable_cache(
async (categoryId: string) => {
return prisma.product.findMany({
where: { categoryId },
include: { images: true },
orderBy: { createdAt: 'desc' }
})
},
['products-by-category'],
{
revalidate: 3600,
tags: ['products']
}
)
// Invalidation du cache après mutation
export async function createProduct(data: ProductInput) {
const product = await prisma.product.create({ data })
// Invalider les caches concernés
revalidateTag('products')
return product
}
```
## Streaming et Suspense pour une UX optimale
Le streaming permet d'envoyer progressivement le HTML au navigateur, affichant immédiatement les parties disponibles pendant que d'autres se chargent. Combiné à Suspense, ce mécanisme améliore drastiquement le Time to First Byte (TTFB) et l'expérience utilisateur perçue.
```tsx
// app/product/[id]/page.tsx
import { Suspense } from 'react'
import { getProduct } from '@/lib/products'
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import RecommendedProducts from './RecommendedProducts'
import { Skeleton } from '@/components/ui/Skeleton'
interface ProductPageProps {
params: { id: string }
}
export default async function ProductPage({ params }: ProductPageProps) {
// Cette requête bloque le rendu initial
const product = await getProduct(params.id)
return (
{/* Rendu immédiat avec les données produit */}
{/* Les reviews se chargent en streaming */}
Avis clients
}>
{/* Ce composant async sera streamé */}
{/* Les recommandations aussi */}
Produits similaires
}>
)
}
// Skeleton pour les reviews
function ReviewsSkeleton() {
return (
{[1, 2, 3].map((i) => (
))}
)
}
```
```tsx
// app/product/[id]/ProductReviews.tsx
// Server Component async qui sera streamé
import { getProductReviews } from '@/lib/reviews'
interface ProductReviewsProps {
productId: string
}
export default async function ProductReviews({ productId }: ProductReviewsProps) {
// Cette requête peut prendre du temps
// Le composant sera streamé quand elle sera terminée
const reviews = await getProductReviews(productId)
if (reviews.length === 0) {
return (
Aucun avis pour le moment. Soyez le premier à donner votre avis !
)
}
```
Le navigateur reçoit d'abord le shell HTML avec les skeletons, puis le contenu réel est injecté au fur et à mesure via streaming.
## Server Actions pour les mutations
Les Server Actions permettent d'exécuter du code serveur depuis les composants client sans créer d'API routes. Cette approche simplifie considérablement la gestion des mutations.
```tsx
// actions/cart.ts
'use server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
import { getCurrentUser } from '@/lib/auth'
// Action pour ajouter au panier
export async function addToCart(productId: string, quantity: number = 1) {
const user = await getCurrentUser()
if (!user) {
// Retourner une erreur structurée
return { error: 'Connexion requise', code: 'UNAUTHORIZED' }
}
try {
// Vérifier le stock
const product = await prisma.product.findUnique({
where: { id: productId }
})
if (!product || product.stock < quantity) {
return { error: 'Stock insuffisant', code: 'OUT_OF_STOCK' }
}
// Ajouter ou mettre à jour l'item du panier
await prisma.cartItem.upsert({
where: {
cartId_productId: {
cartId: user.cartId,
productId
}
},
update: {
quantity: { increment: quantity }
},
create: {
cartId: user.cartId,
productId,
quantity
}
})
// Invalider le cache de la page panier
revalidatePath('/cart')
return { success: true, message: 'Produit ajouté au panier' }
} catch (error) {
console.error('Add to cart error:', error)
return { error: 'Une erreur est survenue', code: 'SERVER_ERROR' }
}
}
// Action pour supprimer du panier
export async function removeFromCart(itemId: string) {
const user = await getCurrentUser()
if (!user) {
return { error: 'Connexion requise' }
}
await prisma.cartItem.delete({
where: { id: itemId, cart: { userId: user.id } }
})
revalidatePath('/cart')
return { success: true }
}
```
```tsx
// components/AddToCartButton.tsx
'use client'
import { useTransition } from 'react'
import { addToCart } from '@/actions/cart'
import { toast } from '@/components/ui/toast'
interface AddToCartButtonProps {
productId: string
}
export function AddToCartButton({ productId }: AddToCartButtonProps) {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(async () => {
const result = await addToCart(productId)
if (result.error) {
toast.error(result.error)
return
}
toast.success(result.message)
})
}
return (
)
}
```
> **Validation côté serveur**
>
> Toujours valider les données dans les Server Actions. Les validations côté client peuvent être contournées. Utiliser Zod ou une bibliothèque similaire pour une validation robuste.
## Gestion des erreurs et Error Boundaries
React 19 améliore la gestion des erreurs avec les Server Components. Les Error Boundaries fonctionnent de la même manière qu'avec les Client Components.
```tsx
// app/products/error.tsx
'use client'
// Error Boundary pour le segment /products
interface ErrorProps {
error: Error & { digest?: string }
reset: () => void
}
export default function ProductsError({ error, reset }: ErrorProps) {
return (
Erreur de chargement
Impossible de charger les produits. Veuillez réessayer.
{/* Afficher le digest pour le debugging */}
{error.digest && (
Référence : {error.digest}
)}
)
}
```
```tsx
// app/products/loading.tsx
// Loading UI pendant le chargement initial
export default function ProductsLoading() {
return (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
)
}
```
Pour une gestion plus fine des erreurs dans les composants spécifiques, le composant `ErrorBoundary` peut être utilisé directement.
```tsx
// components/ErrorBoundary.tsx
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Logger l'erreur vers un service de monitoring
console.error('ErrorBoundary caught:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
Une erreur est survenue
)
}
return this.props.children
}
}
```
## Optimisations de performance en production
Plusieurs techniques permettent d'optimiser les performances des Server Components en production.
```tsx
// app/layout.tsx
import { Suspense } from 'react'
import { headers } from 'next/headers'
// Précharger les données critiques
export const dynamic = 'force-dynamic'
export default async function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
{/* Header streamé indépendamment */}
}>
{children}
{/* Footer peut être statique */}
)
}
```
```tsx
// lib/prefetch.ts
// Préchargement des données pour les liens
import { preload } from 'react-dom'
export function prefetchProduct(productId: string) {
// Précharger les images
preload(`/api/products/${productId}/image`, { as: 'image' })
}
// Utilisation dans un composant
// prefetchProduct(id)}>
```
```tsx
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Optimisations de build
experimental: {
// Activer le Partial Prerendering (PPR)
ppr: true,
// Optimiser les imports de packages
optimizePackageImports: ['lucide-react', '@radix-ui/react-icons']
},
// Configuration des images
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ hostname: 'cdn.example.com' }
]
}
}
module.exports = nextConfig
```
## Conclusion
Les Server Components de React 19 transforment fondamentalement la façon de construire des applications React. Les points essentiels à retenir :
- ✅ **Séparation serveur/client** : utiliser `"use client"` uniquement pour les composants interactifs
- ✅ **Data fetching direct** : `async/await` dans les composants, sans useEffect ni API routes
- ✅ **Streaming avec Suspense** : affichage progressif pour une meilleure UX perçue
- ✅ **Server Actions** : mutations simplifiées sans créer d'endpoints API
- ✅ **Cache intelligent** : `revalidate` et `tags` pour optimiser les performances
- ✅ **Composition flexible** : pattern `children` pour mixer Server et Client Components
Cette architecture permet de créer des applications plus performantes avec moins de JavaScript côté client, tout en simplifiant considérablement le code. La transition vers les Server Components représente un investissement qui se rentabilise rapidement en termes de performance et de maintenabilité.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/fr/blog/react-next/react-19-server-components-production