Blog | Filterbare Listen mit Next.js: Server Components & Suspense
Von David Grünberger am 30.09.2025
Filterbare Listen gehören zu den häufigsten Features moderner Webanwendungen: ob es um Produkte in einem Online-Shop, Veranstaltungen in einem Kalender oder Mitglieder einer Organisation geht – Nutzer:innen wollen Inhalte nach bestimmten Kriterien durchsuchen und einschränken können.
In diesem Artikel zeige ich, wie man mit Next.js (App Router), React Server Components und Suspense eine performante, filterbare Liste baut. Dabei erkläre ich nicht nur den Code, sondern auch die zugrunde liegenden Konzepte.
1. React Server Components (RSC) – kurz erklärt
Mit dem App Router hat Next.js Server Components eingeführt.
Das bedeutet: Komponenten können auf dem Server gerendert werden, bevor HTML an den Browser geschickt wird.
Vorteile:
- Weniger JavaScript im Client → bessere Performance.
- Direkter Zugriff auf Datenbanken/Server-APIs (kein API-Layer nötig).
- SEO-freundlich, da Inhalte direkt im HTML stehen.
Für unsere filterbare Liste bedeutet das:
Wir können die gefilterten Daten direkt im Server Component holen und rendern, ohne zusätzliche API-Endpunkte schreiben zu müssen.
2. Suspense – was ist das?
React Suspense erlaubt es, Ladezustände elegant zu handhaben.
Anstatt im Code manuell isLoading-Zustände zu bauen, können wir Teile des UI in <Suspense fallback={...}> hüllen.
So wird z. B. eine Liste erst angezeigt, wenn die Daten geladen sind, und bis dahin zeigt React automatisch eine Fallback Komponente an (z. B. „Loading…“).
In Next.js ist Suspense schon integriert – wir müssen es nur nutzen.
3. Datenmodell vorbereiten
Als Beispiel nutzen wir Prisma ORM mit einer PostgreSQL-Datenbank. Unser Modell könnte so aussehen:
// prisma/schema.prisma
model Event {
id String @id @default(cuid())
title String
location String
date DateTime
status String // e.g. "open", "closed"
}
Wir haben also eine Liste von Events mit Titel, Ort, Datum und Status.
4. Eine einfache Liste rendern
Zuerst bauen wir eine Seite, die alle Events anzeigt:
Eine Server Component rendert direkt die Liste. Vorteil: Sie läuft auf dem Server, hat Zugriff auf Prisma und schickt nur HTML an den Client.
// app/events/page.tsx
import { prisma } from "@/lib/db";
export default async function EventsPage() {
const events = await prisma.event.findMany();
return (
<div>
<h1 className="text-2xl font-bold mb-4">Events</h1>
<ul className="space-y-2">
{events.map((e) => (
<li key={e.id} className="p-3 border rounded-lg">
<div className="font-semibold">{e.title}</div>
<div>{e.location} – {e.date.toDateString()}</div>
<div>Status: {e.status}</div>
</li>
))}
</ul>
</div>
);
}
Das ist schon SSR out of the box, ohne zusätzliche API.
5. Filter via Search Params
Jetzt machen wir die Liste filterbar, indem wir searchParams nutzen.
Ein Beispielaufruf:
/events?status=open&location=StPoelten
Somit kann auch über die URL im Browser ein Filter für die Liste definiert werden.
// app/events/page.tsx
import { prisma } from "@/lib/db";
export default async function EventsPage({
searchParams,
}: {
searchParams: { status?: string; location?: string };
}) {
const { status, location } = searchParams;
const events = await prisma.event.findMany({
where: {
status: status || undefined,
location: location || undefined,
},
});
return (
<div>
<h1 className="text-2xl font-bold mb-4">Events</h1>
<Filters />
<EventList events={events} />
</div>
);
}
6. Eine Client Komponente für die Filter-UI
Interaktive Filter brauchen Client-Side Hooks (useRouter, useSearchParams). Darum trennen wir UI (Client) von Datenfetching (Server).
// app/events/Filters.tsx
"use client";
import { useRouter } from "next/navigation";
export function Filters() {
const router = useRouter();
return (
<div className="mb-4 flex gap-2">
<button
className="px-3 py-1 border rounded"
onClick={() => router.push("/events?status=open")}
>
Open
</button>
<button
className="px-3 py-1 border rounded"
onClick={() => router.push("/events?status=closed")}
>
Closed
</button>
<button
className="px-3 py-1 border rounded"
onClick={() => router.push("/events")}
>
All
</button>
</div>
);
}
Die Page-Component bleibt Server-seitig, nur die UI für die Filter ist Client-seitig.
7. Ladezustände mit Suspense
Damit beim Filterwechsel nicht plötzlich ein „Leeres UI“ flackert, setzen wir eine Suspense Boundary.
Während dem Ladevorgang wird somit Loading events... angezeigt. Dies soll nur der Veranschaulichung dienen – in modernen Webanwendungen würde man hier z.B. eine Skeleton-Komponente anzeigen.
// app/events/page.tsx
import { Suspense } from "react";
import { EventList } from "./EventList";
import { Filters } from "./Filters";
export default function EventsPage({ searchParams }: { searchParams: any }) {
return (
<div>
<h1 className="text-2xl font-bold mb-4">Events</h1>
<Filters />
<Suspense fallback={<p>Loading events...</p>}>
<EventList searchParams={searchParams} />
</Suspense>
</div>
);
}
Die EventList selbst bleibt ein Server Component:
// app/events/EventList.tsx
import { prisma } from "@/lib/db";
export async function EventList({ searchParams }: { searchParams: any }) {
const { status, location } = searchParams;
const events = await prisma.event.findMany({
where: {
status: status || undefined,
location: location || undefined,
},
});
return (
<ul className="space-y-2">
{events.map((e) => (
<li key={e.id} className="p-3 border rounded-lg">
<div className="font-semibold">{e.title}</div>
<div>{e.location} – {e.date.toDateString()}</div>
<div>Status: {e.status}</div>
</li>
))}
</ul>
);
}
8. Fazit
Filterbare Listen sind ein zentrales Feature in fast jeder Web-App. Mit Next.js App Router, Server Components und Suspense können wir sie:
- performant (wenig Client-JS, schnelle Server-Abfragen),
- einfach (keine extra API nötig),
- userfreundlich (Suspense für Ladezustände)
umsetzen.
Das Prinzip lässt sich auf viele Bereiche anwenden – ob Produktkataloge, Reisebuchungen oder Mitgliederlisten.
The comments are closed.