Next.js : composants serveur et client
Dans l’App Router, les composants sont des composants serveur par défaut : ils s’exécutent sur le serveur, n’envoient pas leur code au navigateur et peuvent lire directement une base de données ou le système de fichiers.
Serveur ou client ?
Section intitulée « Serveur ou client ? »| Composant serveur (défaut) | Composant client ('use client') |
|
|---|---|---|
| Exécution | Serveur (à la construction ou à la requête) | Serveur (rendu initial) puis navigateur |
| Accès aux données, secrets, base de données | Oui | Non |
useState, useEffect, événements (onClick) |
Non | Oui |
APIs du navigateur (window, localStorage) |
Non | Oui |
| JavaScript envoyé au navigateur | Non | Oui |
Règle : rester côté serveur et ne passer côté client que les parties interactives.
Un composant serveur qui lit des données
Section intitulée « Un composant serveur qui lit des données »// app/clients/page.tsx (composant serveur)import { listerClients } from '@/lib/clients';
export default async function Clients() { const clients = await listerClients(); // accès direct à la base ou à une API return ( <ul> {clients.map((c) => ( <li key={c.id}>{c.nom}</li> ))} </ul> );}Le composant est async : il peut await directement. Le code d’accès aux données (et ses secrets) ne part jamais au navigateur.
Un composant client
Section intitulée « Un composant client »'use client';
import { useState } from 'react';
export function Recherche({ onChange }: { onChange?: (q: string) => void }) { const [q, setQ] = useState(''); return ( <input value={q} onChange={(e) => { setQ(e.target.value); onChange?.(e.target.value); }} placeholder="Rechercher" /> );}'use client' en première ligne du fichier déclare la frontière : ce fichier et tout ce qu’il importe deviennent des composants client.
Combiner les deux
Section intitulée « Combiner les deux »Un composant serveur peut importer un composant client, et lui passer des enfants (qui restent des composants serveur) :
// app/page.tsx (serveur)import { Panneau } from './Panneau'; // composant clientimport { Statistiques } from './Statistiques'; // composant serveur
export default function Accueil() { return ( <Panneau titre="Tableau de bord"> <Statistiques /> </Panneau> );}'use client';import { useState, type ReactNode } from 'react';
export function Panneau({ titre, children,}: { titre: string; children: ReactNode;}) { const [ouvert, setOuvert] = useState(true); return ( <section> <button onClick={() => setOuvert(!ouvert)}>{titre}</button> {ouvert && children} </section> );}Un composant client ne peut pas importer un composant serveur : il le reçoit en children ou en prop.
Les données passées du serveur au client doivent être sérialisables (objets simples, pas de fonctions ni de classes).
Chargement progressif : loading.tsx et Suspense
Section intitulée « Chargement progressif : loading.tsx et Suspense »import { Suspense } from 'react';
export default function Tableau() { return ( <> <h1>Tableau de bord</h1> <Suspense fallback={<p>Chargement des statistiques…</p>}> <Statistiques /> </Suspense> </> );}La page s’affiche immédiatement et la partie lente arrive ensuite (streaming). Un fichier loading.tsx dans un dossier fait la même chose pour toute la page.
Server Actions
Section intitulée « Server Actions »Une Server Action est une fonction exécutée sur le serveur, appelée depuis un formulaire ou un composant client, sans écrire de route d’API :
'use server';
import { revalidatePath } from 'next/cache';import { z } from 'zod';
const schema = z.object({ nom: z.string().min(1).max(100), email: z.string().email(),});
export async function creerClient( _etat: { message: string }, donnees: FormData,) { const resultat = schema.safeParse({ nom: donnees.get('nom'), email: donnees.get('email'), }); if (!resultat.success) return { message: 'Données invalides' };
await insererClient(resultat.data); // accès à la base revalidatePath('/clients'); // met à jour la page return { message: 'Client enregistré' };}'use client';import { useActionState } from 'react';import { creerClient } from './actions';
export function Formulaire() { const [etat, action, enCours] = useActionState(creerClient, { message: '' }); return ( <form action={action}> <input name="nom" required /> <input name="email" type="email" required /> <button disabled={enCours}>Ajouter</button> <p role="status">{etat.message}</p> </form> );}Cookies et en-têtes
Section intitulée « Cookies et en-têtes »import { cookies, headers } from 'next/headers';
export default async function Page() { const jar = await cookies(); const theme = jar.get('theme')?.value ?? 'light'; const agent = (await headers()).get('user-agent'); return ( <p> {theme} / {agent} </p> );}Depuis Next.js 15, cookies() et headers() sont asynchrones. Les lire rend la page dynamique.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
You're importing a component that needs useState… |
Ajouter 'use client' |
Functions cannot be passed directly to Client Components |
Fonction passée en prop à un composant client : utiliser une Server Action ou la définir côté client |
Hydration failed because the initial UI does not match |
Rendu serveur et client différents (date, Math.random, window) : déplacer dans un useEffect |
window is not defined |
Code navigateur dans un composant serveur ou rendu initial : 'use client' + useEffect |
Only plain objects can be passed to Client Components |
Donnée non sérialisable (classe, Date selon le cas) |
| La page ne se met pas à jour après une action | revalidatePath / revalidateTag manquant |
cookies() renvoie une promesse |
Depuis Next.js 15 : await cookies() |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune.