warum SearchParams oft die bessere Wahl sind
Filter, Sortierung, Pagination und aktive Tabs landen in vielen React Anwendungen in useState, obwohl sie eigentlich Teil der Adresse sind. Wer diesen Zustand stattdessen in der URL speichert, bekommt Deep Linking, funktionierenden Browser Zurück Button und teilbare Links geschenkt, ganz ohne zusätzlichen Store.
Inhaltsverzeichnis
- 1. Warum URL Zustand oft besser ist als useState
- 2. Native SearchParams mit useSearchParams
- 3. nuqs: typsichere Query Parameter ohne Boilerplate
- 4. Browser History: push versus replace richtig einsetzen
- 5. Mehrere unabhängige Zustände in einer URL kombinieren
- 6. Server Side Rendering und URL Zustand
- 7. Grenzen des Patterns: was nicht in die URL gehört
- 8. URL Zustand mit Client und Server State kombinieren
- 9. URL Zustand im Vergleich zu useState und Context
- 10. Zusammenfassung
- 11. FAQ
1. Warum URL Zustand oft besser ist als useState
Eine Produktliste mit Filtern für Kategorie, Preisbereich und Sortierung wird in vielen React Anwendungen mit useState umgesetzt. Das funktioniert auf den ersten Blick, hat aber einen entscheidenden Nachteil: Der Zustand existiert nur im Arbeitsspeicher der aktuellen Seite. Ein Neuladen der Seite setzt die Filter zurück, ein geteilter Link zeigt beim Empfänger die ungefilterte Liste, und der Browser Zurück Button springt nicht zwischen Filterzuständen, sondern verlässt direkt die Seite. Das URL als State Pattern löst all diese Probleme, indem Filterzustand, Pagination und aktive Tabs direkt in die Adresse geschrieben werden.
Die URL ist im Web seit jeher der native Ort für adressierbaren Zustand, genau dafür wurden Query Parameter ursprünglich erfunden. Wird Zustand konsequent in SearchParams statt in useState gehalten, wird jede Kombination aus Filter, Sortierung und Seite automatisch zu einer eigenen, teilbaren, bookmarkbaren Adresse. Für SEO relevante Listenseiten ist das URL als State Pattern zudem oft die einzige Möglichkeit, überhaupt indexierbare, eindeutige URLs pro Filterkombination zu erzeugen.
Der Umstieg auf URL Zustand bedeutet nicht, dass useState komplett verschwindet. Rein visuelle, kurzlebige Interaktionszustände wie ein geöffnetes Dropdown Menü gehören weiterhin in lokalen Component State. Die Faustregel lautet: Alles, was ein Nutzer als Teil der aktuell angezeigten Ansicht verstehen und mit anderen teilen würde, gehört in die URL, alles rein Interne bleibt in useState.
2. Native SearchParams mit useSearchParams
React Router und Next.js bieten beide einen useSearchParams Hook, der die aktuellen Query Parameter als URLSearchParams Objekt zurückgibt und eine Setter Funktion zum Aktualisieren bereitstellt. Anders als bei useState triggert ein Update hier eine tatsächliche Navigation, wodurch die Adresszeile des Browsers synchron zum angezeigten Zustand bleibt, ohne dass Entwickler manuell window.history manipulieren müssen.
Der native Ansatz erfordert allerdings manuelles Parsen und Serialisieren: Ein numerischer Filter kommt als String aus der URL zurück, ein Array von ausgewählten Kategorien muss selbst kodiert und dekodiert werden, und ein fehlender Parameter muss explizit mit einem Default Wert abgefangen werden. Für einfache Fälle mit ein bis zwei Parametern ist das gut handhabbar, bei komplexeren Filterformularen mit mehreren Typen wird der Boilerplate schnell unübersichtlich.
// components/ProductFilters.jsx — native useSearchParams, manual parsing required
import { useSearchParams } from 'react-router-dom';
function ProductFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all';
const page = Number(searchParams.get('page') ?? '1'); // manual parsing to number
function setCategory(value) {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set('category', value);
next.set('page', '1'); // reset pagination when the filter changes
return next;
});
}
return (
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="all">All categories</option>
<option value="shoes">Shoes</option>
<option value="bags">Bags</option>
</select>
);
}
3. nuqs: typsichere Query Parameter ohne Boilerplate
nuqs ist eine spezialisierte Bibliothek, die genau dieses Parsen und Serialisieren übernimmt und dabei eine API bietet, die sich fast identisch zu useState anfühlt. Statt manuell mit URLSearchParams zu arbeiten, definiert useQueryState einen Parameter mit einem Parser, etwa parseAsInteger oder parseAsArrayOf, und liefert direkt den typisierten Wert samt Setter Funktion zurück, exakt wie ein normaler React State Hook.
Ein zentrales Feature von nuqs ist die eingebaute Debounce und Throttle Unterstützung für die URL Aktualisierung: Bei einem Live Suchfeld, das bei jedem Tastendruck die URL ändern würde, lässt sich über die Option shallow und einen konfigurierbaren Timeout verhindern, dass bei jedem Zeichen ein neuer History Eintrag entsteht. Für Next.js und React Router bringt nuqs jeweils eigene Adapter mit, die sich um die Integration mit dem jeweiligen Routing System kümmern.
// components/ProductFilters.jsx — nuqs feels like useState but syncs to the URL
import { useQueryState, parseAsInteger, parseAsStringEnum } from 'nuqs';
function ProductFilters() {
const [category, setCategory] = useQueryState(
'category',
parseAsStringEnum(['all', 'shoes', 'bags']).withDefault('all')
);
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
function handleCategoryChange(value) {
setCategory(value);
setPage(1); // reset pagination alongside the filter change
}
return (
<select value={category} onChange={(e) => handleCategoryChange(e.target.value)}>
<option value="all">All categories</option>
<option value="shoes">Shoes</option>
<option value="bags">Bags</option>
</select>
);
}
4. Browser History: push versus replace richtig einsetzen
Ein häufig übersehenes Detail beim URL als State Pattern ist die Wahl zwischen pushState und replaceState. Jede Filteränderung mit push in die History zu schreiben führt dazu, dass der Zurück Button des Browsers durch jede einzelne Filteränderung navigiert, statt zur vorherigen Seite zurückzukehren, was bei Live Suchfeldern mit Debounce schnell zu einem frustrierenden Dutzend History Einträgen für eine einzige Eingabe führt.
Die pragmatische Regel: Bewusste, diskrete Nutzeraktionen wie das Wechseln einer Kategorie oder das Umschalten einer Sortierung rechtfertigen einen push Eintrag, weil der Nutzer plausibel dorthin zurückkehren möchte. Kontinuierliche Eingaben wie Tippen in ein Suchfeld oder das Ziehen eines Preisreglers sollten stattdessen mit replace aktualisiert werden, sodass die History nicht mit Zwischenzuständen überflutet wird. Sowohl die native History API als auch nuqs unterstützen diese Unterscheidung über eine explizite Options Angabe pro Update.
5. Mehrere unabhängige Zustände in einer URL kombinieren
Sobald eine Seite mehrere unabhängige URL Zustände gleichzeitig verwaltet, etwa Filter, Sortierung, Pagination und einen aktiven Tab, wird die Koordination zwischen diesen Parametern wichtig. nuqs bietet dafür useQueryStates, mit dem sich mehrere zusammengehörige Parameter als ein Objekt lesen und in einem einzigen atomaren Update schreiben lassen, statt mit mehreren separaten Aufrufen mehrere History Einträge oder Race Conditions zu riskieren.
Ein praktisches Beispiel: Ändert sich die Kategorie, muss die Pagination gleichzeitig auf Seite eins zurückgesetzt werden. Werden beide Parameter mit zwei separaten Setter Aufrufen aktualisiert, kann es je nach Batching Verhalten zu zwei getrennten Navigationen kommen. Mit useQueryStates lässt sich das gesamte Update als ein Objekt in einem einzigen Aufruf durchführen, wodurch nur eine einzige URL Änderung und ein einziger History Eintrag entstehen.
// components/ProductListState.jsx — coordinate multiple related query params atomically
import { useQueryStates, parseAsInteger, parseAsStringEnum } from 'nuqs';
function useProductListState() {
return useQueryStates({
category: parseAsStringEnum(['all', 'shoes', 'bags']).withDefault('all'),
sort: parseAsStringEnum(['price', 'newest']).withDefault('newest'),
page: parseAsInteger.withDefault(1),
});
}
function ProductList() {
const [{ category, sort, page }, setState] = useProductListState();
function changeCategory(value) {
// Single atomic update — one navigation, one history entry
setState({ category: value, page: 1 });
}
return <p>{category} / {sort} / page {page}</p>;
}
6. Server Side Rendering und URL Zustand
Ein wesentlicher Vorteil von URL Zustand gegenüber Client seitigem State zeigt sich bei Server Side Rendering: Weil die Query Parameter bereits im initialen Request an den Server verfügbar sind, kann ein Next.js Server Component den korrekten, gefilterten Datensatz direkt beim ersten Rendern laden, ohne auf einen Client seitigen Effekt und einen zweiten Request warten zu müssen. Das verbessert sowohl Time to Content als auch die Konsistenz zwischen Server und Client gerenderten Inhalten erheblich.
In der App Router Architektur von Next.js werden SearchParams als searchParams Prop direkt an Server Components durchgereicht, wodurch sich Filterlogik teilweise komplett auf den Server verlagern lässt, während der Client lediglich für die interaktive Aktualisierung der URL zuständig bleibt. Diese Kombination aus serverseitigem initialem Rendering und clientseitiger URL Manipulation ist einer der Hauptgründe, warum das URL als State Pattern in modernen React Frameworks so gut funktioniert.
7. Grenzen des Patterns: was nicht in die URL gehört
Nicht jeder Zustand gehört in die URL. Sensible Daten wie Authentifizierungstoken oder personenbezogene Informationen dürfen niemals als Query Parameter erscheinen, weil URLs in Browser Historien, Server Logs und Analytics Tools landen und damit faktisch nicht vertraulich sind. Ebenso ungeeignet sind sehr große Datenmengen, etwa der komplette Inhalt eines mehrstufigen Formulars, weil Browser und Server praktische Längenbeschränkungen für URLs durchsetzen, üblicherweise irgendwo zwischen zwei und acht Kilobyte je nach Browser und Server Konfiguration.
Auch rein transiente UI Zustände wie die Animation eines Modals oder der Fokus Zustand eines Eingabefelds sollten nicht in die URL wandern, weil sie für den Nutzer keinen adressierbaren Wert haben und die URL unnötig mit technischem Rauschen füllen würden. Die Trennlinie verläuft klar entlang der Frage, ob ein Zustand Teil dessen ist, was ein Nutzer als aktuelle Ansicht der Anwendung versteht und mit einem Link teilen würde.
8. URL Zustand mit Client und Server State kombinieren
In der Praxis existiert URL Zustand fast nie isoliert, sondern als ein Baustein neben Server State über TanStack Query und gelegentlichem Client State über Zustand oder Context. Ein typisches Muster: Die aktuellen Filterwerte kommen aus der URL über nuqs, werden als Query Key an useQuery übergeben, und TanStack Query übernimmt Caching und Deduplizierung für die tatsächlich geladenen Produktdaten.
Diese Kombination sorgt dafür, dass ein Wechsel der Filter automatisch einen neuen, korrekt gecachten Query auslöst, während der Browser Zurück Button zuverlässig zum vorherigen Filterzustand samt zugehörigem, bereits gecachtem Ergebnis zurückspringt. Die URL fungiert dabei als einzige Quelle der Wahrheit für den aktuellen Filterzustand, TanStack Query kümmert sich ausschließlich um die zugehörigen Serverdaten.
9. URL Zustand im Vergleich zu useState und Context
Die folgende Tabelle zeigt, für welche Art von Zustand die URL die passende Wahl ist, im Vergleich zu klassischem Component State und Context.
| Kriterium | useState | Context | URL / SearchParams |
|---|---|---|---|
| Überlebt Seiten Reload | Nein | Nein | Ja, immer |
| Teilbar per Link | Nein | Nein | Ja, nativ |
| Browser Zurück funktioniert | Nein | Nein | Ja, über History |
| Eignung für sensible Daten | Geeignet | Geeignet | Ungeeignet |
| Server Component Zugriff (SSR) | Nicht möglich | Nicht möglich | Direkt über searchParams Prop |
Mironsoft
React Architektur, State Management und moderne Frontend Infrastruktur
Filter, die nach jedem Reload verschwinden?
Wir bringen Filter, Pagination und Tab Zustände dorthin, wo sie hingehören, in die URL, mit nuqs, sauberer History Behandlung und direkter Server Component Integration für Next.js.
Filter Migration
Bestehende useState Filter auf SearchParams und nuqs umstellen
SSR Integration
Server Components direkt mit searchParams für schnelleres First Paint versorgen
TanStack Query Kopplung
URL Zustand als Query Key für konsistentes Caching der Serverdaten nutzen
10. Zusammenfassung
Das URL als State Pattern verlagert Filter, Sortierung, Pagination und Tab Zustände von useState in die Adresszeile, wodurch Deep Linking, ein funktionierender Browser Zurück Button und teilbare Links automatisch entstehen. Native useSearchParams Hooks funktionieren für einfache Fälle, erfordern aber manuelles Parsen und Serialisieren. nuqs übernimmt diese Arbeit mit einer useState ähnlichen API, typsicheren Parsern und eingebauter Unterstützung für Debouncing sowie die Koordination mehrerer zusammengehöriger Parameter.
Nicht jeder Zustand gehört in die URL: Sensible Daten, sehr große Datenmengen und rein transiente UI Zustände bleiben besser in useState oder einem Client Store. Kombiniert mit Server Side Rendering und TanStack Query für die zugehörigen Serverdaten wird die URL zur einzigen verlässlichen Quelle der Wahrheit für alles, was ein Nutzer als aktuelle, adressierbare Ansicht der Anwendung verstehen und teilen würde.
URL als State: Das Wichtigste auf einen Blick
Wann URL Zustand nutzen
Für Filter, Sortierung, Pagination und Tabs, die geteilt, gebookmarkt und über Reload hinweg erhalten bleiben sollen.
nuqs statt useSearchParams
Typsichere Parser, useState ähnliche API und automatische Serialisierung ersetzen manuelles URLSearchParams Handling.
push versus replace
Diskrete Aktionen mit push, kontinuierliche Eingaben mit replace, um die Browser History nicht zu überfluten.
Grenzen des Patterns
Sensible Daten, große Datenmengen und rein transiente UI Zustände gehören nicht in die URL.