React Server Components en production : patterns et pièges à éviter

React Server Components en production : patterns éprouvés, anti-patterns fréquents et stratégies de débogage pour des applications Next.js 15 robustes.

Patterns et pièges des React Server Components en production

React Server Components (RSC) changent la donne pour le rendu côté serveur dans Next.js 15, mais leur adoption en production révèle des pièges que la documentation officielle ne couvre pas toujours. Cet article détaille les patterns qui fonctionnent, ceux qui cassent, et comment diagnostiquer les problèmes avant qu'ils n'atteignent la production.

Server Components vs Client Components

Un Server Component s'exécute uniquement sur le serveur et n'envoie aucun JavaScript au navigateur. Un Client Component (marqué "use client") s'exécute des deux côtés. La règle : garder les Client Components aussi petits et aussi bas dans l'arbre que possible.

La frontière serveur-client : comprendre le boundary pattern

Le piège le plus courant avec les RSC concerne la frontière entre Server et Client Components. Dès qu'un composant porte la directive "use client", tous ses enfants importés deviennent aussi des Client Components, même sans la directive.

ProductPage.tsx (Server Component)tsx
import { ProductDetails } from './ProductDetails'
import { AddToCartButton } from './AddToCartButton'

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const product = await getProduct(id)

  return (
    <div>
      {/* Server Component : accès direct à la DB */}
      <ProductDetails product={product} />
      {/* Client Component : interactivité isolée */}
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  )
}
AddToCartButton.tsx (Client Component)tsx
'use client'

import { useState } from 'react'

export function AddToCartButton({ productId, price }: { productId: string; price: number }) {
  const [adding, setAdding] = useState(false)

  async function handleAdd() {
    setAdding(true)
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, quantity: 1 }),
    })
    setAdding(false)
  }

  return (
    <button onClick={handleAdd} disabled={adding}>
      {adding ? 'Ajout...' : `Ajouter au panier — ${price}`}
    </button>
  )
}

Le pattern clé : passer les données en props sérialisables depuis le Server Component vers le Client Component. Les fonctions, les classes et les objets Date ne traversent pas cette frontière.

Anti-pattern : le Client Component wrapper inutile

Une erreur fréquente consiste à créer un Client Component qui encapsule des Server Components enfants, forçant tout le sous-arbre côté client.

PageWrapper.tsx — ANTI-PATTERNtsx
'use client'

import { useState } from 'react'

// Tout le contenu enfant devient client-side
export function PageWrapper({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
  return (
    <div className={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle theme
      </button>
      {children}
    </div>
  )
}
children comme slot

La solution : passer les Server Components en tant que children (slot pattern). Les enfants passés comme props restent des Server Components même si le parent est un Client Component. Le code ci-dessus fonctionne correctement tant que children est passé depuis un Server Component parent.

layout.tsx (Server Component)tsx
import { PageWrapper } from './PageWrapper'
import { HeavyServerContent } from './HeavyServerContent'

export default function Layout() {
  return (
    <PageWrapper>
      {/* Reste un Server Component malgré le wrapper client */}
      <HeavyServerContent />
    </PageWrapper>
  )
}

Ce pattern de composition préserve les bénéfices du rendu serveur pour le contenu lourd tout en permettant l'interactivité au niveau du wrapper.

Gestion des données asynchrones : le pattern fetch-in-component

React 19 et Next.js 15 permettent d'utiliser async/await directement dans les Server Components. Ce pattern simplifie la récupération de données par rapport aux anciennes approches avec getServerSideProps.

UserProfile.tsx (Server Component)tsx
import { cache } from 'react'

// Déduplique les appels identiques dans le même rendu
const getUser = cache(async (userId: string) => {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 3600 }, // Cache 1h
  })
  if (!res.ok) throw new Error('Utilisateur non trouvé')
  return res.json()
})

export default async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId)

  return (
    <section>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>Membre depuis {new Date(user.createdAt).toLocaleDateString('fr-FR')}</p>
    </section>
  )
}

Trois points critiques :

  • cache() de React déduplique les appels identiques pendant un seul rendu serveur
  • next: { revalidate } contrôle la durée de cache côté Next.js
  • Les erreurs dans un Server Component async déclenchent le error.tsx le plus proche

Piège de la sérialisation : ce qui ne passe pas la frontière

Les données échangées entre Server et Client Components doivent être sérialisables en JSON. Voici ce qui provoque des erreurs silencieuses ou des crashes.

tsx
// PIÈGE : passer des types non-sérialisables
// Fonction — ne fonctionne pas
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// Utiliser une Server Action importée
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />

// Objet Date — ne fonctionne pas
<ClientComp createdAt={new Date()} />
// String ISO — fonctionne
<ClientComp createdAt={new Date().toISOString()} />

// Map, Set, RegExp — ne fonctionne pas
<ClientComp data={new Map([['key', 'value']])} />
// Objet ou tableau simple — fonctionne
<ClientComp data={{ key: 'value' }} />

Les Server Actions (fonctions marquées "use server") constituent l'exception : elles peuvent être passées comme props à un Client Component car Next.js les transforme en endpoints HTTP.

Prêt à réussir tes entretiens React / Next.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Streaming et Suspense : patterns de chargement progressif

Le streaming SSR avec Suspense permet d'envoyer le HTML progressivement au navigateur. Le pattern optimal utilise des Suspense boundaries granulaires autour de chaque section asynchrone.

DashboardPage.tsx (Server Component)tsx
import { Suspense } from 'react'
import { RevenueChart } from './RevenueChart'
import { RecentOrders } from './RecentOrders'
import { UserStats } from './UserStats'

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-6">
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>

      <Suspense fallback={<StatsSkeleton />}>
        <UserStats />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

Chaque section se charge indépendamment. Si RevenueChart prend 3 secondes et UserStats 200ms, les stats apparaissent immédiatement sans attendre le graphique.

Suspense et SEO

Le contenu à l'intérieur d'un Suspense boundary est rendu côté serveur et inclus dans le HTML initial. Les crawlers voient le contenu complet. Le streaming affecte uniquement la vitesse de livraison au navigateur, pas la visibilité SEO.

Débogage en production : tracer les problèmes RSC

Les erreurs RSC sont souvent cryptiques. Trois techniques de diagnostic fonctionnent en production.

1. Identifier les hydration mismatches

debug-hydration.tsxtsx
'use client'

import { useEffect, useState } from 'react'

export function HydrationDebug() {
  const [isClient, setIsClient] = useState(false)

  useEffect(() => {
    setIsClient(true)
  }, [])

  if (process.env.NODE_ENV !== 'development') return null

  return (
    <div style={{ position: 'fixed', bottom: 0, right: 0, padding: '4px 8px', fontSize: 12 }}>
      {isClient ? 'Client' : 'Server'}
    </div>
  )
}

2. Logger le payload RSC

Dans Next.js 15, activer le logging RSC dans next.config.ts :

next.config.tstypescript
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true, // Affiche les URLs complètes des fetch
    },
  },
}

export default nextConfig

3. Vérifier la taille du payload

Un payload RSC trop volumineux (> 128 KB) dégrade les performances. Surveiller dans les DevTools réseau les requêtes avec le content-type text/x-component.

Pattern avancé : composition avec Server Actions

Les Server Actions combinées aux Server Components créent un pattern CQRS naturel : lecture sur le serveur (RSC), écriture via les actions.

TodoList.tsx (Server Component)tsx
import { getTodos } from '@/lib/services/todo'
import { TodoForm } from './TodoForm'
import { deleteTodo } from '@/lib/actions/todo'

export default async function TodoList() {
  const todos = await getTodos()

  return (
    <div>
      <TodoForm />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.title}
            <form action={deleteTodo}>
              <input type="hidden" name="id" value={todo.id} />
              <button type="submit">Supprimer</button>
            </form>
          </li>
        ))}
      </ul>
    </div>
  )
}
actions/todo.tstsx
'use server'

import { revalidatePath } from 'next/cache'
import { TodoService } from '@/lib/services/todo'

export async function deleteTodo(formData: FormData) {
  const id = formData.get('id') as string
  await TodoService.delete(id)
  revalidatePath('/todos')
}

Le revalidatePath déclenche un nouveau rendu du Server Component avec les données fraîches, sans rechargement de page.

Pour approfondir les questions d'entretien sur ces sujets, consulter le module Server Actions Next.js et le module Data Fetching Next.js sur SharpSkill. La documentation officielle de React détaille les spécifications techniques complètes des Server Components.

Conclusion

  • Garder les Client Components petits et isolés en bas de l'arbre de composants
  • Utiliser le slot pattern (children) pour préserver les Server Components dans un wrapper client
  • Toujours vérifier la sérialisabilité des props passées à travers la frontière serveur-client
  • Placer des Suspense boundaries granulaires autour de chaque section asynchrone indépendante
  • Surveiller la taille des payloads RSC en production (objectif < 128 KB)
  • Combiner Server Components (lecture) et Server Actions (écriture) pour un pattern CQRS naturel
  • Utiliser cache() de React pour dédupliquer les requêtes dans un même rendu serveur

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#react server components
#next.js 15
#rsc patterns
#production
#react 19

Partager

Articles similaires