Accueil / Articles / Où tracer la ligne serveur-client dans une page du Next.js App Router

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.

1321 mots

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.
  • Composant serveur : s’exécute sur le serveur, peut lire des données directement depuis des bases de données ou des fichiers, et n’envoie en retour que le contenu rendu.
  • 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 que window et localStorage.
    • Les composants serveur peuvent importer et afficher des composants client, comme c’est le cas pour CommentSection sur la page.
  • Les composants client ne peuvent pas importer de composants serveur, mais ils peuvent les recevoir en tant que 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.
  • Les propriétés transmises du serveur au client doivent être serialisables : les données simples fonctionnent, mais les fonctions et les instances de classe ne le font pas.
  • Le stylisme n’est pas affecté. Les classes Tailwind et le CSS fonctionnent de la même manière dans les deux types de composants.
  • "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

  • Migrer de next/router : Params, URLs shallow et 404s dans App Router — Découvrez comment les params, useParams, les mises à jour d’URL shallow, la navigation programmée et les pages 404 fonctionnent dans le Next.js App Router une fois next/router supprimé.
  • SEO technique dans Next.js App Router : Metadata, sitemaps et JSON-LD — Apprenez comment les outils d’aide aux metadata partagés, les paramètres par défaut du layout racine, robots.ts, un sitemap dynamique, des JSON-LD fiables et les audits de pages permettent à une application Next.js d’avoir une base SEO solide.
  • Pourquoi les WebSockets bloquent dans les routes API de Next.js et comment un server personnalisé le résout — Découvrez pourquoi un server WS à l’intérieur d’une route pages/api ne termine jamais la mise en relation, et comment gérer vous-même l’événement upgrade avec un server Next.js personnalisé.