Menu

Datenabruf und Caching

Beispiele

Diese Anleitung führt Sie durch die Grundlagen des Datenabrufs und Cachings in Next.js und bietet praktische Beispiele und Best Practices.

Hier ist ein minimales Beispiel für Datenabruf in Next.js:

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

Dieses Beispiel zeigt einen grundlegenden serverseitigen Datenabruf mit der fetch-API in einer asynchronen React-Serverkomponente.

Referenz

Beispiele

Datenabruf auf dem Server mit der fetch-API

Diese Komponente ruft eine Liste von Blogbeiträgen ab und zeigt sie an. Die Antwort von fetch wird automatisch zwischengespeichert.

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

Wenn Sie keine dynamischen APIs an anderer Stelle in dieser Route verwenden, wird sie während next build zu einer statischen Seite vorgerendert. Die Daten können dann mit Inkrementeller Statischer Regeneration aktualisiert werden.

Wenn Sie die Antwort von fetch nicht zwischenspeichern möchten, können Sie Folgendes tun:

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

Datenabruf auf dem Server mit einem ORM oder einer Datenbank

Diese Komponente ruft eine Liste von Blogbeiträgen ab und zeigt sie an. Die Antwort aus der Datenbank wird standardmäßig nicht zwischengespeichert, könnte aber mit zusätzlicher Konfiguration zwischengespeichert werden.

app/page.tsx
TypeScript
import { db, posts } from '@/lib/db'
 
export default async function Page() {
  let allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Wenn Sie keine dynamischen APIs an anderer Stelle in dieser Route verwenden, wird sie während next build zu einer statischen Seite vorgerendert. Die Daten können dann mit Inkrementeller Statischer Regeneration aktualisiert werden.

Um zu verhindern, dass die Seite vorgerendert wird, können Sie Folgendes zu Ihrer Datei hinzufügen:

export const dynamic = 'force-dynamic'

Üblicherweise verwenden Sie jedoch Funktionen wie cookies, headers oder das Lesen der eingehenden searchParams von den Seitenprops, was die Seite automatisch dynamisch rendert. In diesem Fall benötigen Sie force-dynamic nicht explizit.

Datenabruf auf dem Client

Wir empfehlen zunächst, Daten serverseitig abzurufen.

Es gibt jedoch immer noch Fälle, in denen clientseitiger Datenabruf Sinn macht. In diesen Szenarien können Sie manuell fetch in einem useEffect aufrufen (nicht empfohlen) oder sich auf beliebte React-Bibliotheken in der Community (wie SWR oder React Query) für Client-Abruf verlassen.

app/page.tsx
TypeScript
'use client'
 
import { useState, useEffect } from 'react'
 
export function Posts() {
  const [posts, setPosts] = useState(null)
 
  useEffect(() => {
    async function fetchPosts() {
      let res = await fetch('https://api.vercel.app/blog')
      let data = await res.json()
      setPosts(data)
    }
    fetchPosts()
  }, [])
 
  if (!posts) return <div>Laden...</div>
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Zwischenspeichern von Daten mit einem ORM oder einer Datenbank

Sie können die unstable_cache-API verwenden, um die Antwort zwischenzuspeichern und Seiten beim Ausführen von next build vorzurendern.

app/page.tsx
TypeScript
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
 
const getPosts = unstable_cache(
  async () => {
    return await db.select().from(posts)
  },
  ['posts'],
  { revalidate: 3600, tags: ['posts'] }
)
 
export default async function Page() {
  const allPosts = await getPosts()
 
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Dieses Beispiel speichert das Ergebnis der Datenbankabfrage für 1 Stunde (3600 Sekunden) zwischen. Es fügt außerdem das Cache-Tag posts hinzu, das dann mit Inkrementeller Statischer Regeneration ungültig gemacht werden kann.

Wiederverwendung von Daten über mehrere Funktionen hinweg

Next.js verwendet APIs wie generateMetadata und generateStaticParams, bei denen Sie die gleichen Daten verwenden müssen, die in der page abgerufen wurden.

Bei Verwendung von fetch werden Anfragen automatisch memoisiert. Das bedeutet, Sie können dieselbe URL mit denselben Optionen sicher aufrufen, und es wird nur eine Anfrage gestellt.

app/page.tsx
TypeScript
import { notFound } from 'next/navigation'
 
interface Post {
  id: string
  title: string
  content: string
}
 
async function getPost(id: string) {
  let res = await fetch(`https://api.vercel.app/blog/${id}`)
  let post: Post = await res.json()
  if (!post) notFound()
  return post
}
 
export async function generateStaticParams() {
  let posts = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json()
  )
 
  return posts.map((post: Post) => ({
    id: post.id,
  }))
}
 
export async function generateMetadata({ params }: { params: { id: string } }) {
  let post = await getPost(params.id)
 
  return {
    title: post.title,
  }
}
 
export default async function Page({ params }: { params: { id: string } }) {
  let post = await getPost(params.id)
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

Wenn Sie nicht fetch verwenden und stattdessen ein ORM oder eine Datenbank direkt nutzen, können Sie Ihre Datenabrufe mit der React-Funktion cache umwickeln. Dies wird Doppelanfragen eliminieren und nur eine Abfrage durchführen.

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db' // Beispiel mit Drizzle ORM
import { notFound } from 'next/navigation'
 
export const getPost = cache(async (id) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
 
  if (!post) notFound()
  return post
})

Zwischengespeicherte Daten neu validieren

Erfahren Sie mehr über das Neu-Validieren zwischengespeicherter Daten mit Inkrementeller Statischer Regeneration.

Muster

Paralleler und sequenzieller Datenabruf

Beim Datenabruf in Komponenten müssen Sie zwei Datenabrufmuster beachten: Parallel und Sequenziell.

Sequenzieller und paralleler Datenabruf
  • Sequenziell: Anfragen in einer Komponentenhierarchie sind voneinander abhängig. Dies kann zu längeren Ladezeiten führen.
  • Parallel: Anfragen in einer Route werden sofort initiiert und laden Daten gleichzeitig. Dies reduziert die Gesamtzeit für den Datenabruf.

Sequenzieller Datenabruf

Wenn Sie verschachtelte Komponenten haben und jede Komponente ihre eigenen Daten abruft, dann erfolgt der Datenabruf sequenziell, wenn diese Datenanfragen nicht memoisiert sind.

Es kann Fälle geben, in denen Sie dieses Muster wünschen, weil ein Abruf vom Ergebnis eines anderen abhängt. Beispielsweise beginnt die Playlists-Komponente erst mit dem Datenabruf, wenn die Artist-Komponente ihre Daten abgerufen hat, da Playlists von der artistID abhängt:

app/artist/[username]/page.tsx
TypeScript
export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // 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>Lädt...</div>}>
        {/* Künstler-ID an die Playlists-Komponente übergeben */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
 
async function Playlists({ artistID }: { artistID: string }) {
  // Playlists mit der Künstler-ID abrufen
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

Sie können loading.js (für Routensegmente) oder React <Suspense> (für verschachtelte Komponenten) verwenden, um einen sofortigen Ladestand anzuzeigen, während React das Ergebnis streamt.

Dies verhindert, dass die gesamte Route durch Datenanfragen blockiert wird, und der Benutzer kann mit den bereits fertigen Teilen der Seite interagieren.

Paralleler Datenabruf

Standardmäßig werden Layout- und Seitensegmente parallel gerendert. Das bedeutet, Anfragen werden parallel initiiert.

Aufgrund der Natur von async/await blockiert eine abgerufene Anfrage innerhalb desselben Segments oder derselben Komponente alle Anfragen darunter.

Um Daten parallel abzurufen, können Sie Anfragen initiieren, indem Sie sie außerhalb der Komponenten definieren, die die Daten verwenden. Dies spart Zeit, da beide Anfragen parallel gestartet werden, jedoch sieht der Benutzer das gerenderte Ergebnis erst, wenn beide Promises aufgelöst sind.

Im folgenden Beispiel werden die Funktionen getArtist und getAlbums außerhalb der Page-Komponente definiert und innerhalb der Komponente mit Promise.all initiiert:

app/artist/[username]/page.tsx
TypeScript
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: { username },
}: {
  params: { username: string }
}) {
  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} />
    </>
  )
}

Zusätzlich können Sie eine Suspense-Grenze hinzufügen, um die Renderarbeit aufzuteilen und einen Teil des Ergebnisses so schnell wie möglich anzuzeigen.

Daten vorabladen

Eine weitere Möglichkeit, Wasserfall-Effekte zu vermeiden, ist das Preloading-Muster durch Erstellen einer Hilfsfunktion, die Sie über blockierenden Anfragen gezielt aufrufen. Beispielsweise blockiert checkIsAvailable() das Rendering von <Item/>, daher können Sie preload() vor dem Rendering aufrufen, um die Datenabhängigkeiten von <Item/> frühzeitig zu initiieren. Bis <Item/> gerendert wird, sind seine Daten bereits abgerufen.

Beachten Sie, dass die preload-Funktion checkIsAvailable() nicht blockiert.

components/Item.tsx
TypeScript
import { getItem } from '@/utils/get-item'
 
export const preload = (id: string) => {
  // void evaluiert den Ausdruck und gibt undefined zurück
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
app/item/[id]/page.tsx
TypeScript
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // Laden der Elementdaten starten
  preload(id)
  // Andere asynchrone Aufgabe ausführen
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}

Hinweis: Die "preload"-Funktion kann auch einen beliebigen Namen haben, da es sich um ein Muster und nicht um eine API handelt.

Verwendung von React cache und server-only mit dem Preload-Muster

Sie können die cache-Funktion, das preload-Muster und das server-only-Paket kombinieren, um ein Daten-Fetching-Utility zu erstellen, das in Ihrer App verwendet werden kann.

utils/get-item.ts
import { cache } from 'react'
import 'server-only'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})
utils/get-item.js
import { cache } from 'react'
import 'server-only'
 
export const preload = (id) => {
  void getItem(id)
}
 
export const getItem = cache(async (id) => {
  // ...
})

Mit diesem Ansatz können Sie Daten gezielt vorab abrufen, Antworten zwischenspeichern und sicherstellen, dass das Daten-Fetching nur auf dem Server stattfindet.

Die Exporte von utils/get-item können von Layouts, Seiten oder anderen Komponenten verwendet werden, um die Kontrolle über den Zeitpunkt des Datenabrufs eines Elements zu haben.

Hinweis:

  • Wir empfehlen die Verwendung des server-only-Pakets, um sicherzustellen, dass Daten-Fetching-Funktionen des Servers niemals auf dem Client verwendet werden.

Verhindern der Offenlegung sensibler Daten auf dem Client

Wir empfehlen die Verwendung der Taint-APIs von React, taintObjectReference und taintUniqueValue, um zu verhindern, dass ganze Objektinstanzen oder sensible Werte an den Client übergeben werden.

Um Tainting in Ihrer Anwendung zu aktivieren, setzen Sie die Next.js-Konfigurationsoption experimental.taint auf true:

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

Übergeben Sie dann das Objekt oder den Wert, den Sie taggen möchten, an die Funktionen experimental_taintObjectReference oder experimental_taintUniqueValue:

app/utils.ts
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Das gesamte Benutzerobjekt nicht an den Client übergeben',
    data
  )
  experimental_taintUniqueValue(
    'Die Adresse des Benutzers nicht an den Client übergeben',
    data,
    data.address
  )
  return data
}
app/utils.js
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Das gesamte Benutzerobjekt nicht an den Client übergeben',
    data
  )
  experimental_taintUniqueValue(
    'Die Adresse des Benutzers nicht an den Client übergeben',
    data,
    data.address
  )
  return data
}
app/page.tsx
TypeScript
import { getUserData } from './data'
 
export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // dies führt aufgrund von taintObjectReference zu einem Fehler
      address={userData.address} // dies führt aufgrund von taintUniqueValue zu einem Fehler
    />
  )
}