Datenabruf und Streaming

Diese Seite führt Sie durch den Datenabruf in Server- und Client-Komponenten und das Streaming von Komponenten, die von Daten abhängig sind.

Datenabruf

Server-Komponenten

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

  1. Der fetch-API
  2. Einem ORM oder 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>
  )
}
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. Next.js wird die Route jedoch vorrendern und die Ausgabe für bessere Leistung zwischenspeichern. Für dynamisches Rendering verwenden Sie die Option { cache: 'no-store' }. Siehe fetch-API-Referenz.
  • Während der Entwicklung können Sie fetch-Aufrufe protokollieren für bessere Sichtbarkeit und Debugging. Siehe logging-API-Referenz.

Mit einem ORM oder 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:

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

Daten-Streaming 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() {
  // Die Datenabruffunktion nicht mit await aufrufen
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}
import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // Die Datenabruffunktion nicht mit await aufrufen
  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>
  )
}
'use client'
import { use } from 'react'

export default function Posts({ posts }) {
  const posts = use(posts)

  return (
    <ul>
      {posts.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. 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 eigene 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>
  )
}
'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) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Anfragededuplizierung mit React.cache

Deduplizierung ist der Prozess, doppelte Anfragen für dieselbe Ressource während eines Renderdurchlaufs 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, Sie können dieselbe URL mit denselben Optionen sicher aufrufen, und es wird nur eine Anfrage gemacht.

Wenn Sie kein fetch verwenden, sondern direkt ein ORM oder eine Datenbank, 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)),
  })
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'

export const getPost = cache(async (id) => {
  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.

Bei Verwendung von async/await in Server-Komponenten wird Next.js dynamisches Rendering verwenden. Das bedeutet, die Daten werden für jede Benutzeranfrage auf dem Server abgerufen und gerendert. Wenn langsame Datenanfragen vorhanden sind, wird die gesamte Route blockiert.

Um die initiale Ladezeit und Benutzererfahrung zu verbessern, können Sie Streaming verwenden, um das HTML der Seite in kleinere Teile aufzuteilen und diese schrittweise vom Server zum Client zu senden.

Wie Server-Rendering mit Streaming funktioniert

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

  1. Umhüllen einer Seite mit einer loading.js-Datei
  2. Umhüllen 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 zu streamen, während die Daten abgerufen werden. Um beispielsweise 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>
}
export default function Loading() {
  // Definieren Sie hier die Lade-UI
  return <div>Loading...</div>
}

Bei Navigation sieht der Benutzer sofort das Layout und einen Ladezustand, während die Seite gerendert wird. Der neue Inhalt wird automatisch eingeblendet, 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), für granulareres 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>
  )
}
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, die sinnvoll sind und dem Benutzer vermitteln, dass die Anwendung reagiert. Beispielsweise können Sie Skelette und Spinner verwenden oder einen kleinen, aber bedeutungsvollen Teil zukünftiger Bildschirme wie ein Titelbild, Titel usw.

In der Entwicklung können Sie die Ladezustände 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 einer anderen abhängt.

Beispielsweise beginnt die <Playlists>-Komponente erst mit dem Datenabruf, wenn 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>
  )
}
export default async function Page({ params }) {
  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 }) {
  // 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 Daten-Fetching

Paralleles Daten-Fetching findet statt, wenn Datenanfragen in einer Route eifrig initiiert werden und gleichzeitig starten.

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

Allerdings können innerhalb jeder Komponente mehrere async/await-Anfragen dennoch sequenziell sein, wenn sie nacheinander 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, zum Beispiel 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} />
    </>
  )
}
import Albums from './albums'

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

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

export default async function Page({ params }) {
  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 der Verwendung von Promise.all fehlschlägt, schlägt der gesamte Vorgang fehl. Um dies zu behandeln, 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)
  // ...
}
import { getItem } from '@/lib/data'

export default async function Page({ params }) {
  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) => {
  // 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 }) {
  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) => {
  // ...
})
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

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

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