Où tracer la ligne serveur-client dans une page du Next.js App Router
Un modèle mental pratique pour les composants serveur React : ce qui s’exécute où, comment une page de blog se divise en parties serveur et client, ainsi que les règles d’importation à respecter.
Les composants serveur React peuvent sembler déroutants même après avoir lu la documentation, principalement parce qu’ils vous demandent d’abandonner une hypothèse que tout développeur React partage depuis des années : à savoir que les composants s’exécutent dans le navigateur. Une fois cette hypothèse éliminée, le reste suit assez naturellement. Cet article construit un modèle mental autour d’une seule page de blog, en montrant quelles parties doivent rester sur le serveur, lesquelles ont besoin du client, ainsi que les quelques règles qui maintiennent une frontière claire entre eux.
Pour en savoir plus sur le pipeline de rendu lui-même, consultez l’architecture derrière le rendu sans bundle avec les composants serveur.
De « tout se hydrate » à « seulement ce qui le faut »
Au préalable des composants serveur, chaque composant React s’exécutait finalement dans le navigateur. Même avec le rendu du côté serveur, le JavaScript correspondant à l’ensemble de la structure était envoyé au client et hydraté afin que l’application devienne interactive. Cela est gaspilleur pour les composants qui se contentent de récupérer des données et de les transmettre sous forme de props : leur code est téléchargé et exécuté sans jamais réagir à l’utilisateur.
Un composant serveur change cela en ne s’exécutant que sur le serveur. Son code n’est jamais envoyé au navigateur et n’est jamais hydraté, de sorte qu’il ne ajoute aucun JavaScript au bundle client. Ce qui parvient au navigateur, c’est sa sortie rendue, que React serialise en un payload compact aux côtés de l’HTML.
Le moyen le plus simple pour distinguer ces deux types dans son esprit :
- Composant client : s’exécute dans le navigateur (après avoir été pré-renderisé sur le serveur), peut gérer un état et réagir aux événements.
Pourquoi cette distinction est avantageuse
Prenons une page typique d’article de blog. Le titre et le corps proviennent d’une base de données, ont l’air identiques pour tous les visiteurs et ignorent les clics. Traditionnellement, le code qui les rend, ainsi que toutes les bibliothèques Markdown ou de mise en forme, sont encore envoyés au navigateur. Avec les composants serveurs, tout cela reste sur le serveur. Le fichier compressé devient plus petit et les pages se chargent généralement plus rapidement, surtout sur des appareils moins puissants.
Les équipes qui passent à l’App Router, conçu autour des composants serveurs, ont constaté de meilleurs scores Core Web Vitals, en particulier pour LCP, dans certains cas. Considérez cela comme dépendant du contexte : l’avantage dépend de la quantité de JavaScript côté client que vos pages éliminent, il faut donc mesurer ses propres routes.
Une page d’article de blog, divisée en deux
Dans un projet Next.js App Router, un fichier de page est un composant serveur à moins qu’il ne soit indiqué autrement. La page ci-dessous est une fonction async qui attend les données directement depuis la couche de données, gère le cas de page introuvable, affiche le contenu statique, puis insère un seul élément interactif pour les commentaires. Elle a été écrite en utilisant l’API de Next.js 14 :
// app/posts/[slug]/page.tsx
// This is a Server Component by default — no "use client" needed
import { getPostBySlug } from "@/lib/db"
import { PostContent } from "@/components/PostContent"
import { CommentSection } from "@/components/CommentSection"
type Props = {
params: { slug: string }
}
export default async function PostPage({ params }: Props) {
// Direct DB call — no useEffect, no API route, no loading state
const post = await getPostBySlug(params.slug)
if (!post) {
return <div>Post not found.</div>
}
return (
<article className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<PostContent content={post.body} />
{/* This one needs interactivity — so it's a Client Component */}
<CommentSection postId={post.id} />
</article>
)
}
Remarquez ce qui est absent : pas de useState, pas de useEffect, pas d’enveloppe pour la route API et pas de suivi de l’état de chargement. Les données sont attendues exactement comme dans n’importe quelle autre fonction async, ce qui constitue le cœur du modèle.
Deux remarques pratiques. À partir de Next.js 15, params est transmis sous forme de Promise, ce qui oblige la page à await params avant de lire slug ; vérifiez la version que vous utilisez. De plus, en cas de véritable page introuvable, l’appel à notFound() depuis next/navigation renvoie un statut 404 approprié plutôt qu’une page normale affichant un message d’erreur.
La boîte de commentaires est différente. Elle conserve le texte en brouillon dans l’état et réagit aux saisies ainsi qu’aux clics, ce qui nécessite son exécution dans le navigateur. La directive "use client" en haut du fichier la désigne comme un composant Client :
// components/CommentSection.tsx
"use client" // opts into browser rendering
import { useState } from "react"
type Props = {
postId: string
}
export function CommentSection({ postId }: Props) {
const [comment, setComment] = useState("")
const handleSubmit = async () => {
await fetch("/api/comments", {
method: "POST",
body: JSON.stringify({ postId, comment }),
})
setComment("")
}
return (
<div className="mt-8">
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Leave a comment..."
className="w-full border rounded p-2 text-sm"
/>
<button
onClick={handleSubmit}
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded text-sm"
>
Post Comment
</button>
</div>
)
}
Tout ce qui est interactif se trouve ici : l’état local du champ de texte, un gestionnaire de modification et un gestionnaire d’envoi qui envoie les données vers une route API avant de vider le champ. Lorsque vous mettez cela en œuvre réellement, envoyez une en-tête Content-Type: application/json avec la requête, et envisagez une action serveur comme alternative à une route API distincte.
Cette séparation permet une compréhension simple : le serveur gère les données, tandis que le client gère l’interaction.
Règles pour maintenir des frontières claires
- Dans le App Router, les composants sont des composants serveur par défaut.
- Ajoutez
"use client"uniquement lorsque vous avez besoin d’état, d’effets, de gestionnaires d’événements ou d’API du navigateur telles quewindowetlocalStorage. - Les composants serveur peuvent importer et afficher des composants client, comme c’est le cas pour
CommentSectionsur la page.
enfants ou d’autres propriétés, ce qui vous permet de nester du contenu rendu côté serveur à l’intérieur d’une coquille interactive."use client" marque également une frontière plutôt qu’un seul fichier : tout ce que ce fichier importe fait partie du bundle client. En plaçant cette directive aussi bas que possible dans la hiérarchie, sur les petites feuilles interactives, on garde le reste côté serveur.
En résumé
Le modèle mental s’éclaire dès que la première question posée à propos d’un composant devient « où cela s’exécute-t-il ? », et la réponse par défaut du App Router est le serveur.
- Les composants serveur déplacent le rendu et l’accès aux données sur le serveur et ne livrent aucun JavaScript de composant.
- La récupération des données devient une simple opération
async/awaità l’intérieur du composant. - Considérez
"use client"comme une option pour les éléments interactifs, et non comme la valeur par défaut. - Premier pas pratique : choisissez un composant de récupération de données dans un projet existant, demandez-vous s’il a réellement besoin du navigateur, et convertissez-le le cas échéant.
Avec ce modèle en place, les maquettes, le streaming Suspense, les actions serveur et les routes parallèles deviennent bien plus faciles à apprendre, car chacun s’appuie sur la même base axée sur le serveur.
Lectures complémentaires
- Construire une page d’information sur un film résiliente avec Next.js App Router — Apprenez à récupérer et à mettre en cache correctement les données de l’API OMDB dans Next.js App Router en utilisant des composants serveur asynchrones, des paramètres attendus et un traitement adéquat des erreurs 404.
- Actions serveur ou gestionnaires de route ? Un guide de décision pour Next.js 16 — Découvrez quand une mutation dans Next.js 16 doit être placée dans une action serveur et quand elle nécessite un gestionnaire de route, avec des exemples corrigés pour les formulaires, les erreurs, l’optimisme et les webhooks.