Aller au contenu

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.

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.

// 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.

app/clients/Recherche.tsx
'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.

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 client
import { Statistiques } from './Statistiques'; // composant serveur
export default function Accueil() {
return (
<Panneau titre="Tableau de bord">
<Statistiques />
</Panneau>
);
}
Panneau.tsx
'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).

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.

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 :

app/clients/actions.ts
'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é' };
}
app/clients/Formulaire.tsx
'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>
);
}
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.

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.