Data Fetching & Caching
In Server Components Daten holen: fetch mit Cache, direkte DB-Queries, revalidierbare Daten und Server Actions fuer Mutationen.
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.tsxim 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/awaitdirekt in Server Componentsfetchist erweitert:cache,next.revalidate,next.tags- Direkt auf DB zugreifen ist der moderne Weg
Promise.allfuer parallele Fetches<Suspense>+ Streaming fuer schnelle Page-Loads- Server Actions (
"use server") fuer Mutationen ohne API-Routes revalidatePath/revalidateTagnach Aenderungen- React-Query / SWR fuer Client-seitige Fetches
Im naechsten Kapitel: Deployment auf Vercel.