blog_thumbnail_filterbare_liste_nextjs

Blog | Filterbare Listen mit Next.js: Server Components & Suspense

Von 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.