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 |
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
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>
);
}
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>
);
}
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...)
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
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
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.