React 19 : Server Components et Actions

Front-end 30/03/2026 14:00:00 angularforall.com
React React 19 Server Components Server Actions Next.js
React 19 : Server Components et Actions

Découvrez React 19 : Server Components, Server Actions, useFormStatus, useOptimistic et use(). Guide pratique pour débutants avec Next.js 14.

La révolution React 19

React 19, sorti en décembre 2024, est la mise à jour la plus significative depuis l'introduction des hooks en 2019. Elle apporte un changement de paradigme fondamental : les composants React peuvent désormais s'exécuter côté serveur, lire des bases de données directement, et soumettre des formulaires sans écrire une seule ligne d'API REST. Pour un développeur junior, comprendre cette évolution est désormais essentiel.

Ce qui change avec React 19

Fonctionnalité Avant React 19 React 19
Rendu composants Client uniquement (navigateur) Serveur + Client
Appels BDD / API Via fetch() dans useEffect Directement dans le composant serveur
Soumission formulaire Handler + fetch() + state Server Action (action={maFonction})
État optimiste Gestion manuelle complexe useOptimistic()
Lecture promises useEffect + useState use(promise)
Bundle JS client Tout envoyé au navigateur Composants serveur exclus du bundle
Important : React 19 ne remplace pas React 18 — il l'étend. Vos composants existants continuent de fonctionner. RSC et Server Actions sont des fonctionnalités additionnelles, pas des ruptures de compatibilité.

Prérequis

Pour utiliser React 19 avec toutes ses fonctionnalités (RSC, Server Actions), vous avez besoin d'un framework qui intègre un serveur Node.js. Les deux options principales :

  • Next.js 14+ — intégration native, la plus répandue en production
  • Remix 2+ — aussi prise en charge complète
  • React seul (Vite) — uniquement les hooks React 19, pas les RSC

React Server Components (RSC)

Un React Server Component est un composant qui s'exécute exclusivement sur le serveur Node.js. Il peut lire des fichiers, interroger une base de données, accéder à des variables d'environnement secrètes — sans que ce code ne soit jamais envoyé au navigateur. Le résultat (HTML + JSON sérialisé) est streamé vers le client.

Exemple concret : liste de produits depuis une BDD

// app/products/page.tsx (Next.js App Router)
// Par défaut, tous les fichiers dans app/ sont des Server Components
// PAS besoin de 'use client' — c'est le comportement par défaut

import { db } from '@/lib/database'; // Import direct BDD ✅

// async/await directement dans le composant — impossible avant React 19
async function ProductList() {
  // Requête BDD directe — le code ne part JAMAIS au navigateur
  const products = await db.query('SELECT * FROM products WHERE active = true');

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <h2>{product.name}</h2>
          <p>Prix : {product.price} €</p>
        </li>
      ))}
    </ul>
  );
}

export default ProductList;

Avantages clés des Server Components

// AVANT React 19 (composant client classique)
// Le code de fetch ET la logique métier sont envoyés au navigateur
'use client';
import { useEffect, useState } from 'react';

function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 1. Appel API depuis le navigateur
    // 2. API appelle la BDD
    // 3. BDD retourne les données
    // 4. API sérialise et retourne JSON
    // 5. Composant met à jour le state
    fetch('/api/products')
      .then(r => r.json())
      .then(data => { setProducts(data); setLoading(false); });
  }, []);

  if (loading) return <p>Chargement...</p>;
  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// AVEC React 19 Server Component
// Plus besoin d'API route, de state, d'useEffect
// Zéro JS envoyé au navigateur pour ce composant

async function ProductList() {
  // Accès BDD direct, sécurisé côté serveur
  const products = await fetchProductsFromDB();
  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// Bénéfices mesurables :
// ✅ Bundle JS réduit (code serveur exclu)
// ✅ Pas de waterfall réseau client
// ✅ Données fraîches à chaque requête
// ✅ Clés API et secrets jamais exposés
Limitation : Les Server Components ne peuvent pas utiliser les hooks React (useState, useEffect, etc.), les gestionnaires d'événements (onClick), ni les APIs navigateur (window, localStorage). Pour ces besoins, utilisez un Client Component.

Composants Client vs Serveur

La règle de base est simple : tout composant est un Server Component par défaut dans le App Router de Next.js. Pour en faire un Client Component, ajoutez 'use client' en première ligne.

Capacité Server Component Client Component
Accès BDD direct ✅ Oui ❌ Non
Variables d'env secrètes ✅ Oui ❌ Non (NEXT_PUBLIC_ seulement)
async/await direct ✅ Oui ❌ Non
useState / useEffect ❌ Non ✅ Oui
onClick, onChange… ❌ Non ✅ Oui
window, localStorage ❌ Non ✅ Oui
Envoyé au navigateur ❌ Non (HTML seulement) ✅ Oui (JS inclus)

Pattern de composition Server + Client

// app/dashboard/page.tsx — Server Component (pas de 'use client')
// Récupère les données côté serveur, passe au composant client

import { UserDashboard } from '@/components/UserDashboard'; // Client Component
import { getUserData } from '@/lib/user-service';           // Service BDD

// Le composant page est Server, getUserData s'exécute côté serveur
async function DashboardPage() {
  // Récupère l'utilisateur depuis la BDD (côté serveur)
  const user = await getUserData(); // Pas besoin d'API route !

  // Passe les données au Client Component via props (sérialisées en JSON)
  return <UserDashboard initialUser={user} />;
}

export default DashboardPage;
// components/UserDashboard.tsx — Client Component
'use client'; // Directif obligatoire pour activer les hooks

import { useState } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

// Reçoit les données initiales du Server Component parent
export function UserDashboard({ initialUser }: { initialUser: User }) {
  // useState est autorisé ici car 'use client'
  const [user, setUser] = useState(initialUser);
  const [editing, setEditing] = useState(false);

  return (
    <div>
      <h1>Bonjour, {user.name}</h1>
      {editing ? (
        <EditForm user={user} onSave={setUser} />
      ) : (
        <button onClick={() => setEditing(true)}>Modifier</button>
      )}
    </div>
  );
}
Règle de composition : Un Server Component peut importer et utiliser des Client Components. L'inverse n'est pas possible directement — un Client Component ne peut pas importer un Server Component. Pensez-y comme un arbre : les Server Components sont les branches, les Client Components sont les feuilles.

Server Actions : mutations sans API

Les Server Actions sont des fonctions asynchrones qui s'exécutent côté serveur mais peuvent être appelées depuis le client. Elles simplifient radicalement la soumission de formulaires et les mutations de données — plus besoin de créer une route API dédiée pour chaque opération.

Formulaire de contact avec Server Action

// app/contact/actions.ts — Server Actions dans un fichier dédié
'use server'; // Marque toutes les fonctions comme Server Actions

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

// Cette fonction s'exécute sur le serveur, jamais dans le navigateur
export async function submitContactForm(formData: FormData) {
  // FormData est automatiquement passé par le formulaire HTML
  const name    = formData.get('name')    as string;
  const email   = formData.get('email')   as string;
  const message = formData.get('message') as string;

  // Validation côté serveur (jamais dépendant du client)
  if (!name || !email || !message) {
    return { error: 'Tous les champs sont obligatoires' };
  }

  // Insert direct en BDD (pas d'API intermédiaire !)
  await db.query(
    'INSERT INTO contacts (name, email, message) VALUES (?, ?, ?)',
    [name, email, message]
  );

  // Invalide le cache de la page pour afficher les nouvelles données
  revalidatePath('/admin/contacts');

  return { success: true };
}
// app/contact/page.tsx — Formulaire qui utilise la Server Action
import { submitContactForm } from './actions';

// Ce formulaire fonctionne MÊME SANS JavaScript côté client !
export default function ContactPage() {
  return (
    <main>
      <h1>Contactez-nous</h1>

      {/* action={submitContactForm} : appelle directement la Server Action */}
      <form action={submitContactForm}>
        <div>
          <label htmlFor="name">Nom</label>
          <input id="name" name="name" type="text" required />
        </div>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" name="email" type="email" required />
        </div>
        <div>
          <label htmlFor="message">Message</label>
          <textarea id="message" name="message" required></textarea>
        </div>
        <button type="submit">Envoyer</button>
      </form>
    </main>
  );
}

Server Action depuis un Client Component

// components/AddToCartButton.tsx — Client Component qui appelle une Server Action
'use client';

import { useState } from 'react';
import { addToCart } from '@/app/cart/actions'; // Import d'une Server Action

interface Props {
  productId: number;
}

export function AddToCartButton({ productId }: Props) {
  const [pending, setPending] = useState(false);
  const [message, setMessage] = useState('');

  const handleClick = async () => {
    setPending(true);
    try {
      // Appel de la Server Action — s'exécute sur le serveur
      const result = await addToCart(productId);
      setMessage(result.success ? 'Ajouté au panier !' : 'Erreur');
    } finally {
      setPending(false);
    }
  };

  return (
    <div>
      <button onClick={handleClick} disabled={pending}>
        {pending ? 'Ajout...' : 'Ajouter au panier'}
      </button>
      {message && <p aria-live="polite">{message}</p>}
    </div>
  );
}
Sécurité : Les Server Actions sont exposées comme des endpoints HTTP POST. Next.js génère un token CSRF automatiquement. Validez toujours les données côté serveur, même si vous avez déjà une validation côté client.

Nouveaux hooks : useFormStatus et useOptimistic

useFormStatus : état du formulaire parent

useFormStatus permet à un composant enfant de connaître l'état de soumission du formulaire qui le contient — sans prop drilling.

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

import { useFormStatus } from 'react-dom'; // Importé depuis react-dom

// Composant réutilisable pour tous les boutons de soumission
export function SubmitButton({ label }: { label: string }) {
  // pending = true quand la Server Action est en cours d'exécution
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {/* Affiche un état de chargement pendant la soumission */}
      {pending ? 'Envoi en cours...' : label}
    </button>
  );
}
// app/contact/page.tsx — Utilisation avec Server Action
import { submitContactForm } from './actions';
import { SubmitButton } from '@/components/SubmitButton';

export default function ContactPage() {
  return (
    <form action={submitContactForm}>
      <input name="email" type="email" required />
      {/* SubmitButton "sait" automatiquement que le formulaire est en cours */}
      <SubmitButton label="S'inscrire" />
    </form>
  );
}

useOptimistic : mises à jour optimistes

useOptimistic permet d'afficher immédiatement le résultat attendu d'une action avant que le serveur ait confirmé. L'interface reste réactive même sur connexion lente.

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

import { useOptimistic, useState } from 'react';
import { addTodo } from '@/app/todos/actions';

interface Todo {
  id: number;
  text: string;
  sending?: boolean; // Marqueur d'état optimiste
}

export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [todos, setTodos] = useState<Todo[]>(initialTodos);

  // useOptimistic(état, (étatActuel, valeurOptimiste) => nouvelÉtat)
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodoText: string) => [
      ...currentTodos,
      // Ajoute le todo avec un ID temporaire et un flag "sending"
      { id: Date.now(), text: newTodoText, sending: true },
    ]
  );

  const handleAddTodo = async (formData: FormData) => {
    const text = formData.get('todo') as string;

    // 1. Met à jour l'UI immédiatement (optimiste)
    addOptimisticTodo(text);

    // 2. Envoie au serveur en arrière-plan
    const savedTodo = await addTodo(text);

    // 3. Met à jour l'état réel avec la réponse serveur
    setTodos(prev => [...prev, savedTodo]);
  };

  return (
    <div>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.sending ? 0.6 : 1 }}>
            {todo.text}
            {todo.sending && <span> (envoi...)
UX avec useOptimistic : L'utilisateur voit la mise à jour instantanément (0ms de latence perçue). Si le serveur retourne une erreur, React annule automatiquement la mise à jour optimiste et affiche l'état réel. C'est la même technique utilisée par WhatsApp, Twitter/X, et la plupart des apps modernes.

Le hook use() : lire promises et contextes

React 19 introduit use(), un nouveau hook polyvalent qui peut lire la valeur d'une Promise ou d'un Context. Sa particularité : il peut être appelé de façon conditionnelle (contrairement aux autres hooks).

use() avec une Promise

// app/products/page.tsx — Server Component qui crée la Promise
import { Suspense } from 'react';
import { ProductCard } from '@/components/ProductCard';

async function getProducts() {
  // Simule une requête API avec délai
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

export default function ProductsPage() {
  // Crée la promise SANS l'attendre (pas de await ici)
  const productsPromise = getProducts();

  return (
    <main>
      <h1>Nos produits</h1>
      {/* Suspense affiche le fallback pendant que la promise se résout */}
      <Suspense fallback={<p>Chargement des produits...</p>}>
        {/* Passe la promise au composant client */}
        <ProductCard productsPromise={productsPromise} />
      </Suspense>
    </main>
  );
}
// components/ProductCard.tsx — Client Component qui utilise use()
'use client';

import { use } from 'react'; // Importé depuis react

interface Product {
  id: number;
  name: string;
  price: number;
}

interface Props {
  productsPromise: Promise<Product[]>;
}

export function ProductCard({ productsPromise }: Props) {
  // use() "suspend" le composant jusqu'à ce que la promise se résolve
  // Suspense dans le parent affiche le fallback pendant ce temps
  const products = use(productsPromise);

  return (
    <div className="grid">
      {products.map(product => (
        <div key={product.id} className="card">
          <h2>{product.name}</h2>
          <p>{product.price} €</p>
        </div>
      ))}
    </div>
  );
}

use() avec un Context (utilisation conditionnelle)

// Contrairement à useContext, use() peut être appelé conditionnellement
'use client';

import { use } from 'react';
import { ThemeContext } from '@/contexts/ThemeContext';

function ThemeToggle({ showToggle }: { showToggle: boolean }) {
  // ✅ use() peut être dans un bloc conditionnel — impossible avec useContext
  if (!showToggle) {
    return null; // Sort tôt AVANT d'appeler use()
  }

  const theme = use(ThemeContext);
  return (
    <button onClick={theme.toggle}>
      Thème actuel : {theme.current}
    </button>
  );
}

Patterns avancés : composition RSC

Pattern 1 : Fetch parallèle dans les Server Components

// app/dashboard/page.tsx — Requêtes parallèles côté serveur
import { getUserProfile, getUserOrders, getUserStats } from '@/lib/api';

async function DashboardPage() {
  // Promise.all() pour paralléliser les requêtes (pas de waterfall)
  const [profile, orders, stats] = await Promise.all([
    getUserProfile(),  // Requête 1 en parallèle
    getUserOrders(),   // Requête 2 en parallèle
    getUserStats(),    // Requête 3 en parallèle
  ]);
  // Temps total = max(r1, r2, r3) au lieu de r1 + r2 + r3

  return (
    <div>
      <ProfileCard profile={profile} />
      <OrderHistory orders={orders} />
      <StatsPanel stats={stats} />
    </div>
  );
}

Pattern 2 : Streaming avec Suspense

// app/blog/page.tsx — Streaming : affiche d'abord le contenu rapide
import { Suspense } from 'react';
import { PostList, PostListSkeleton } from '@/components/PostList';
import { Sidebar, SidebarSkeleton } from '@/components/Sidebar';

export default function BlogPage() {
  // Les deux composants fetchent leurs données indépendamment
  // React les streame au navigateur dès qu'ils sont prêts
  return (
    <div style={{ display: 'flex' }}>
      {/* Sidebar peut s'afficher avant PostList si elle charge plus vite */}
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar /> {/* Async Server Component */}
      </Suspense>

      {/* PostList charge souvent plus de données — son skeleton reste visible */}
      <Suspense fallback={<PostListSkeleton />}>
        <PostList /> {/* Async Server Component */}
      </Suspense>
    </div>
  );
}

Pattern 3 : Server Action avec validation Zod

// app/profile/actions.ts — Server Action robuste avec validation
'use server';

import { z } from 'zod'; // Validation de schéma TypeScript-first
import { db } from '@/lib/database';

// Schéma de validation Zod
const UpdateProfileSchema = z.object({
  name:     z.string().min(2).max(50),
  bio:      z.string().max(500).optional(),
  website:  z.string().url().optional().or(z.literal('')),
});

export async function updateProfile(formData: FormData) {
  const rawData = {
    name:    formData.get('name'),
    bio:     formData.get('bio'),
    website: formData.get('website'),
  };

  // Validation avec Zod — retourne les erreurs structurées
  const parsed = UpdateProfileSchema.safeParse(rawData);

  if (!parsed.success) {
    // Retourne les erreurs au format {field: message}
    return {
      errors: parsed.error.flatten().fieldErrors,
    };
  }

  // Données validées et typées — safe to use
  await db.query('UPDATE users SET name = ?, bio = ?, website = ?', [
    parsed.data.name,
    parsed.data.bio ?? null,
    parsed.data.website ?? null,
  ]);

  return { success: true };
}

Adopter React 19 avec Next.js 14+

Next.js est le chemin le plus direct pour utiliser React 19 en production. Le App Router (introduit en Next.js 13) est conçu spécifiquement pour les Server Components et Server Actions.

Créer un projet Next.js avec App Router

# Créer un nouveau projet Next.js 14 (inclut React 19)
npx create-next-app@latest mon-projet --typescript --app

# Structure générée
# mon-projet/
# ├── app/
# │   ├── layout.tsx      ← Layout global (Server Component)
# │   ├── page.tsx        ← Page d'accueil (Server Component)
# │   └── globals.css
# ├── components/         ← Composants réutilisables
# └── lib/                ← Utilitaires, BDD, services

Migrer depuis Pages Router vers App Router

// AVANT (Pages Router — Next.js <13)
// pages/products.tsx
export async function getServerSideProps() {
  // Fetch côté serveur avec getServerSideProps
  const products = await fetchProducts();
  return { props: { products } };
}

export default function ProductsPage({ products }) {
  // Données disponibles via props
  return <ProductList products={products} />;
}
// APRÈS (App Router — Next.js 14+ avec React 19)
// app/products/page.tsx — Server Component async
async function ProductsPage() {
  // Fetch direct dans le composant — plus besoin de getServerSideProps
  const products = await fetchProducts();
  return <ProductList products={products} />;
}

export default ProductsPage;
// Beaucoup plus simple et naturel !

Mettre à jour React en projet existant

# Mettre à jour React vers la version 19
npm install react@19 react-dom@19

# Types TypeScript (si projet TS)
npm install --save-dev @types/react@19 @types/react-dom@19

# Vérifier les breaking changes avec le codemod officiel
npx codemod@latest react/19/migration-recipe
Breaking changes React 19 : Les principales ruptures concernent la suppression de certains comportements dépréciés (defaultProps pour les fonctions, string refs, ReactDOM.render). Le codemod officiel corrige automatiquement la majorité des cas. Consultez le guide de migration officiel avant de mettre à jour un projet de production.

Checklist React 19

Comprendre les fondamentaux

  • Distinguer Server Components et Client Components
  • Savoir quand ajouter 'use client' (hooks, événements, APIs navigateur)
  • Comprendre que les Server Components ne s'envoient pas au navigateur
  • Utiliser async/await directement dans les Server Components

Server Actions

  • Créer des Server Actions avec 'use server'
  • Lier une Server Action à un formulaire via action={}
  • Valider les données côté serveur (Zod recommandé)
  • Utiliser useFormStatus pour les états de chargement
  • Implémenter useOptimistic pour les mises à jour instantanées

Performance et patterns

  • Paralléliser les requêtes avec Promise.all()
  • Utiliser Suspense pour streamer le contenu progressivement
  • Réserver 'use client' aux feuilles de l'arbre de composants
  • Utiliser use() pour lire des promises passées en props
  • Invalider le cache avec revalidatePath() après les mutations
Conseils pour commencer : Ne migrez pas tout d'un coup. Commencez par un nouveau projet Next.js avec App Router, apprenez les patterns RSC, puis adoptez les Server Actions. L'apprentissage progressif est plus efficace que de vouloir tout migrer d'un coup.

Récapitulatif des nouveautés React 19

Fonctionnalité API Cas d'usage Framework requis
Server Components async function Component() Fetch BDD, secrets, bundle réduit Next.js / Remix
Server Actions 'use server' + action={} Formulaires, mutations sans API Next.js / Remix
useFormStatus const { pending } = useFormStatus() État de soumission du formulaire Non (react-dom)
useOptimistic const [opt, add] = useOptimistic(state, fn) UI optimiste avant confirmation serveur Non
use() const data = use(promise) Lire promises / contextes conditionnels Non

React 19 marque l'entrée de React dans l'ère du rendu hybride. La séparation claire entre Server Components et Client Components oblige à réfléchir différemment à l'architecture des applications, mais ouvre des possibilités inédites : moins de JavaScript au client, des accès BDD directs, et des formulaires qui fonctionnent sans JavaScript. Ce n'est pas une révolution à maîtriser en un weekend, mais un investissement qui positionne les développeurs au cœur de la direction que prend l'écosystème React.

Partager