Zum Inhalt springen
Next.js Fortgeschritten 30 min

Data Fetching & Caching

In Server Components Daten holen: fetch mit Cache, direkte DB-Queries, revalidierbare Daten und Server Actions fuer Mutationen.

Aktualisiert:
Inhaltsverzeichnis

Data Fetching & Caching

In klassischem React holtest du Daten mit useEffect und API-Routes. Im App Router geht es direkt in der Server Component - einfacher, schneller, mit eingebautem Caching.

Der Standard: async/await

// app/posts/page.tsx
export default async function Posts() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

Das ist alles. Die Komponente ist async, wartet auf die Daten, rendert fertig HTML.

fetch ist erweitert

Next.js erweitert fetch mit Caching-Optionen:

Immer cachen (Default)

const res = await fetch("https://api.example.com/posts");

Nie cachen (dynamisch)

const res = await fetch("https://api.example.com/posts", {
  cache: "no-store",
});

Mit Revalidierung

const res = await fetch("https://api.example.com/posts", {
  next: { revalidate: 60 }, // 60 Sekunden gueltig
});

Nach 60s wird bei naechstem Request neu geholt. Das nennt sich Incremental Static Regeneration (ISR).

Mit Tags

Fuer gezielte Invalidierung:

const res = await fetch(url, { next: { tags: ["posts"] } });

Dann in einer Server Action oder API:

import { revalidateTag } from "next/cache";

revalidateTag("posts");

Alle gecachten Requests mit Tag โ€œpostsโ€ werden invalidiert.

DB-Queries direkt

Fuer echte Apps greift man direkt auf DB zu - keine API dazwischen:

import { db } from "@/lib/db";

export default async function Dashboard() {
  const users = await db.user.findMany();
  const ordersCount = await db.order.count();

  return (
    <div>
      <h1>{users.length} Nutzer</h1>
      <p>{ordersCount} Bestellungen</p>
    </div>
  );
}

Kein fetch, keine API - direkt.

Mit unstable_cache cachen

import { unstable_cache } from "next/cache";

const getTopUsers = unstable_cache(
  async () => {
    return db.user.findMany({ orderBy: { points: "desc" }, take: 10 });
  },
  ["top-users"],
  { revalidate: 60, tags: ["users"] }
);

export default async function Page() {
  const users = await getTopUsers();
  return <UserList users={users} />;
}

Jetzt ist auch die DB-Query cached - sehr performant.

Parallel Data Fetching

Mehrere Daten parallel holen:

export default async function Page({ params }: Props) {
  const { id } = await params;

  const [user, posts, comments] = await Promise.all([
    getUser(id),
    getPosts(id),
    getComments(id),
  ]);

  return (
    <>
      <Profile user={user} />
      <Posts posts={posts} />
      <Comments comments={comments} />
    </>
  );
}

Promise.all startet alle drei gleichzeitig - deutlich schneller als sequentiell.

Streaming mit Suspense

Langsame Teile der Seite muessen nicht alles andere blockieren:

import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </>
  );
}

async function Posts() {
  const posts = await slowFetch("posts");
  return <PostList posts={posts} />;
}

async function Comments() {
  const comments = await slowFetch("comments");
  return <CommentList comments={comments} />;
}

Header kommt sofort, Posts und Comments streamen nach. Ergebnis: schneller Time-to-First-Paint.

loading.tsx vs. Suspense

  • loading.tsx im Ordner: wraps die ganze Page
  • <Suspense>: wraps einzelne Teile

In komplexeren Pages nutzt man oft beides.

Daten mutieren: Server Actions

Mit Server Actions muessen Formulare keine API-Routes mehr haben:

// app/actions.ts
"use server";

import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

export async function addTodo(formData: FormData) {
  const text = formData.get("text") as string;
  await db.todo.create({ data: { text } });
  revalidatePath("/todos");
}
// app/todos/page.tsx
import { addTodo } from "@/app/actions";
import { db } from "@/lib/db";

export default async function TodosPage() {
  const todos = await db.todo.findMany();

  return (
    <>
      <form action={addTodo}>
        <input name="text" required />
        <button>Hinzufuegen</button>
      </form>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>{t.text}</li>
        ))}
      </ul>
    </>
  );
}

Kein useState, kein fetch, kein API-Endpoint. Die "use server"-Direktive macht aus der Funktion einen RPC-Call.

Server Actions mit useFormStatus

Fuer Loading-States im Formular:

"use client";

import { useFormStatus } from "react-dom";

export function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? "Speichert..." : "Speichern"}
    </button>
  );
}

Server Actions mit Validation

Ein robustes Pattern mit zod:

"use server";

import { z } from "zod";

const schema = z.object({
  text: z.string().min(1).max(200),
});

export async function addTodo(formData: FormData) {
  const parsed = schema.safeParse({
    text: formData.get("text"),
  });

  if (!parsed.success) {
    return { error: "Ungueltige Eingabe" };
  }

  await db.todo.create({ data: parsed.data });
  revalidatePath("/todos");
}

Revalidierung

Nach Mutationen muessen Caches aktualisiert werden:

import { revalidatePath, revalidateTag } from "next/cache";

revalidatePath("/todos");       // spezifische Route
revalidatePath("/todos", "layout"); // Route + Layouts
revalidateTag("users");           // alle mit diesem Tag

redirect und notFound

In Server Components und Server Actions:

import { redirect, notFound } from "next/navigation";

export default async function Page({ params }: Props) {
  const { id } = await params;
  const post = await db.posts.findById(id);

  if (!post) notFound();
  if (!post.published) redirect("/login");

  return <Article post={post} />;
}

Client-side Data Fetching (wenn noetig)

Manchmal willst du nach dem initialen Render Daten holen - z.B. bei Suche, Pagination mit Infinite-Scroll. Fuer so was:

React-Query (TanStack Query)

"use client";

import { useQuery } from "@tanstack/react-query";

export function SearchResults({ query }: { query: string }) {
  const { data, isLoading } = useQuery({
    queryKey: ["search", query],
    queryFn: () => fetch(`/api/search?q=${query}`).then((r) => r.json()),
  });

  if (isLoading) return <div>Laedt...</div>;
  return <Results data={data} />;
}

SWR

Aehnlich, von Vercel selbst:

"use client";

import useSWR from "swr";

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

export function Profile() {
  const { data } = useSWR("/api/user", fetcher);
  if (!data) return <div>Laedt...</div>;
  return <div>Hi, {data.name}!</div>;
}

Ein kompletteres Beispiel

Eine Blog-Seite mit Suche:

// app/blog/page.tsx (Server Component)
import { db } from "@/lib/db";
import { SearchBox } from "@/components/SearchBox";

export default async function BlogPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = await searchParams;

  const posts = await db.post.findMany({
    where: q ? { title: { contains: q, mode: "insensitive" } } : undefined,
    orderBy: { createdAt: "desc" },
    take: 20,
  });

  return (
    <main>
      <SearchBox defaultValue={q ?? ""} />
      <ul>
        {posts.map((p) => (
          <li key={p.id}>
            <a href={`/blog/${p.slug}`}>{p.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}
// components/SearchBox.tsx (Client Component)
"use client";

import { useRouter } from "next/navigation";

export function SearchBox({ defaultValue }: { defaultValue: string }) {
  const router = useRouter();

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const q = new FormData(e.currentTarget).get("q");
    router.push(`/blog?q=${q}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" defaultValue={defaultValue} placeholder="Suche..." />
      <button>Suchen</button>
    </form>
  );
}

Die Suche laeuft auf Server-Seite, die URL enthaelt die Query - bookmarkbar und shareable.

Zusammenfassung

  • async/await direkt in Server Components
  • fetch ist erweitert: cache, next.revalidate, next.tags
  • Direkt auf DB zugreifen ist der moderne Weg
  • Promise.all fuer parallele Fetches
  • <Suspense> + Streaming fuer schnelle Page-Loads
  • Server Actions ("use server") fuer Mutationen ohne API-Routes
  • revalidatePath / revalidateTag nach Aenderungen
  • React-Query / SWR fuer Client-seitige Fetches

Im naechsten Kapitel: Deployment auf Vercel.

Zurรผck zum Next.js Kurs