Zurück zur Startseite

URL als Quelle des Zustands im Next.js App Router

Im Next.js App Router wird die URL zur einzigen Quelle des Seiten-Zustands. Server-Komponenten lesen searchParams und gestalten den Inhalt vor dem Rendering. Client-Form synchronisiert mit der Browser-Historie ohne extra useEffect.

URL steuert Daten im Next.js App Router
Advertisement 728x90

URL als einziges Quellen der Wahrheit im Next.js App Router

Im Next.js App Router wird die URL zur endgültigen Quelle für den Seitenstatus. Anstatt den lokalen useState mit der Adressleiste abzugleichen, liest die serverseitig gerenderte Seite direkt searchParams aus und generiert sofort Inhalte. Dies beseitigt Inkonsistenzen zwischen Eingabefeldern, Daten und Navigation.

Für Produkt-Suchanfragen funktioniert das so: Die URL enthält ?q=phone → der Server extrahiert den Parameter → ruft eine API auf → rendert eine sofort anzeigbare Liste. Zurück/Weiter-Navigation und direkte Links verhalten sich vorhersehbar.

Häufige Fehler bei client-seitigen Ansätzen

Traditioneller React-Code mit useEffect erzeugt mehrere Quellen der Wahrheit:

Google AdInline article slot
"use client";

import { useEffect, useState } from "react";

export default function GoodsPage() {
  const [q, setQ] = useState("");
  const [items, setItems] = useState([]);

  useEffect(() => {
    async function load() {
      const res = await fetch(`/api/goods?q=${encodeURIComponent(q)}`);
      const data = await res.json();
      setItems(data.products);
    }

    load();
  }, [q]);

  return (
    <>
      <input value={q} onChange={e => setQ(e.target.value)} />
      <div>{items.map(item => <div key={item.id}>{item.title}</div>)}</div>
    </>
  );
}

Folgen:

  • Direkte Links stellen den Zustand nicht wieder her
  • Seiteneinladungen setzen die Daten zurück
  • Zurück/Weiter bricht die Benutzererfahrung
  • Manuelle Synchronisation zwischen Eingabe und URL ist erforderlich

Server-seitiges Modell: Daten vor dem Rendern

Server-Komponenten lesen searchParams auf dem Server und holen Daten vor der Hydratisierung:

  • Die URL speichert Filterparameter
  • Der Server analysiert searchParams
  • Eine Abfrage mit Caching läuft
  • UI rendert aus vorgeladenen Props

Eine universelle Datenschicht für Produkte:

Google AdInline article slot
// src/app/_data/dummyjson.js
const API_BASE = "https://dummyjson.com";

async function fetchJson(url, fetchOptions = {}) {
  const res = await fetch(url, fetchOptions);

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    const err = new Error(`DummyJSON error: ${res.status} ${res.statusText}. ${text}`);
    err.status = res.status;
    throw err;
  }

  return res.json();
}

export async function getProducts({ q = "", limit = 12, skip = 0 } = {}) {
  const safeQ = String(q).trim();
  const qs = new URLSearchParams({
    limit: String(limit),
    skip: String(skip),
  });

  const url = safeQ
    ? `${API_BASE}/products/search?${qs.toString()}&q=${encodeURIComponent(safeQ)}`
    : `${API_BASE}/products?${qs.toString()}`;

  return fetchJson(url, {
    next: { revalidate: 60 },
  });
}

Die Suchlogik ist isoliert – Seitenkomponenten bleiben sauber und fokussiert.

Implementierung der serverseitigen Seite

// src/app/(app)/goods/page.js
import Link from "next/link";
import { getProducts } from "@/app/_data/goodsApi";
import GoodsSearchBar from "@/app/_ui/GoodsSearchBar";
import GoodsGridMotionClient from "@/app/_ui/GoodsGridMotionClient";

export default async function GoodsPage({ searchParams }) {
  const sp = await searchParams;
  const q = typeof sp?.q === "string" ? sp.q : "";

  const data = await getProducts({ q, limit: 12, skip: 0 });

  const isEmpty = !data?.products?.length;
  const hasFilter = !!String(q || "").trim();

  return (
    <div>
      <h1>Produkte</h1>
      <GoodsSearchBar initialQuery={q} />

      {isEmpty ? (
        <div>
          <h2>Keine Ergebnisse gefunden</h2>

          {hasFilter ? (
            <p>Es gibt keine Produkte zu "{q}"</p>
          ) : (
            <p>Die Liste ist leer</p>
          )}

          <Link href="/goods">Alle Produkte anzeigen</Link>
        </div>
      ) : (
        <GoodsGridMotionClient products={data.products} />
      )}
    </div>
  );
}

Die URL bestimmt vollständig den Inhalt der Seite.

Client-seitige Suche mit URL-Synchronisation

Das Eingabefeld bleibt client-seitig, aktualisiert aber nur die URL nach dem Absenden:

Google AdInline article slot
"use client";

import { useEffect, useMemo, useState } from "react";

import { useRouter, useSearchParams } from "next/navigation";

function getFirstQ(sp) {
  const all = sp.getAll("q");
  return all[0] ?? "";
}

export default function GoodsSearchBar({ basePath = "/goods" }) {
  const router = useRouter();
  const sp = useSearchParams();

  const qFromUrl = getFirstQ(sp);
  const [value, setValue] = useState(qFromUrl);

  useEffect(() => {
    setValue(qFromUrl);
  }, [qFromUrl]);

  function buildHref(nextQ) {
    const q = nextQ.trim();
    return q ? `${basePath}?q=${encodeURIComponent(q)}` : basePath;
  }

  function onSubmit(e) {
    e.preventDefault();
    router.push(buildHref(value));
  }

  function onReset() {
    router.push(basePath);
  }

  const canSearch = useMemo(() => value.trim().length > 0, [value]);

  return (
    <form onSubmit={onSubmit}>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="z. B. Handy"
      />

      <button type="submit" disabled={!canSearch}>
        Suchen
      </button>

      <button type="button" onClick={onReset}>
        Zurücksetzen
      </button>
    </form>
  );
}

Wichtige Punkte:

  • useSearchParams() synchronisiert das Eingabefeld mit dem Browserverlauf
  • router.push() ändert die URL ohne client-seitigen Fetch auszulösen
  • getFirstQ() behandelt doppelte Abfrageparameter

Vorteile dieser Architektur

  • Einziges Quellen der Wahrheit: Die URL definiert den Seitenstatus
  • Direkte Links: /goods?q=phone liefert immer dieselben Ergebnisse
  • Navigation: Zurück/Weiter stellt den genauen Zustand wieder her
  • Server-Rendering: Daten werden vor der Hydratisierung geladen
  • Debugging: Der Zustand ist in der URL sichtbar

Wann URL-Parameter vermeiden sollte

Gute Kandidaten für URL-Parameter:

  • Suchbegriffe
  • Sortierung und Filter
  • Pagination
  • Ansichtsmodi

Besser im client-seitigen Zustand belassen:

  • Dropdowns und Modals
  • Animationen
  • Temporäre Eingaben
  • Hover- oder Fokus-Zustände

Drei kritische Fehler, die vermieden werden sollten

  • Doppelter Zustand ohne Synchronisation: useState und URL mischen, ohne Koordination
  • Client-seitiger Fetch innerhalb von Server-Komponenten über useEffect
  • Eingabe als primäre Quelle, statt URL nach Formularabschluss

Was am wichtigsten ist:

  • Die URL sollte im App Router die Seiten-Daten definieren
  • Server-Komponenten lesen searchParams vor dem Rendern
  • Client-Eingaben synchronisieren sich mit useSearchParams()
  • Mehrfache Parameter mit getAll() behandeln

— Editorial Team

Advertisement 728x90

Weiterlesen