Retour au blog

React 19 Server Components : Guide Pratique Pour Maîtriser la Nouvelle Ère de React

Salut HaWkers, React 19 a apporté un changement fondamental dans la façon dont nous construisons des applications web. Les Server Components représentent la plus grande évolution de React depuis l'introduction des Hooks, et comprendre ce concept est essentiel pour tout développeur frontend en 2025.

Vous êtes-vous déjà demandé pourquoi vos applications React envoient tant de JavaScript au navigateur ? Les Server Components sont arrivés pour résoudre exactement ce problème, permettant aux composants d'être rendus sur le serveur sans envoyer de code inutile au client.

Que Sont les Server Components

Les Server Components sont des composants React qui s'exécutent exclusivement sur le serveur. Contrairement aux composants traditionnels qui tournent dans le navigateur, ils ne sont jamais envoyés au client, résultant en des bundles plus petits et de meilleures performances.

React Server Components

Avantages Principaux

  • Zéro JavaScript côté client : Les composants serveur n'ajoutent pas au bundle
  • Accès direct aux ressources : Base de données, système de fichiers, APIs internes
  • Streaming automatique : Contenu envoyé progressivement à l'utilisateur
  • SEO optimisé : HTML complet disponible pour les crawlers

Client Components vs Server Components

La distinction entre ces deux types de composants est fondamentale pour architecturer des applications React modernes.

Server Components (Par Défaut dans React 19)

// app/produits/page.tsx
// Ceci est un Server Component par défaut

import { db } from '@/lib/database';

interface Produit {
  id: number;
  nom: string;
  prix: number;
  description: string;
}

async function chercherProduits(): Promise<Produit[]> {
  // Accès direct à la base - impossible dans les Client Components
  const produits = await db.query('SELECT * FROM produits ORDER BY cree_a DESC');
  return produits;
}

export default async function PageProduits() {
  const produits = await chercherProduits();

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Nos Produits</h1>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {produits.map((produit) => (
          <article key={produit.id} className="border rounded-lg p-4">
            <h2 className="text-xl font-semibold">{produit.nom}</h2>
            <p className="text-gray-600 mt-2">{produit.description}</p>
            <p className="text-2xl font-bold mt-4 text-green-600">
              € {produit.prix.toFixed(2)}
            </p>
          </article>
        ))}
      </div>
    </main>
  );
}

Client Components (Interactivité)

// components/BoutonPanier.tsx
'use client'; // Directive obligatoire pour les Client Components

import { useState } from 'react';

interface BoutonPanierProps {
  produitId: number;
  nom: string;
  prix: number;
}

export function BoutonPanier({ produitId, nom, prix }: BoutonPanierProps) {
  const [quantite, setQuantite] = useState(1);
  const [ajoute, setAjoute] = useState(false);

  const handleAjouter = async () => {
    try {
      await fetch('/api/panier', {
        method: 'POST',
        body: JSON.stringify({ produitId, quantite }),
        headers: { 'Content-Type': 'application/json' }
      });

      setAjoute(true);
      setTimeout(() => setAjoute(false), 2000);
    } catch (error) {
      console.error('Erreur lors de l\'ajout au panier :', error);
    }
  };

  return (
    <div className="flex items-center gap-4 mt-4">
      <div className="flex items-center border rounded">
        <button
          onClick={() => setQuantite(Math.max(1, quantite - 1))}
          className="px-3 py-1 hover:bg-gray-100"
        >
          -
        </button>
        <span className="px-4">{quantite}</span>
        <button
          onClick={() => setQuantite(quantite + 1)}
          className="px-3 py-1 hover:bg-gray-100"
        >
          +
        </button>
      </div>

      <button
        onClick={handleAjouter}
        disabled={ajoute}
        className={`px-6 py-2 rounded font-medium transition-colors ${
          ajoute
            ? 'bg-green-500 text-white'
            : 'bg-blue-600 text-white hover:bg-blue-700'
        }`}
      >
        {ajoute ? 'Ajouté !' : 'Ajouter au Panier'}
      </button>
    </div>
  );
}

Composition : Combiner Server et Client Components

La vraie magie se produit quand nous combinons les deux types de composants de manière stratégique.

// app/produits/[id]/page.tsx
// Server Component - récupère les données

import { db } from '@/lib/database';
import { BoutonPanier } from '@/components/BoutonPanier';
import { GalerieImages } from '@/components/GalerieImages';
import { AvisClients } from '@/components/AvisClients';

interface PageProps {
  params: { id: string };
}

async function chercherProduit(id: string) {
  const produit = await db.query(
    'SELECT * FROM produits WHERE id = ?',
    [id]
  );
  return produit[0];
}

async function chercherAvis(produitId: string) {
  return await db.query(
    'SELECT * FROM avis WHERE produit_id = ? ORDER BY date DESC LIMIT 10',
    [produitId]
  );
}

export default async function PageProduit({ params }: PageProps) {
  // Requêtes parallèles sur le serveur
  const [produit, avis] = await Promise.all([
    chercherProduit(params.id),
    chercherAvis(params.id)
  ]);

  if (!produit) {
    return <div>Produit non trouvé</div>;
  }

  return (
    <main className="container mx-auto p-4">
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* Client Component pour l'interactivité de la galerie */}
        <GalerieImages images={produit.images} />

        <div>
          <h1 className="text-3xl font-bold">{produit.nom}</h1>
          <p className="text-gray-600 mt-4">{produit.description}</p>

          <div className="mt-6">
            <span className="text-4xl font-bold text-green-600">
              € {produit.prix.toFixed(2)}
            </span>
          </div>

          {/* Client Component pour ajouter au panier */}
          <BoutonPanier
            produitId={produit.id}
            nom={produit.nom}
            prix={produit.prix}
          />
        </div>
      </div>

      {/* Server Component rendu avec les données du serveur */}
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Avis des Clients</h2>
        <AvisClients avis={avis} />
      </section>
    </main>
  );
}

GalerieImages comme Client Component

// components/GalerieImages.tsx
'use client';

import { useState } from 'react';
import Image from 'next/image';

interface GalerieImagesProps {
  images: string[];
}

export function GalerieImages({ images }: GalerieImagesProps) {
  const [imageActive, setImageActive] = useState(0);

  return (
    <div className="space-y-4">
      <div className="relative aspect-square bg-gray-100 rounded-lg overflow-hidden">
        <Image
          src={images[imageActive]}
          alt="Image du produit"
          fill
          className="object-cover"
          priority
        />
      </div>

      <div className="flex gap-2 overflow-x-auto">
        {images.map((img, index) => (
          <button
            key={index}
            onClick={() => setImageActive(index)}
            className={`relative w-20 h-20 rounded border-2 overflow-hidden flex-shrink-0 ${
              index === imageActive ? 'border-blue-500' : 'border-transparent'
            }`}
          >
            <Image src={img} alt="" fill className="object-cover" />
          </button>
        ))}
      </div>
    </div>
  );
}

Streaming et Suspense

Un des grands avantages des Server Components est le streaming de contenu avec Suspense, permettant à des parties de la page d'être affichées pendant que d'autres chargent encore.

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StatistiquesGenerales } from '@/components/StatistiquesGenerales';
import { GraphiqueVentes } from '@/components/GraphiqueVentes';
import { TableauCommandes } from '@/components/TableauCommandes';
import { LoadingSpinner } from '@/components/LoadingSpinner';

export default function Dashboard() {
  return (
    <main className="p-6 space-y-6">
      <h1 className="text-3xl font-bold">Dashboard</h1>

      {/* Les statistiques chargent en premier - rapides */}
      <Suspense fallback={<LoadingSpinner texte="Chargement des statistiques..." />}>
        <StatistiquesGenerales />
      </Suspense>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Le graphique peut prendre plus de temps */}
        <Suspense fallback={<LoadingSpinner texte="Chargement du graphique..." />}>
          <GraphiqueVentes />
        </Suspense>

        {/* Tableau indépendant du graphique */}
        <Suspense fallback={<LoadingSpinner texte="Chargement des commandes..." />}>
          <TableauCommandes />
        </Suspense>
      </div>
    </main>
  );
}

// components/StatistiquesGenerales.tsx
// Server Component - s'exécute sur le serveur

import { db } from '@/lib/database';

async function chercherStatistiques() {
  const [ventes, clients, commandes] = await Promise.all([
    db.query('SELECT SUM(montant) as total FROM ventes WHERE mois = CURRENT_MONTH'),
    db.query('SELECT COUNT(*) as total FROM clients WHERE actif = true'),
    db.query('SELECT COUNT(*) as total FROM commandes WHERE statut = "en_attente"')
  ]);

  return {
    ventesMois: ventes[0].total,
    clientsActifs: clients[0].total,
    commandesEnAttente: commandes[0].total
  };
}

export async function StatistiquesGenerales() {
  const stats = await chercherStatistiques();

  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500 text-sm">Ventes du Mois</h3>
        <p className="text-3xl font-bold text-green-600">
          € {stats.ventesMois.toLocaleString()}
        </p>
      </div>

      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500 text-sm">Clients Actifs</h3>
        <p className="text-3xl font-bold text-blue-600">
          {stats.clientsActifs.toLocaleString()}
        </p>
      </div>

      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500 text-sm">Commandes en Attente</h3>
        <p className="text-3xl font-bold text-orange-600">
          {stats.commandesEnAttente}
        </p>
      </div>
    </div>
  );
}

Server Actions : Mutations sur le Serveur

Les Server Actions permettent d'exécuter des fonctions sur le serveur directement depuis les composants, simplifiant drastiquement le traitement des formulaires.

// app/contact/page.tsx
import { envoyerMessage } from './actions';

export default function PageContact() {
  return (
    <main className="container mx-auto p-4 max-w-lg">
      <h1 className="text-3xl font-bold mb-6">Contactez-nous</h1>

      <form action={envoyerMessage} className="space-y-4">
        <div>
          <label htmlFor="nom" className="block text-sm font-medium mb-1">
            Nom
          </label>
          <input
            type="text"
            id="nom"
            name="nom"
            required
            className="w-full border rounded-lg px-4 py-2"
          />
        </div>

        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-1">
            Email
          </label>
          <input
            type="email"
            id="email"
            name="email"
            required
            className="w-full border rounded-lg px-4 py-2"
          />
        </div>

        <div>
          <label htmlFor="message" className="block text-sm font-medium mb-1">
            Message
          </label>
          <textarea
            id="message"
            name="message"
            rows={5}
            required
            className="w-full border rounded-lg px-4 py-2"
          />
        </div>

        <BoutonEnvoyer />
      </form>
    </main>
  );
}

// Bouton avec état de chargement
'use client';

import { useFormStatus } from 'react-dom';

function BoutonEnvoyer() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50"
    >
      {pending ? 'Envoi en cours...' : 'Envoyer le Message'}
    </button>
  );
}
// app/contact/actions.ts
'use server';

import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function envoyerMessage(formData: FormData) {
  const nom = formData.get('nom') as string;
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  // Validation côté serveur
  if (!nom || !email || !message) {
    throw new Error('Tous les champs sont obligatoires');
  }

  if (!email.includes('@')) {
    throw new Error('Email invalide');
  }

  // Sauvegarder en base
  await db.query(
    'INSERT INTO messages (nom, email, message, cree_a) VALUES (?, ?, ?, NOW())',
    [nom, email, message]
  );

  // Envoyer un email de notification
  await fetch(process.env.EMAIL_API_URL!, {
    method: 'POST',
    body: JSON.stringify({
      to: process.env.ADMIN_EMAIL,
      subject: `Nouveau message de ${nom}`,
      body: message
    })
  });

  // Revalider le cache et rediriger
  revalidatePath('/contact');
  redirect('/contact/succes');
}

Patterns de Cache et Revalidation

Comprendre le système de cache de React 19 et Next.js est crucial pour des applications performantes.

// lib/data.ts

// Cache par durée - revalide chaque heure
export async function chercherCategories() {
  const response = await fetch('https://api.exemple.com/categories', {
    next: { revalidate: 3600 } // 1 heure en secondes
  });
  return response.json();
}

// Cache statique - revalide uniquement au build
export async function chercherPagesStatiques() {
  const response = await fetch('https://api.exemple.com/pages', {
    cache: 'force-cache'
  });
  return response.json();
}

// Sans cache - toujours récupérer des données fraîches
export async function chercherNotifications() {
  const response = await fetch('https://api.exemple.com/notifications', {
    cache: 'no-store'
  });
  return response.json();
}

// Cache avec tags pour invalidation sélective
export async function chercherProduitsParCategorie(categorieId: string) {
  const response = await fetch(
    `https://api.exemple.com/categories/${categorieId}/produits`,
    {
      next: {
        revalidate: 300, // 5 minutes
        tags: [`categorie-${categorieId}`, 'produits']
      }
    }
  );
  return response.json();
}
// app/admin/produits/actions.ts
'use server';

import { revalidateTag, revalidatePath } from 'next/cache';

export async function mettreAJourProduit(produitId: string, donnees: FormData) {
  await db.query('UPDATE produits SET ... WHERE id = ?', [produitId]);

  // Invalider le cache spécifique
  revalidateTag(`produit-${produitId}`);
  revalidateTag('produits');

  // Ou invalider par chemin
  revalidatePath('/produits');
  revalidatePath(`/produits/${produitId}`);
}

export async function supprimerProduit(produitId: string) {
  await db.query('DELETE FROM produits WHERE id = ?', [produitId]);

  // Invalider tous les caches liés aux produits
  revalidateTag('produits');
  revalidatePath('/produits');
}

Erreurs Courantes et Comment les Éviter

Certaines erreurs sont fréquentes quand on travaille avec les Server Components pour la première fois.

Erreur 1 : Utiliser des Hooks dans les Server Components

// FAUX - les hooks ne fonctionnent pas dans les Server Components
export default async function Page() {
  const [etat, setEtat] = useState(''); // Erreur !
  // ...
}

// CORRECT - extraire dans un Client Component
'use client';
export function ComposantInteractif() {
  const [etat, setEtat] = useState('');
  // ...
}

Erreur 2 : Passer des Fonctions comme Props aux Client Components

// FAUX - les fonctions ne sont pas sérialisables
export default function ServerComponent() {
  const handleClick = () => console.log('click');

  return <ClientComponent onClick={handleClick} />; // Erreur !
}

// CORRECT - utiliser des Server Actions
// actions.ts
'use server';
export async function handleSubmit(formData: FormData) {
  // ...
}

// page.tsx
import { handleSubmit } from './actions';

export default function ServerComponent() {
  return (
    <form action={handleSubmit}>
      {/* ... */}
    </form>
  );
}

Si vous voulez maîtriser plus de techniques avancées de frontend, je vous recommande de consulter l'article CSS Moderne en 2025 où nous explorons les nouveautés qui complètent parfaitement React.

C'est parti ! 🦅

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires