Filtry w useState to hook React do trzymania stanu komponentu w pamięci — stan ginie przy odświeżeniu strony i nie da się go udostępnić linkiem. giną po odświeżeniu strony. URL state to wzorzec trzymania stanu aplikacji (np. filtrów) w parametrach adresu URL — przetrwa odświeżenie, udostępnienie linku i nawigację wstecz/wprzód. (?q=next&tag=react&tag=seo&sort=newest) przetrwa: odświeżenie, udostępnienie linku, nawigację wstecz/wprzód i jest czytelny dla Server Components bez klientowego JS do odczytania filtrów.
Architektura
Code
URL: /blog?q=next&tag=react&sort=newest
↓
Server Component → czyta searchParams → pobiera dane z filtrami
↓
Client Component → aktualizuje URL → Server Component re-renderuje
Server Component — odczyt filtrów i pobieranie danych
Prosty trik: key={resultsKey} na <Suspense> — zmiana filtrów = nowy key = nowy skeleton = nowe pobranie danych. Klucz buduj ze znormalizowanych filtrów, a nie z surowego obiektu searchParams, bo kolejność parametrów i puste wartości nie powinny tworzyć sztucznych stanów.
Drugi detal jest ważny przy buildzie produkcyjnym: komponenty klienckie, które używają useSearchParams, opakuj w <Suspense>. W trybie deweloperskim może działać bez tego, ale przy statycznie renderowanej stronie Next.js wymaga granicy Suspense albo przeniesie większą część drzewa w client-side rendering.
Walidacja parametrów w URL
Nie traktuj searchParams jak zaufanego stanu aplikacji. To część URL-a, więc użytkownik może wpisać tam cokolwiek: ?page=-100, ?sort=DROP_TABLE, ?tag= albo bardzo długie q.
Minimalny zestaw zasad:
trimuj tekst wyszukiwania i ustaw minimalną długość, np. 2 znaki,
whitelistuj sortowanie, zamiast przekazywać dowolny string do zapytania,
normalizuj multi-select, bo tag może być stringiem, tablicą albo pustą wartością,
pilnuj paginacji, czyli minimum 1 i rozsądny maksymalny perPage,
nie buduj router.push z niezweryfikowanego URL-a, tylko z pathname i URLSearchParams.
Dzięki temu URL pozostaje udostępnialny, ale nie staje się drugim, niekontrolowanym magazynem stanu aplikacji.
Pole wyszukiwania z debounce
Debounce to technika opóźniania reakcji do momentu, aż użytkownik przestanie działać — np. aktualizacja po 300 ms od ostatniego wciśniętego znaku, zamiast po każdym. chroni serwer przed zapytaniem na każdy
wpisany znak.
useTransition oznacza aktualizację URL jako niski priorytet, więc wpisywanie w pole jest mniej podatne na lagi przy wolniejszej nawigacji. router.replace jest tu lepszy niż router.push, bo debounce może odpalić kilka zmian podczas jednego wpisywania — użytkownik nie chce potem klikać „wstecz” przez każdy pośredni stan. Nadal pilnuj kosztu zapytań i nie odpalaj ciężkiego wyszukiwania pełnotekstowego na każdy znak bez debounce.
replace czy push?
Praktyczna reguła jest prosta:
Interakcja
Metoda
Dlaczego
Wpisywanie w pole wyszukiwania
router.replace()
Nie zaśmieca historii stanami pośrednimi
Kliknięcie filtra
router.push()
To świadoma zmiana widoku, do której można wrócić
Zmiana sortowania
router.push()
Użytkownik często porównuje warianty
Paginacja
router.push()
Strony wyników powinny działać z przyciskiem „wstecz”
Zmiana czysto wizualna, bez pobrania listy
History API
Nie musi ponownie renderować Server Component
Jeśli chcesz zmienić tylko lokalny stan URL-a i nie potrzebujesz nowego renderu Server Component, Next.js wspiera natywne window.history.pushState i window.history.replaceState, które współpracują z usePathname i useSearchParams. Do filtrowania wyników po stronie serwera zwykle jednak chcesz normalną nawigację przez router.
Wyszukiwarka z filtrami jest elementem interaktywnym, więc sam działający URL nie wystarczy. Zadbaj o kilka drobiazgów:
input[type="search"] powinien mieć aria-label, jeśli placeholder jest jedynym opisem,
przyciski filtrów powinny mieć aria-pressed, bo zachowują się jak przełączniki,
przycisk usuwania etykiety powinien mieć aria-label, np. Usuń filtr React,
spinner powinien mieć tekst dla czytników ekranu, np. sr-only,
nie kasuj scrolla przy każdej zmianie filtrów, jeśli użytkownik pracuje w środku listy,
pokazuj pusty stan z informacją, które filtry można usunąć.
Największy błąd UX to traktowanie filtrów jak formularza, który „magicznie” zmienia wyniki bez sygnału zwrotnego. Użytkownik powinien widzieć, że wyniki są aktualizowane, które filtry są aktywne i jak wrócić do szerszego zestawu.
Filtry w URL są świetne dla użytkownika, ale mogą być kosztowne dla SEO. Jeśli każdy parametr tworzy nowy indeksowalny adres, łatwo wygenerować tysiące podobnych stron:
Nie każda kombinacja zasługuje na indeks. W praktyce podziel filtrowane URL-e na trzy grupy:
Typ URL-a
Decyzja SEO
Główne kategorie i ważne tagi
indeksuj, linkuj wewnętrznie, daj własny title
Sortowanie, wyszukiwanie tekstowe, paginacja
zwykle canonical do głównej wersji albo noindex
Losowe kombinacje wielu filtrów o małym popycie
ogranicz crawling lub linkowanie
canonical jest dobrym sygnałem, ale nie jest magicznym przełącznikiem crawl budgetu. Przy dużych katalogach rozważ też noindex, ograniczenie linków do kombinacji filtrów i reguły w robots.txt dla parametrów, których nie chcesz crawlowanych. Google w dokumentacji faceted navigation zwraca uwagę, że parametry filtrów mogą tworzyć bardzo dużą przestrzeń URL-i i spowalniać odkrywanie ważniejszych stron.
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.