Modèles de composition pour les composants Serveur et Client

Lors de la construction d'applications React, vous devrez déterminer quelles parties de votre application doivent être rendues côté serveur ou côté client. Cette page présente des modèles de composition recommandés lors de l'utilisation des composants Serveur et Client.

Quand utiliser les composants Serveur et Client ?

Voici un résumé rapide des différents cas d'utilisation pour les composants Serveur et Client :

Quel est votre besoin ?Composant ServeurComposant Client
Récupérer des donnéesCheck IconCross Icon
Accéder aux ressources backend (directement)Check IconCross Icon
Garder des informations sensibles sur le serveur (jetons d'accès, clés API, etc)Check IconCross Icon
Garder des dépendances volumineuses sur le serveur / Réduire le JavaScript côté clientCheck IconCross Icon
Ajouter de l'interactivité et des écouteurs d'événements (onClick(), onChange(), etc)Cross IconCheck Icon
Utiliser l'état et les effets de cycle de vie (useState(), useReducer(), useEffect(), etc)Cross IconCheck Icon
Utiliser des APIs spécifiques au navigateurCross IconCheck Icon
Utiliser des hooks personnalisés dépendant de l'état, des effets ou des APIs navigateurCross IconCheck Icon
Utiliser des composants React ClassCross IconCheck Icon

Modèles pour les composants Serveur

Avant d'opter pour le rendu côté client, vous pouvez effectuer certaines opérations côté serveur comme la récupération de données ou l'accès à votre base de données ou services backend.

Voici quelques modèles courants lors de l'utilisation des composants Serveur :

Partage de données entre composants

Lors de la récupération de données côté serveur, il peut arriver que vous ayez besoin de partager des données entre différents composants. Par exemple, vous pourriez avoir une mise en page (layout) et une page qui dépendent des mêmes données.

Au lieu d'utiliser React Context (qui n'est pas disponible côté serveur) ou de passer les données via des props, vous pouvez utiliser fetch ou la fonction cache de React pour récupérer les mêmes données dans les composants qui en ont besoin, sans vous soucier des requêtes en double pour les mêmes données. Cela est possible car React étend fetch pour mémoïser automatiquement les requêtes de données, et la fonction cache peut être utilisée lorsque fetch n'est pas disponible.

En savoir plus sur la mémoïsation dans React.

Garder le code réservé au serveur hors de l'environnement client

Comme les modules JavaScript peuvent être partagés entre les composants Serveur et Client, il est possible que du code destiné uniquement au serveur se retrouve côté client.

Par exemple, prenons la fonction suivante de récupération de données :

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

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

  return res.json()
}

À première vue, il semble que getData fonctionne à la fois côté serveur et côté client. Cependant, cette fonction contient une API_KEY, écrite avec l'intention qu'elle ne soit exécutée que côté serveur.

Comme la variable d'environnement API_KEY n'est pas préfixée par NEXT_PUBLIC, c'est une variable privée qui ne peut être accédée que côté serveur. Pour éviter que vos variables d'environnement ne fuient vers le client, Next.js remplace les variables d'environnement privées par une chaîne vide.

Par conséquent, bien que getData() puisse être importée et exécutée côté client, elle ne fonctionnera pas comme prévu. Et bien que rendre la variable publique permettrait à la fonction de fonctionner côté client, vous ne voudrez peut-être pas exposer des informations sensibles au client.

Pour éviter ce type d'utilisation involontaire de code serveur côté client, vous pouvez utiliser le package server-only pour générer une erreur au moment de la construction si quelqu'un importe accidentellement un de ces modules dans un composant Client.

Pour utiliser server-only, installez d'abord le package :

Terminal
npm install server-only

Puis importez le package dans tout module 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()
}

Maintenant, tout composant Client qui importera getData() recevra une erreur au moment de la construction indiquant que ce module ne peut être utilisé que côté serveur.

Le package correspondant client-only peut être utilisé pour marquer les modules contenant du code réservé au client - par exemple, du code qui accède à l'objet window.

Utilisation de packages et fournisseurs tiers

Comme les composants Serveur sont une nouvelle fonctionnalité de React, les packages tiers et les fournisseurs de l'écosystème commencent tout juste à ajouter la directive "use client" aux composants qui utilisent des fonctionnalités réservées au client comme useState, useEffect et createContext.

Aujourd'hui, de nombreux composants de packages npm utilisant des fonctionnalités réservées au client n'ont pas encore cette directive. Ces composants tiers fonctionneront comme prévu dans les composants Client puisqu'ils ont la directive "use client", mais ils ne fonctionneront pas dans les composants Serveur.

Par exemple, supposons que vous ayez installé le package hypothétique acme-carousel qui contient un composant <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() {
  let [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Voir les images</button>

      {/* Fonctionne, car Carousel est utilisé dans un composant Client */}
      {isOpen && <Carousel />}
    </div>
  )
}
'use client'

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

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

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Voir les images</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 :

import { Carousel } from 'acme-carousel'

export default function Page() {
  return (
    <div>
      <p>Voir les images</p>

      {/* Erreur : `useState` ne peut pas être utilisé dans les composants Serveur */}
      <Carousel />
    </div>
  )
}
import { Carousel } from 'acme-carousel'

export default function Page() {
  return (
    <div>
      <p>Voir les images</p>

      {/* Erreur : `useState` ne peut pas être utilisé dans les composants Serveur */}
      <Carousel />
    </div>
  )
}

Cela est dû au fait 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
'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>Voir les images</p>

      {/* Fonctionne, car Carousel est un composant Client */}
      <Carousel />
    </div>
  )
}
import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>Voir les images</p>

      {/* Fonctionne, car Carousel est un composant Client */}
      <Carousel />
    </div>
  )
}

Nous ne nous attendons pas à ce que vous ayez besoin d'encapsuler la plupart des composants tiers, car il est probable que vous les utiliserez dans des composants Client. Cependant, une exception concerne les fournisseurs (providers), car ils dépendent de l'état et du contexte React, et sont généralement nécessaires à la racine d'une application. En savoir plus sur les fournisseurs de contexte tiers ci-dessous.

Utilisation des fournisseurs de contexte

Les fournisseurs de contexte sont généralement rendus près de la racine d'une application pour partager des préoccupations globales, comme le thème actuel. Comme React context n'est pas supporté dans les composants Serveur, essayer de créer un contexte à la racine de votre application générera une erreur :

import { createContext } from 'react'

// createContext n'est pas supporté dans les composants Serveur
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}
import { createContext } from 'react'

// createContext n'est pas supporté dans les composants Serveur
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}

Pour résoudre ce problème, créez votre contexte et rendez son fournisseur dans un composant Client :

'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>
}
'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

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

Votre composant Serveur pourra maintenant rendre directement votre fournisseur puisqu'il a été marqué comme composant Client :

import ThemeProvider from './theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}
import ThemeProvider from './theme-provider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Avec le fournisseur rendu à la racine, tous les autres composants Client de votre application pourront consommer ce contexte.

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

Conseils pour les auteurs de bibliothèques

De manière similaire, les auteurs de bibliothèques créant des packages destinés à être utilisés par d'autres développeurs peuvent utiliser la directive "use client" pour marquer les points d'entrée client de leur package. Cela permet aux utilisateurs du package d'importer des composants directement dans leurs composants Serveur sans avoir à créer une limite d'encapsulation.

Vous pouvez optimiser votre package en utilisant 'use client' plus profondément dans l'arborescence, permettant aux modules importés de faire partie du graphe de modules du composant Serveur.

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.

Composants Client

Descendre les composants Client dans l'arborescence

Pour réduire la taille du bundle JavaScript côté client, nous recommandons de descendre les composants Client dans votre arborescence de composants.

Par exemple, vous pourriez avoir une mise en page (Layout) avec des éléments statiques (logo, liens, etc) et une barre de recherche interactive utilisant l'état.

Au lieu de rendre toute la mise en page comme composant Client, déplacez la logique interactive dans un composant Client (par exemple <SearchBar />) et gardez votre mise en page comme composant Serveur. Cela signifie que vous n'aurez pas à envoyer tout le JavaScript du composant de mise en page au client.

// SearchBar est un composant Client
import SearchBar from './searchbar'
// Logo est un composant Serveur
import Logo from './logo'

// Layout est un composant Serveur par défaut
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}
// SearchBar est un composant Client
import SearchBar from './searchbar'
// Logo est un composant Serveur
import Logo from './logo'

// Layout est un composant Serveur par défaut
export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

Passage de props des composants Serveur aux composants Client (Sérialisation)

Si vous récupérez des données dans un composant Serveur, vous pourriez vouloir les passer comme props à des composants Client. Les props passées d'un composant Serveur à un composant Client doivent être sérialisables par React.

Si vos composants Client dépendent de données non sérialisables, vous pouvez récupérer les données côté client avec une bibliothèque tierce ou côté serveur via un Route Handler.

Entrelacement des composants serveur et client

Lorsque vous entrelacez des composants client et serveur, il peut être utile de visualiser votre interface utilisateur comme une arborescence de composants. En commençant par la disposition racine, qui est un composant serveur, vous pouvez ensuite rendre certaines sous-arbres de composants côté client en ajoutant la directive "use client".

Dans ces sous-arbres clients, vous pouvez toujours imbriquer des composants serveur ou appeler des actions serveur, mais il y a quelques éléments à garder à l'esprit :

  • Durant le cycle de vie d'une requête-réponse, votre code passe du serveur au client. Si vous avez besoin d'accéder à des données ou ressources sur le serveur depuis le client, vous ferez une nouvelle requête vers le serveur - vous ne basculerez pas d'avant en arrière.
  • Lorsqu'une nouvelle requête est faite vers le serveur, tous les composants serveur sont rendus en premier, y compris ceux imbriqués dans des composants clients. Le résultat rendu (charge utile RSC) contiendra des références aux emplacements des composants clients. Ensuite, côté client, React utilise la charge utile RSC pour réconcilier les composants serveur et client en une seule arborescence.
  • Comme les composants clients sont rendus après les composants serveur, vous ne pouvez pas importer un composant serveur dans un module de composant client (car cela nécessiterait une nouvelle requête vers le serveur). À la place, vous pouvez passer un composant serveur comme props à un composant client. Voir les sections motif non pris en charge et motif pris en charge ci-dessous.

Motif non pris en charge : Importer des composants serveur dans des composants clients

Le motif suivant n'est pas pris en charge. Vous ne pouvez pas importer un composant serveur dans un composant client :

'use client'

// Vous ne pouvez pas importer un composant serveur dans un composant client.
import ServerComponent from './Server-Component'

export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ServerComponent />
    </>
  )
}
'use client'

// Vous ne pouvez pas importer un composant serveur dans un composant client.
import ServerComponent from './Server-Component'

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ServerComponent />
    </>
  )
}

Motif pris en charge : Passer des composants serveur à des composants clients comme props

Le motif suivant est pris en charge. Vous pouvez passer des composants serveur comme prop à un composant client.

Un motif courant consiste à utiliser la prop children de React pour créer un "emplacement" dans votre composant client.

Dans l'exemple ci-dessous, <ClientComponent> accepte une prop children :

'use client'

import { useState } from 'react'

export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  )
}
'use client'

import { useState } from 'react'

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      {children}
    </>
  )
}

<ClientComponent> ne sait pas que children sera éventuellement rempli par le résultat d'un composant serveur. La seule responsabilité de <ClientComponent> est de décider children sera placé.

Dans un composant serveur parent, vous pouvez importer à la fois <ClientComponent> et <ServerComponent> et passer <ServerComponent> comme enfant de <ClientComponent> :

// Ce motif fonctionne :
// Vous pouvez passer un composant serveur comme enfant ou prop d'un
// composant client.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// Les pages dans Next.js sont par défaut des composants serveur
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}
// Ce motif fonctionne :
// Vous pouvez passer un composant serveur comme enfant ou prop d'un
// composant client.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// Les pages dans Next.js sont par défaut des composants serveur
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

Avec cette approche, <ClientComponent> et <ServerComponent> sont découplés et peuvent être rendus indépendamment. Dans ce cas, l'enfant <ServerComponent> peut être rendu sur le serveur, bien avant que <ClientComponent> ne soit rendu côté client.

Bon à savoir :

  • Le motif de "remonter le contenu" a été utilisé pour éviter de re-rendre un composant enfant imbriqué lorsqu'un composant parent se re-rend.
  • Vous n'êtes pas limité à la prop children. Vous pouvez utiliser n'importe quelle prop pour passer du JSX.