Menu

Kompositionsmuster für Server- und Client-Komponenten

Beim Erstellen von React-Anwendungen musst du dir überlegen, welche Teile deiner Anwendung auf dem Server und welche auf dem Client gerendert werden sollen. Diese Seite behandelt empfohlene Kompositionsmuster für die Verwendung von Server- und Client-Komponenten.

Wann Server- und Client-Komponenten einsetzen?

Hier ist eine kurze Übersicht der verschiedenen Anwendungsfälle für Server- und Client-Komponenten:

Was möchtest du tun?Server-KomponenteClient-Komponente
Daten abrufen
Auf Backend-Ressourcen zugreifen (direkt)
Sensible Informationen auf dem Server behalten (Zugriffstoken, API-Keys etc.)
Große Abhängigkeiten auf dem Server behalten / Client-seitiges JavaScript reduzieren
Interaktivität und Event Listener hinzufügen (onClick(), onChange() etc.)
State und Lifecycle Effects verwenden (useState(), useReducer(), useEffect() etc.)
Browser-only APIs verwenden
Custom Hooks verwenden, die von State, Effects oder Browser-only APIs abhängen
React-Klassenkomponenten verwenden

Muster für Server-Komponenten

Bevor du dich für Client-seitiges Rendering entscheidest, möchtest du vielleicht bestimmte Aufgaben auf dem Server erledigen, wie das Abrufen von Daten oder den Zugriff auf deine Datenbank oder Backend-Services.

Hier sind einige gängige Muster für die Arbeit mit Server-Komponenten:

Daten zwischen Komponenten teilen

Beim Abrufen von Daten auf dem Server kann es Fälle geben, in denen du Daten über verschiedene Komponenten hinweg teilen musst. Zum Beispiel könnte ein Layout und eine Seite von denselben Daten abhängig sein.

Anstatt React Context zu verwenden (was auf dem Server nicht verfügbar ist) oder Daten als Props zu übergeben, kannst du fetch oder die cache-Funktion von React verwenden, um dieselben Daten in den Komponenten abzurufen, die sie benötigen. Dabei musst du dir keine Sorgen machen, dass dieselben Daten mehrfach angefragt werden. Der Grund dafür ist, dass React fetch automatisch erweitert, um Datenanfragen zu memoizieren, und die cache-Funktion verwendet werden kann, wenn fetch nicht verfügbar ist.

Sieh dir ein Beispiel für dieses Muster an.

Server-only Code vom Client-Kontext fernhalten

Da JavaScript-Module zwischen Server- und Client-Komponenten geteilt werden können, ist es möglich, dass Code, der eigentlich nur für den Server gedacht war, sich in den Client einschleicht.

Betrachte zum Beispiel die folgende Datenabfrage-Funktion:

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

Auf den ersten Blick scheint getData sowohl auf dem Server als auch auf dem Client zu funktionieren. Diese Funktion enthält jedoch einen API_KEY, der mit der Absicht geschrieben wurde, dass er nur auf dem Server ausgeführt wird.

Da die Umgebungsvariable API_KEY nicht mit NEXT_PUBLIC beginnt, ist sie eine private Variable, auf die nur auf dem Server zugegriffen werden kann. Um zu verhindern, dass deine Umgebungsvariablen an den Client durchsickern, ersetzt Next.js private Umgebungsvariablen durch einen leeren String.

Dadurch funktioniert getData() zwar importiert und ausgeführt werden kann, aber nicht wie erwartet. Und obwohl die Variable öffentlich machen würde, dass die Funktion auf dem Client funktioniert, möchtest du vielleicht keine sensiblen Informationen an den Client weitergeben.

Um diese Art von unbeabsichtigter Client-Nutzung von Server-Code zu verhindern, können wir das server-only Paket verwenden. Dies gibt anderen Entwicklern einen Build-Zeit-Fehler, wenn sie versehentlich eines dieser Module in eine Client-Komponente importieren.

Um server-only zu verwenden, installiere zunächst das Paket:

Terminal
npm install server-only

Dann importiere das Paket in jedes Modul, das Server-only 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()
}

Jetzt erhält jede Client-Komponente, die getData() importiert, einen Build-Zeit-Fehler mit der Erklärung, dass dieses Modul nur auf dem Server verwendet werden kann.

Das entsprechende Paket client-only kann verwendet werden, um Module zu kennzeichnen, die Client-only Code enthalten – zum Beispiel Code, der auf das window-Objekt zugreift.

Verwendung von Drittanbieter-Paketen und Providern

Da Server-Komponenten ein neues React-Feature sind, beginnen Drittanbieter-Pakete und Provider im Ökosystem gerade erst damit, die "use client"-Direktive für Komponenten hinzuzufügen, die Client-only Features wie useState, useEffect und createContext verwenden.

Heute haben viele Komponenten aus npm-Paketen, die Client-only Features verwenden, noch nicht diese Direktive. Diese Drittanbieter-Komponenten funktionieren wie erwartet innerhalb von Client-Komponenten, da diese die "use client"-Direktive haben, aber sie funktionieren nicht innerhalb von Server-Komponenten.

Nehmen wir zum Beispiel an, du hast das hypothetische acme-carousel Paket installiert, das eine <Carousel />-Komponente enthält. Diese Komponente verwendet useState, hat aber noch nicht die "use client"-Direktive.

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

app/gallery.tsx
TypeScript
'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 du sie jedoch direkt in einer Server-Komponente verwendest, siehst du einen Fehler:

app/page.tsx
TypeScript
import { Carousel } from 'acme-carousel'
 
export default function Page() {
  return (
    <div>
      <p>Bilder anzeigen</p>
 
      {/* Fehler: `useState` kann nicht in Server-Komponenten verwendet werden */}
      <Carousel />
    </div>
  )
}

Der Grund dafür ist, dass Next.js nicht weiß, dass <Carousel /> Client-only Features verwendet.

Um dies zu beheben, kannst du Drittanbieter-Komponenten, die von Client-only Features abhängig sind, in deine eigenen Client-Komponenten einpacken:

app/carousel.tsx
TypeScript
'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

Jetzt kannst du <Carousel /> direkt in einer Server-Komponente verwenden:

app/page.tsx
TypeScript
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>Bilder anzeigen</p>
 
      {/* Funktioniert, da Carousel eine Client-Komponente ist */}
      <Carousel />
    </div>
  )
}

Du musst voraussichtlich nicht viele Drittanbieter-Komponenten einpacken, da du sie wahrscheinlich innerhalb von Client-Komponenten verwendest. Eine Ausnahme sind Provider, da sie auf React State und Context basieren und typischerweise am Root einer Anwendung benötigt werden. Erfahre mehr über Drittanbieter-Context-Provider unten.

Verwendung von Context Providern

Context Provider werden typischerweise nahe der Wurzel einer Anwendung gerendert, um globale Anliegen wie das aktuelle Theme zu teilen. Da React Context in Server-Komponenten nicht unterstützt wird, wird der Versuch, einen Context an der Wurzel deiner Anwendung zu erstellen, einen Fehler verursachen:

app/layout.tsx
TypeScript
import { createContext } from 'react'
 
// createContext wird in Server-Komponenten nicht unterstützt
export const ThemeContext = createContext({})
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}

Um dies zu beheben, erstelle deinen Context und rendere seinen Provider innerhalb einer Client-Komponente:

app/theme-provider.tsx
TypeScript
'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>
}

Deine Server-Komponente kann nun den Provider direkt rendern, da er als Client-Komponente markiert wurde:

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

Nachdem der Provider an der Wurzel gerendert wurde, können alle anderen Client-Komponenten in deiner App diesen Context nutzen.

Hinweis: Provider sollten so tief wie möglich im Baum gerendert werden – beachte, wie ThemeProvider nur {children} umschließt und nicht das gesamte <html>-Dokument. Dies macht es für Next.js einfacher, die statischen Teile deiner Server-Komponenten zu optimieren.

Hinweise für Bibliotheksautoren

Ähnlich können Bibliotheksautoren, die Pakete für andere Entwickler erstellen, die "use client"-Direktive verwenden, um Client-Einstiegspunkte ihres Pakets zu markieren. Dies ermöglicht es Benutzern des Pakets, Paketkomponenten direkt in ihre Server-Komponenten zu importieren, ohne eine umschließende Boundary erstellen zu müssen.

Du kannst dein Paket optimieren, indem du 'use client' tiefer im Baum verwendest, wodurch die importiertenModule Teil des Server-Komponenten-Modulgraphen sein können.

Es ist erwähnenswert, dass einige Bundler die "use client"-Direktiven entfernen könnten. Ein Beispiel für die Konfiguration von esbuild, um die "use client"-Direktive beizubehalten, findest du in den Repositories von React Wrap Balancer und Vercel Analytics.

Client-Komponenten

Client-Komponenten im Baum nach unten verschieben

Um die Client-JavaScript-Bundle-Größe zu reduzieren, empfehlen wir, Client-Komponenten in deinem Komponenten-Baum nach unten zu verschieben.

Zum Beispiel könntest du ein Layout haben, das statische Elemente (z.B. Logo, Links etc.) und eine interaktive Suchleiste enthält, die State verwendet.

Anstatt das gesamte Layout zu einer Client-Komponente zu machen, verschiebe die interaktive Logik in eine Client-Komponente (z.B. <SearchBar />) und behalte dein Layout als Server-Komponente. Das bedeutet, dass du nicht den gesamten Komponenten-JavaScript-Code des Layouts an den Client senden musst.

app/layout.tsx
TypeScript
// SearchBar ist eine Client-Komponente
import SearchBar from './searchbar'
// Logo ist eine Server-Komponente
import Logo from './logo'
 
// Layout ist standardmäßig eine Server-Komponente
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

Props von Server- zu Client-Komponenten übergeben (Serialisierung)

Wenn du Daten in einer Server-Komponente abrufst, möchtest du vielleicht Daten als Props an Client-Komponenten weitergeben. Props, die vom Server an Client-Komponenten übergeben werden, müssen durch React serialisierbar sein.

Wenn deine Client-Komponenten von Daten abhängen, die nicht serialisierbar sind, kannst du Daten auf dem Client mit einer Drittanbieter-Bibliothek abrufen oder auf dem Server mit einem Route Handler.

Verschachtelung von Server- und Client-Komponenten

Bei der Verschachtelung von Client- und Server-Komponenten kann es hilfreich sein, deine UI als Komponenten-Baum zu visualisieren. Beginnend mit dem Root-Layout, das eine Server-Komponente ist, kannst du dann bestimmte Teilbäume von Komponenten auf dem Client rendern, indem du die "use client"-Direktive hinzufügst.

Innerhalb dieser Client-Teilbäume kannst du immer noch Server-Komponenten verschachteln oder Server Actions aufrufen, jedoch gibt es einige Dinge zu beachten:

  • Während eines Request-Response-Lebenszyklus bewegt sich dein Code vom Server zum Client. Wenn du auf dem Client auf Daten oder Ressourcen auf dem Server zugreifen musst, wirst du eine neue Anfrage an den Server stellen - nicht hin und her wechseln.
  • Wenn eine neue Anfrage an den Server gestellt wird, werden zuerst alle Server-Komponenten gerendert, einschließlich derer, die in Client-Komponenten verschachtelt sind. Das gerenderte Ergebnis (RSC Payload) wird Referenzen auf die Positionen der Client-Komponenten enthalten. Dann verwendet React auf dem Client den RSC Payload, um Server- und Client-Komponenten zu einem einzigen Baum zusammenzuführen.
  • Da Client-Komponenten nach Server-Komponenten gerendert werden, kannst du keine Server-Komponente in ein Client-Komponenten-Modul importieren (da dies eine neue Anfrage zurück an den Server erfordern würde). Stattdessen kannst du eine Server-Komponente als props an eine Client-Komponente übergeben. Siehe die Abschnitte nicht unterstütztes Muster und unterstütztes Muster unten.

Nicht unterstütztes Muster: Importieren von Server-Komponenten in Client-Komponenten

Das folgende Muster wird nicht unterstützt. Du kannst keine Server-Komponente in eine Client-Komponente importieren:

app/client-component.tsx
TypeScript
'use client'
 
// Du kannst keine Server-Komponente in eine Client-Komponente importieren.
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 />
    </>
  )
}

Unterstütztes Muster: Server-Komponenten als Props an Client-Komponenten übergeben

Das folgende Muster wird unterstützt. Du kannst Server-Komponenten als Props an eine Client-Komponente übergeben.

Ein gängiges Muster ist die Verwendung der React children prop, um einen "Slot" in deiner Client-Komponente zu erstellen.

Im folgenden Beispiel akzeptiert <ClientComponent> eine children prop:

app/client-component.tsx
TypeScript
'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}
    </>
  )
}

<ClientComponent> weiß nicht, dass children letztendlich mit dem Ergebnis einer Server-Komponente gefüllt wird. Die einzige Verantwortung von <ClientComponent> ist es zu entscheiden, wo children platziert werden wird.

In einer übergeordneten Server-Komponente kannst du sowohl die <ClientComponent> als auch die <ServerComponent> importieren und <ServerComponent> als Kind von <ClientComponent> übergeben:

app/page.tsx
TypeScript
// Dieses Muster funktioniert:
// Du kannst eine Server-Komponente als Kind oder Prop einer
// Client-Komponente übergeben.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
 
// Pages in Next.js sind standardmäßig Server-Komponenten
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

Mit diesem Ansatz sind <ClientComponent> und <ServerComponent> entkoppelt und können unabhängig voneinander gerendert werden. In diesem Fall kann die untergeordnete <ServerComponent> auf dem Server gerendert werden, lange bevor <ClientComponent> auf dem Client gerendert wird.

Hinweis:

  • Das Muster des "Hochhebens von Inhalten" wurde verwendet, um ein erneutes Rendern einer verschachtelten Kindkomponente zu vermeiden, wenn eine übergeordnete Komponente neu gerendert wird.
  • Du bist nicht auf die children prop beschränkt. Du kannst jede beliebige Prop verwenden, um JSX zu übergeben.