Wyszukiwarka z filtrami w Next.js — searchParams, debounce, URL state i UX
Wyszukiwarka z filtrami w Next.js, której wynik można skopiować z URL-a — jak zbudować URL state z searchParams, debounce i dobrym UX?
Maciej Sala
Founder StriveLab
5 min czytaniaOpublikowano 11 kwietnia 2026 (Aktualizacja 11 czerwca 2026)
Dlaczego URL state w Next.js zamiast useState?
Filtry w giną po odświeżeniu strony. (?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 wyszukiwarki z filtrami w Next.js
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 searchParams 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 wyszukiwarki
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 w Next.js
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.
router.replace czy router.push przy filtrach?
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 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 . 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 link kanoniczny 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.
Czy zmiana filtrów powoduje pełne przeładowanie strony?
Nie — router.push albo router.replace z { scroll: false } wykonuje miękką nawigację po stronie klienta. Server Component renderuje nową listę wyników, ale wspólny layout, nawigacja i panel filtrów nie przeładowują całego dokumentu.
Jak obsłużyć wiele tagów naraz?
Użyj powtarzalnego parametru w URL: ?tag=react&tag=nextjs. W Client Component useSearchParams().getAll("tag") zwraca tablicę, a w Server Component searchParams może mieć tag jako string albo string[] — obsłuż oba przypadki w funkcji normalizującej.
Czy Google indeksuje strony z filtrami?
Może, ale nie powinien indeksować każdej kombinacji. Dla wartościowych filtrów przygotuj stabilne, linkowane URL-e i link kanoniczny do właściwej wersji. Dla kombinacji technicznych albo niskiej jakości rozważ noindex, ograniczenie linkowania albo blokady w robots.txt, żeby nie marnować budżetu crawlowania.
Po co debounce, skoro mam useTransition?
To dwie różne warstwy. Debounce ogranicza, jak często aktualizujesz URL (np. raz na 300 ms zamiast na każdy znak) — chroni serwer przed lawiną zapytań. useTransition oznacza tę aktualizację jako niski priorytet, żeby wpisywanie w input pozostało płynne. Najlepiej działają razem.
Kiedy użyć router.replace, a kiedy router.push?
Dla wpisywania w pole wyszukiwania zwykle użyj router.replace, żeby nie zaśmiecać historii przeglądarki. Dla świadomych akcji, takich jak kliknięcie filtra, sortowanie albo paginacja, router.push ma sens, bo użytkownik może chcieć wrócić do poprzedniego widoku.
Dlaczego Suspense nie pokazuje skeletonu przy zmianie filtrów?
Bo bez zmiany key React traktuje to jako ten sam boundary i nie resetuje fallbacku. Dodaj stabilny key zależny od znormalizowanych filtrów. Pamiętaj też, że Client Components korzystające z useSearchParams powinny być opakowane w Suspense, jeśli strona ma zachować statyczną powłokę.
O autorze
Maciej Sala
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 rozwijam własne projekty.
Parametry w URL mają, jak przysłowiowa moneta ma dwie strony. Z jednej, napędzają filtry, sortowanie, paginację i śledzenie kampanii, a z drugiej, jednocześnie potrafią cicho popsuć widoczność serwisu przez duplikacje treści, marnowany budżet indeksowania i rozwodnione link equity. W aplikacjach React, Next.js i Astro to problem architektoniczny i właśnie dlatego samo użycie rel="canonical" go nie rozwiązuje. W tym przewodniku przechodzę od klasyfikacji parametrów do decyzji o indeksacji, normalizacji URL-i, renderowania, paginacji oraz kontroli nawigacji fasetowej.
Maciej Sala
Founder StriveLab
Przez lata React pchał, coraz więcej i więcej pracy do przeglądarki, w efekcie czego mamy większe bundle, hydratacja trwa dłużej, są puste loadingi i strony, które bez JavaScriptu straciły sens. App Router odwraca ten kierunek. React Server Components pozwalają renderować treść na serwerze bez wysyłania całej logiki do klienta, a Server Actions upraszczają formularze i mutacje. To ma znaczenie dla SEO, bo crawler szybciej dostaje HTML. Ma też znaczenie dla biznesu, bo użytkownik szybciej widzi i dostaje to, po co przyszedł.
Maciej Sala
Founder StriveLab
Trudno wyobrazić sobie współczesną analitykę bez Google Search Console GSC , czyli podstawowego narzędzia pokazującego dane bezpośrednio z Google Search. PageSpeed Insights mierzy wydajność, Ahrefs śledzi backlinki, ale GSC pokazuje całą paletę danych ale też problemów/błędów dotyczących strony internetowej. Przykładowo, które strony są zaindeksowane, jakie błędy crawlowania występują, na jakie frazy rankujesz i jakie wyniki Core Web Vitals Dane Core Web Vitals w GSC pochodzą z Chrome UX Report, czyli realnych wizyt użytkowników, a nie z pojedynczego testu laboratoryjnego Lighthouse. mają istniejący użytkownicy.