Datenabruf und Streaming

Diese Seite führt Sie durch den Prozess des Datenabrufs in Server- und Client-Komponenten und zeigt, wie Sie Streaming für datenabhängige Komponenten nutzen können.

Datenabruf

Server-Komponenten

Sie können Daten in Server-Komponenten mit folgenden Methoden abrufen:

  1. Der fetch-API
  2. Einem ORM oder einer Datenbank

Mit der fetch-API

Um Daten mit der fetch-API abzurufen, machen Sie Ihre Komponente zu einer asynchronen Funktion und verwenden Sie await für den fetch-Aufruf. Beispiel:

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Gut zu wissen:

  • fetch-Antworten werden standardmäßig nicht zwischengespeichert. Allerdings wird Next.js die Route vorrendern und das Ergebnis für bessere Leistung zwischenspeichern. Wenn Sie dynamisches Rendering bevorzugen, verwenden Sie die Option { cache: 'no-store' }. Siehe die fetch-API-Referenz.
  • Während der Entwicklung können Sie fetch-Aufrufe protokollieren, um bessere Sichtbarkeit und Debugging zu ermöglichen. Siehe die logging-API-Referenz.

Mit einem ORM oder einer Datenbank

Da Server-Komponenten auf dem Server gerendert werden, können Sie sicher Datenbankabfragen mit einem ORM oder Datenbankclient durchführen. Machen Sie Ihre Komponente zu einer asynchronen Funktion und verwenden Sie await für den Aufruf:

import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Client-Komponenten

Es gibt zwei Möglichkeiten, Daten in Client-Komponenten abzurufen:

  1. Mit Reacts use-Hook
  2. Mit einer Community-Bibliothek wie SWR oder React Query

Datenstreaming mit dem use-Hook

Sie können Reacts use-Hook verwenden, um Daten vom Server zum Client zu streamen. Beginnen Sie mit dem Datenabruf in Ihrer Server-Komponente und übergeben Sie das Promise als Prop an Ihre Client-Komponente:

import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // Warten Sie nicht auf die Datenabruffunktion
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

Verwenden Sie dann in Ihrer Client-Komponente den use-Hook, um das Promise zu lesen:

'use client'
import { use } from 'react'

export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Im obigen Beispiel ist die <Posts>-Komponente in eine <Suspense>-Grenze eingebettet. Das bedeutet, dass der Fallback angezeigt wird, während das Promise aufgelöst wird. Erfahren Sie mehr über Streaming.

Community-Bibliotheken

Sie können eine Community-Bibliothek wie SWR oder React Query verwenden, um Daten in Client-Komponenten abzurufen. Diese Bibliotheken haben ihre eigenen Semantiken für Caching, Streaming und andere Funktionen. Beispiel mit SWR:

'use client'
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((r) => r.json())

export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Deduplizierung von Anfragen mit React.cache

Deduplizierung ist der Prozess, doppelte Anfragen für dieselbe Ressource während eines Render-Durchlaufs zu verhindern. Es ermöglicht Ihnen, dieselben Daten in verschiedenen Komponenten abzurufen, während mehrere Netzwerkanfragen an Ihre Datenquelle verhindert werden.

Wenn Sie fetch verwenden, können Anfragen durch Hinzufügen von cache: 'force-cache' dedupliziert werden. Das bedeutet, dass Sie dieselbe URL mit denselben Optionen sicher aufrufen können und nur eine Anfrage durchgeführt wird.

Wenn Sie fetch nicht verwenden und stattdessen direkt ein ORM oder eine Datenbank nutzen, können Sie Ihren Datenabruf mit der React cache-Funktion umschließen.

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'

export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})

Streaming

Warnung: Der folgende Inhalt setzt voraus, dass die dynamicIO-Konfigurationsoption in Ihrer Anwendung aktiviert ist. Das Flag wurde in Next.js 15 Canary eingeführt.

Wenn Sie async/await in Server-Komponenten verwenden, wird Next.js dynamisches Rendering aktivieren. Das bedeutet, dass die Daten für jede Benutzeranfrage auf dem Server abgerufen und gerendert werden. Wenn es langsame Datenanfragen gibt, wird die gesamte Route blockiert.

Um die anfängliche Ladezeit und Benutzererfahrung zu verbessern, können Sie Streaming verwenden, um den HTML-Code der Seite in kleinere Teile aufzuteilen und diese schrittweise vom Server an den Client zu senden.

Wie Server-Rendering mit Streaming funktioniert

Es gibt zwei Möglichkeiten, Streaming in Ihrer Anwendung zu implementieren:

  1. Umhüllung einer Seite mit einer loading.js-Datei
  2. Umhüllung einer Komponente mit <Suspense>

Mit loading.js

Sie können eine loading.js-Datei im selben Ordner wie Ihre Seite erstellen, um die gesamte Seite während des Datenabrufs zu streamen. Beispiel: Um app/blog/page.js zu streamen, fügen Sie die Datei im Ordner app/blog hinzu.

Blog-Ordnerstruktur mit loading.js-Datei
export default function Loading() {
  // Definieren Sie hier die Lade-UI
  return <div>Loading...</div>
}

Bei der Navigation sieht der Benutzer sofort das Layout und einen Ladezustand, während die Seite gerendert wird. Der neue Inhalt wird automatisch ausgetauscht, sobald das Rendering abgeschlossen ist.

Lade-UI

Im Hintergrund wird loading.js in layout.js eingebettet und automatisch die page.js-Datei und alle untergeordneten Komponenten in eine <Suspense>-Grenze eingeschlossen.

Übersicht über loading.js

Dieser Ansatz funktioniert gut für Routensegmente (Layouts und Seiten), aber für granuläreres Streaming können Sie <Suspense> verwenden.

Mit <Suspense>

<Suspense> ermöglicht es Ihnen, genauer zu steuern, welche Teile der Seite gestreamt werden sollen. Beispielsweise können Sie Inhalte außerhalb der <Suspense>-Grenze sofort anzeigen und die Blogpost-Liste innerhalb der Grenze streamen.

import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'

export default function BlogPage() {
  return (
    <div>
      {/* Dieser Inhalt wird sofort an den Client gesendet */}
      <header>
        <h1>Willkommen im Blog</h1>
        <p>Lesen Sie die neuesten Beiträge unten.</p>
      </header>
      <main>
        {/* Jeder Inhalt innerhalb einer <Suspense>-Grenze wird gestreamt */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

Sinnvolle Ladezustände erstellen

Ein sofortiger Ladezustand ist eine Fallback-UI, die dem Benutzer unmittelbar nach der Navigation angezeigt wird. Für die beste Benutzererfahrung empfehlen wir, Ladezustände zu entwerfen, die sinnvoll sind und dem Benutzer vermitteln, dass die Anwendung reagiert. Beispielsweise können Sie Skelette und Spinner oder einen kleinen, aber bedeutenden Teil zukünftiger Bildschirme wie ein Titelbild, Titel usw. verwenden.

In der Entwicklung können Sie den Ladezustand Ihrer Komponenten mit den React Devtools überprüfen.

Beispiele

Sequenzieller Datenabruf

Sequenzieller Datenabruf tritt auf, wenn verschachtelte Komponenten in einem Baum jeweils ihre eigenen Daten abrufen und die Anfragen nicht dedupliziert werden, was zu längeren Antwortzeiten führt.

Sequenzieller und paralleler Datenabruf

Es gibt Fälle, in denen Sie dieses Muster wünschen, weil eine Abfrage vom Ergebnis der anderen abhängt.

Beispielsweise beginnt die <Playlists>-Komponente erst mit dem Datenabruf, nachdem die <Artist>-Komponente fertig ist, da <Playlists> von der artistID-Prop abhängt:

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // Künstlerinformationen abrufen
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      {/* Fallback-UI anzeigen, während die Playlists-Komponente lädt */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* Künstler-ID an die Playlists-Komponente übergeben */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

async function Playlists({ artistID }: { artistID: string }) {
  // Künstler-ID verwenden, um Playlists abzurufen
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

Um die Benutzererfahrung zu verbessern, sollten Sie React <Suspense> verwenden, um einen fallback während des Datenabrufs anzuzeigen. Dies ermöglicht Streaming und verhindert, dass die gesamte Route durch sequenzielle Datenanfragen blockiert wird.

Paralleles Abrufen von Daten

Paralleles Abrufen von Daten (Parallel Data Fetching) erfolgt, wenn Datenanfragen in einer Route eifrig initiiert werden und gleichzeitig starten.

Standardmäßig werden Layouts und Seiten parallel gerendert. Daher beginnt jedes Segment so früh wie möglich mit dem Abruf der Daten.

Innerhalb jeder Komponente können jedoch mehrere async/await-Anfragen dennoch sequenziell erfolgen, wenn sie hintereinander platziert werden. Beispielsweise wird getAlbums blockiert, bis getArtist aufgelöst ist:

import { getArtist, getAlbums } from '@/app/lib/data'

export default async function Page({ params }) {
  // Diese Anfragen erfolgen sequenziell
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

Sie können Anfragen parallel initiieren, indem Sie sie außerhalb der Komponenten definieren, die die Daten verwenden, und sie gemeinsam auflösen, beispielsweise mit Promise.all:

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  // Beide Anfragen parallel initiieren
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  )
}

Gut zu wissen: Wenn eine Anfrage bei Verwendung von Promise.all fehlschlägt, schlägt der gesamte Vorgang fehl. Um dies zu handhaben, können Sie stattdessen die Methode Promise.allSettled verwenden.

Vorabladung von Daten

Sie können Daten vorab laden, indem Sie eine Hilfsfunktion erstellen, die Sie eifrig über blockierenden Anfragen aufrufen. <Item> rendert bedingt basierend auf der Funktion checkIsAvailable().

Sie können preload() vor checkIsAvailable() aufrufen, um die Datenabhängigkeiten von <Item/> eifrig zu initiieren. Wenn <Item/> gerendert wird, wurden seine Daten bereits abgerufen.

import { getItem } from '@/lib/data'

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // Beginne mit dem Laden der Item-Daten
  preload(id)
  // Führe eine weitere asynchrone Aufgabe aus
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

export const preload = (id: string) => {
  // void wertet den gegebenen Ausdruck aus und gibt undefined zurück
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}

Zusätzlich können Sie Reacts cache-Funktion und das server-only-Paket verwenden, um eine wiederverwendbare Hilfsfunktion zu erstellen. Dieser Ansatz ermöglicht es Ihnen, die Datenabruffunktion zwischenzuspeichern und sicherzustellen, dass sie nur auf dem Server ausgeführt wird.

import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})