Verwendung von Server- und Client-Komponenten

Standardmäßig sind Layouts und Seiten Server-Komponenten (Server Components), was Ihnen ermöglicht, Daten abzurufen und Teile Ihrer Benutzeroberfläche auf dem Server zu rendern, das Ergebnis optional zwischenzuspeichern und es an den Client zu streamen. Wenn Sie Interaktivität oder Browser-APIs benötigen, können Sie Client-Komponenten (Client Components) verwenden, um Funktionalität hinzuzufügen.

Diese Seite erklärt, wie Server- und Client-Komponenten in Next.js funktionieren und wann sie verwendet werden sollten, mit Beispielen, wie sie in Ihrer Anwendung kombiniert werden können.

Wann sollten Server- und Client-Komponenten verwendet werden?

Die Client- und Server-Umgebungen haben unterschiedliche Fähigkeiten. Server- und Client-Komponenten ermöglichen es Ihnen, Logik je nach Anwendungsfall in der jeweiligen Umgebung auszuführen.

Verwenden Sie Client-Komponenten, wenn Sie benötigen:

Verwenden Sie Server-Komponenten, wenn Sie benötigen:

  • Daten aus Datenbanken oder APIs nahe der Quelle abzurufen.
  • API-Schlüssel, Tokens und andere Geheimnisse zu verwenden, ohne sie dem Client preiszugeben.
  • Die Menge an JavaScript, die an den Browser gesendet wird, zu reduzieren.
  • Die First Contentful Paint (FCP) zu verbessern und Inhalte progressiv an den Client zu streamen.

Beispielsweise ist die <Page>-Komponente eine Server-Komponente, die Daten über einen Beitrag abruft und sie als Props an die <LikeButton> weitergibt, die die clientseitige Interaktivität handhabt.

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>
  )
}

Wie funktionieren Server- und Client-Komponenten in Next.js?

Auf dem Server

Auf dem Server verwendet Next.js React-APIs, um das Rendering zu orchestrieren. Die Rendering-Arbeit wird in Abschnitte unterteilt, nach einzelnen Routensegmenten (Layouts und Seiten):

  • Server-Komponenten werden in ein spezielles Datenformat namens React Server Component Payload (RSC Payload) gerendert.
  • Client-Komponenten und der RSC Payload werden verwendet, um HTML vorzurrendern (prerender).

Was ist der React Server Component Payload (RSC)?

Der RSC Payload ist eine kompakte binäre Darstellung des gerenderten React Server Components-Baums. Er wird von React auf dem Client verwendet, um das DOM des Browsers zu aktualisieren. Der RSC Payload enthält:

  • Das gerenderte Ergebnis von Server-Komponenten
  • Platzhalter für die Stellen, an denen Client-Komponenten gerendert werden sollten, und Referenzen zu ihren JavaScript-Dateien
  • Alle Props, die von einer Server-Komponente an eine Client-Komponente übergeben werden

Auf dem Client (erster Ladevorgang)

Dann, auf dem Client:

  1. HTML wird verwendet, um dem Benutzer sofort eine schnelle, nicht-interaktive Vorschau der Route anzuzeigen.
  2. RSC Payload wird verwendet, um die Bäume der Client- und Server-Komponenten abzugleichen.
  3. JavaScript wird verwendet, um Client-Komponenten zu hydratisieren und die Anwendung interaktiv zu machen.

Was ist Hydratisierung?

Hydratisierung ist der Prozess von React, um Event-Handler an das DOM anzuhängen und das statische HTML interaktiv zu machen.

Nachfolgende Navigationen

Bei nachfolgenden Navigationen:

  • Der RSC Payload wird im Voraus abgerufen und zwischengespeichert, um eine sofortige Navigation zu ermöglichen.
  • Client-Komponenten werden vollständig auf dem Client gerendert, ohne das server-seitig gerenderte HTML.

Beispiele

Verwendung von Client-Komponenten

Sie können eine Client-Komponente erstellen, indem Sie die "use client"-Direktive oben in der Datei, über Ihren Imports, hinzufügen.

'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)}>Klick mich</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)}>Klick mich</button>
    </div>
  )
}

"use client" wird verwendet, um eine Grenze zwischen den Server- und Client-Modulgraphen (Bäumen) zu deklarieren.

Sobald eine Datei mit "use client" markiert ist, werden alle ihre Imports und untergeordneten Komponenten als Teil des Client-Bundles betrachtet. Das bedeutet, Sie müssen die Direktive nicht jeder Komponente hinzufügen, die für den Client bestimmt ist.

Reduzierung der JS-Bundle-Größe

Um die Größe Ihrer Client-JavaScript-Bundles zu reduzieren, fügen Sie 'use client' zu bestimmten interaktiven Komponenten hinzu, anstatt große Teile Ihrer Benutzeroberfläche als Client-Komponenten zu markieren.

Beispielsweise enthält die <Layout>-Komponente hauptsächlich statische Elemente wie ein Logo und Navigationslinks, aber eine interaktive Suchleiste. <Search /> ist interaktiv und muss eine Client-Komponente sein, der Rest des Layouts kann jedoch eine Server-Komponente bleiben.

'use client'

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

Übergeben von Daten von Server- zu Client-Komponenten

Sie können Daten von Server-Komponenten an Client-Komponenten über Props übergeben.

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

Alternativ können Sie Daten von einer Server-Komponente an eine Client-Komponente mit dem use-Hook streamen. Siehe ein Beispiel.

Gut zu wissen: Props, die an Client-Komponenten übergeben werden, müssen von React serialisierbar sein.

Verschachteln von Server- und Client-Komponenten

Sie können Server-Komponenten als Prop an eine Client-Komponente übergeben. Dies ermöglicht es Ihnen, server-seitig gerenderte UI visuell innerhalb von Client-Komponenten zu verschachteln.

Ein gängiges Muster ist die Verwendung von children, um einen Slot in einer <ClientComponent> zu erstellen. Zum Beispiel eine <Cart>-Komponente, die Daten auf dem Server abruft, innerhalb einer <Modal>-Komponente, die Client-State verwendet, um die Sichtbarkeit zu steuern.

'use client'

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

Dann können Sie in einer übergeordneten Server-Komponente (z.B. <Page>) eine <Cart> als Kind der <Modal> übergeben:

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

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

In diesem Muster werden alle Server-Komponenten im Voraus auf dem Server gerendert, einschließlich derer, die als Props übergeben werden. Der resultierende RSC Payload enthält Referenzen, wo Client-Komponenten innerhalb des Komponentenbaums gerendert werden sollten.

Context-Provider

React Context wird häufig verwendet, um globalen State wie das aktuelle Theme zu teilen. Allerdings wird React Context in Server-Komponenten nicht unterstützt.

Um Context zu verwenden, erstellen Sie eine Client-Komponente, die children akzeptiert:

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

Dann importieren Sie es in eine Server-Komponente (z.B. layout):

import ThemeProvider from './theme-provider'

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

Ihre Server-Komponente kann nun Ihren Provider direkt rendern, und alle anderen Client-Komponenten in Ihrer App können diesen Context nutzen.

Gut zu wissen: Sie sollten Provider so tief wie möglich im Baum rendern – beachten Sie, dass ThemeProvider nur {children} umschließt und nicht das gesamte <html>-Dokument. Dies erleichtert es Next.js, die statischen Teile Ihrer Server-Komponenten zu optimieren.

Drittanbieter-Komponenten

Wenn Sie eine Drittanbieter-Komponente verwenden, die auf client-spezifischen Funktionen basiert, können Sie sie in eine Client-Komponente einwickeln, um sicherzustellen, dass sie wie erwartet funktioniert.

Zum Beispiel kann die <Carousel /> aus dem acme-carousel-Paket importiert werden. Diese Komponente verwendet useState, hat aber noch keine "use client"-Direktive.

Wenn Sie <Carousel /> innerhalb einer Client-Komponente verwenden, funktioniert sie wie erwartet:

'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)}>Bilder anzeigen</button>
      {/* Funktioniert, da Carousel innerhalb einer Client-Komponente verwendet wird */}
      {isOpen && <Carousel />}
    </div>
  )
}

Wenn Sie jedoch versuchen, sie direkt innerhalb einer Server-Komponente zu verwenden, erhalten Sie einen Fehler. Das liegt daran, dass Next.js nicht weiß, dass <Carousel /> client-spezifische Funktionen verwendet.

Um dies zu beheben, können Sie Drittanbieter-Komponenten, die auf client-spezifischen Funktionen basieren, in Ihre eigenen Client-Komponenten einwickeln:

'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

Jetzt können Sie <Carousel /> direkt innerhalb einer Server-Komponente verwenden:

import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>Bilder anzeigen</p>
      {/* Funktioniert, da Carousel eine Client-Komponente ist */}
      <Carousel />
    </div>
  )
}

Tipp für Bibliotheksautoren

Wenn Sie eine Komponentenbibliothek erstellen, fügen Sie die "use client"-Direktive zu Einstiegspunkten hinzu, die auf client-spezifischen Funktionen basieren. Dies ermöglicht es Ihren Benutzern, Komponenten in Server-Komponenten zu importieren, ohne Wrapper erstellen zu müssen.

Es ist erwähnenswert, dass einige Bundler "use client"-Direktiven entfernen könnten. Ein Beispiel, wie Sie esbuild konfigurieren können, um die "use client"-Direktive einzubeziehen, finden Sie in den Repositories React Wrap Balancer und Vercel Analytics.

Vermeidung von Umgebungskontamination

JavaScript-Module können sowohl von Server- als auch von Client-Komponenten gemeinsam genutzt werden. Das bedeutet, dass versehentlich serverseitiger Code in den Client importiert werden kann. Betrachten Sie beispielsweise die folgende Funktion:

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

  return res.json()
}

Diese Funktion enthält einen API_KEY, der niemals dem Client zugänglich gemacht werden sollte.

In Next.js werden nur Umgebungsvariablen, die mit NEXT_PUBLIC_ beginnen, in das Client-Bundle aufgenommen. Wenn Variablen nicht mit diesem Präfix versehen sind, ersetzt Next.js sie durch einen leeren String.

Folglich wird getData(), selbst wenn es im Client importiert und ausgeführt wird, nicht wie erwartet funktionieren.

Um versehentliche Verwendung in Client-Komponenten zu verhindern, können Sie das server-only-Paket verwenden.

Terminal
npm install server-only

Importieren Sie dann das Paket in eine Datei, die serverseitigen Code enthält:

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()
}

Wenn Sie nun versuchen, das Modul in eine Client-Komponente zu importieren, wird ein Build-Time-Fehler auftreten.

Gut zu wissen: Das entsprechende client-only-Paket kann verwendet werden, um Module zu kennzeichnen, die clientseitige Logik enthalten, wie z.B. Code, der auf das window-Objekt zugreift.