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:
"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:
// 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:
"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 Browserverlaufrouter.push()ändert die URL ohne client-seitigen Fetch auszulösengetFirstQ()behandelt doppelte Abfrageparameter
Vorteile dieser Architektur
- Einziges Quellen der Wahrheit: Die URL definiert den Seitenstatus
- Direkte Links:
/goods?q=phoneliefert 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:
useStateund 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
searchParamsvor dem Rendern - Client-Eingaben synchronisieren sich mit
useSearchParams() - Mehrfache Parameter mit
getAll()behandeln
— Editorial Team
Noch keine Kommentare.