Comment utiliser les composants Serveur et Client

Par défaut, les layouts et les pages sont des composants Serveur (Server Components), ce qui vous permet de récupérer des données et d'afficher des parties de votre interface côté serveur, de mettre en cache le résultat si nécessaire, et de le transmettre au client. Lorsque vous avez besoin d'interactivité ou d'API navigateur, vous pouvez utiliser des composants Client (Client Components) pour ajouter des fonctionnalités.

Cette page explique comment les composants Serveur et Client fonctionnent dans Next.js et quand les utiliser, avec des exemples de leur combinaison dans votre application.

Quand utiliser les composants Serveur et Client ?

Les environnements client et serveur ont des capacités différentes. Les composants Serveur et Client vous permettent d'exécuter une logique dans chaque environnement selon votre cas d'utilisation.

Utilisez des composants Client lorsque vous avez besoin :

Utilisez des composants Serveur lorsque vous avez besoin :

  • De récupérer des données depuis des bases de données ou des API proches de la source.
  • D'utiliser des clés API, des tokens et d'autres secrets sans les exposer au client.
  • De réduire la quantité de JavaScript envoyée au navigateur.
  • D'améliorer le First Contentful Paint (FCP), et de transmettre progressivement le contenu au client.

Par exemple, le composant <Page> est un composant Serveur qui récupère des données sur un post, et les transmet comme props au composant <LikeButton> qui gère l'interactivité côté client.

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}

Comment fonctionnent les composants Serveur et Client dans Next.js ?

Côté serveur

Côté serveur, Next.js utilise les API de React pour orchestrer le rendu. Le travail de rendu est divisé en segments, par route individuelle (layouts et pages) :

  • Les composants Serveur sont rendus dans un format spécial appelé React Server Component Payload (RSC Payload).
  • Les composants Client et le RSC Payload sont utilisés pour prerendre (prerender) le HTML.

Qu'est-ce que le React Server Component Payload (RSC) ?

Le RSC Payload est une représentation binaire compacte de l'arbre des composants Serveur rendus. Il est utilisé par React côté client pour mettre à jour le DOM du navigateur. Le RSC Payload contient :

  • Le résultat rendu des composants Serveur
  • Des emplacements pour les composants Client qui doivent être rendus et des références à leurs fichiers JavaScript
  • Toutes les props passées d'un composant Serveur à un composant Client

Côté client (premier chargement)

Ensuite, côté client :

  1. Le HTML est utilisé pour afficher immédiatement un aperçu rapide et non interactif de la route à l'utilisateur.
  2. Le RSC Payload est utilisé pour réconcilier les arbres des composants Client et Serveur.
  3. Le JavaScript est utilisé pour hydrater (hydrate) les composants Client et rendre l'application interactive.

Qu'est-ce que l'hydratation ?

L'hydratation est le processus de React pour attacher des gestionnaires d'événements (event handlers) au DOM, afin de rendre le HTML statique interactif.

Lors des navigations suivantes :

  • Le RSC Payload est préchargé et mis en cache pour une navigation instantanée.
  • Les composants Client sont entièrement rendus côté client, sans le HTML rendu côté serveur.

Exemples

Utilisation de composants Client

Vous pouvez créer un composant Client en ajoutant la directive "use client" en haut du fichier, avant vos imports.

'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

"use client" est utilisé pour déclarer une frontière entre les graphes (arbres) de modules Serveur et Client.

Une fois qu'un fichier est marqué avec "use client", tous ses imports et composants enfants sont considérés comme faisant partie du bundle client. Cela signifie que vous n'avez pas besoin d'ajouter la directive à chaque composant destiné au client.

Réduction de la taille du bundle JS

Pour réduire la taille de vos bundles JavaScript côté client, ajoutez 'use client' à des composants interactifs spécifiques au lieu de marquer de grandes parties de votre interface comme composants Client.

Par exemple, le composant <Layout> contient principalement des éléments statiques comme un logo et des liens de navigation, mais inclut une barre de recherche interactive. <Search /> est interactive et doit être un composant Client, cependant, le reste du layout peut rester un composant Serveur.

'use client'

export default function Search() {
  // ...
}

Passage de données des composants Serveur aux composants Client

Vous pouvez passer des données des composants Serveur aux composants Client en utilisant des props.

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return <LikeButton likes={post.likes} />
}

Alternativement, vous pouvez diffuser (stream) des données d'un composant Serveur vers un composant Client avec le Hook use. Voir un exemple.

Bon à savoir : Les props passées aux composants Client doivent être sérialisables par React.

Entrelacement des composants Serveur et Client

Vous pouvez passer des composants Serveur comme prop à un composant Client. Cela vous permet d'imbriquer visuellement une interface rendue côté serveur dans des composants Client.

Un motif courant consiste à utiliser children pour créer un slot dans un <ClientComponent>. Par exemple, un composant <Cart> qui récupère des données côté serveur, à l'intérieur d'un composant <Modal> qui utilise un état client pour basculer la visibilité.

'use client'

export default function Modal({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

Ensuite, dans un composant Serveur parent (par exemple <Page>), vous pouvez passer un <Cart> comme enfant du <Modal> :

import Modal from './ui/modal'
import Cart from './ui/cart'

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

Dans ce motif, tous les composants Serveur seront rendus côté serveur à l'avance, y compris ceux passés comme props. Le RSC Payload résultant contiendra des références indiquant où les composants Client doivent être rendus dans l'arbre des composants.

Fournisseurs de contexte (Context providers)

Le contexte React est couramment utilisé pour partager un état global comme le thème actuel. Cependant, le contexte React n'est pas pris en charge dans les composants Serveur.

Pour utiliser un contexte, créez un composant Client qui accepte children :

'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

Ensuite, importez-le dans un composant Serveur (par exemple layout) :

import ThemeProvider from './theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Votre composant Serveur pourra maintenant rendre directement votre fournisseur, et tous les autres composants Client dans votre application pourront consommer ce contexte.

Bon à savoir : Vous devriez rendre les fournisseurs aussi profondément que possible dans l'arbre – notez comment ThemeProvider n'englobe que {children} au lieu de tout le document <html>. Cela facilite l'optimisation par Next.js des parties statiques de vos composants Serveur.

Composants tiers

Lorsque vous utilisez un composant tiers qui dépend de fonctionnalités réservées au client, vous pouvez l'encapsuler dans un composant Client pour vous assurer qu'il fonctionne comme prévu.

Par exemple, le composant <Carousel /> peut être importé depuis le package acme-carousel. Ce composant utilise useState, mais il n'a pas encore la directive "use client".

Si vous utilisez <Carousel /> dans un composant Client, il fonctionnera comme prévu :

'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>
      {/* Fonctionne, car Carousel est utilisé dans un composant Client */}
      {isOpen && <Carousel />}
    </div>
  )
}

Cependant, si vous essayez de l'utiliser directement dans un composant Serveur, vous verrez une erreur. C'est parce que Next.js ne sait pas que <Carousel /> utilise des fonctionnalités réservées au client.

Pour résoudre ce problème, vous pouvez encapsuler les composants tiers qui dépendent de fonctionnalités réservées au client dans vos propres composants Client :

'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

Maintenant, vous pouvez utiliser <Carousel /> directement dans un composant Serveur :

import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>View pictures</p>
      {/* Fonctionne, car Carousel est un composant Client */}
      <Carousel />
    </div>
  )
}

Conseil pour les auteurs de bibliothèques

Si vous créez une bibliothèque de composants, ajoutez la directive "use client" aux points d'entrée qui dépendent de fonctionnalités réservées au client. Cela permet à vos utilisateurs d'importer des composants dans des composants Serveur sans avoir besoin de créer des wrappers.

Il est à noter que certains bundlers peuvent supprimer les directives "use client". Vous pouvez trouver un exemple de configuration d'esbuild pour inclure la directive "use client" dans les dépôts React Wrap Balancer et Vercel Analytics.

Prévenir l'intoxication de l'environnement

Les modules JavaScript peuvent être partagés entre les modules des composants Serveur et Client. Cela signifie qu'il est possible d'importer accidentellement du code réservé au serveur dans le client. Par exemple, considérez la fonction suivante :

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Cette fonction contient une API_KEY qui ne devrait jamais être exposée au client.

Dans Next.js, seules les variables d'environnement préfixées par NEXT_PUBLIC_ sont incluses dans le bundle client. Si les variables ne sont pas préfixées, Next.js les remplace par une chaîne vide.

Par conséquent, même si getData() peut être importée et exécutée côté client, elle ne fonctionnera pas comme prévu.

Pour éviter une utilisation accidentelle dans les composants Client, vous pouvez utiliser le package server-only.

Terminal
npm install server-only

Ensuite, importez le package dans un fichier contenant du code réservé au serveur :

lib/data.js
import 'server-only'

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Désormais, si vous essayez d'importer ce module dans un composant Client, une erreur sera générée lors de la compilation.

Bon à savoir : Le package correspondant client-only peut être utilisé pour marquer les modules contenant une logique réservée au client, comme du code accédant à l'objet window.