[ BLOG // 2026-07-22 // 3 MIN ]

Admin filtry bez tlačítka Filtrovat — URL jako stav

Proč jsme v admin panelu J+P Autodíly vyhodili tlačítko Filtrovat a nechali filtry zapisovat přímo do URL. Debounce, server-rendered seznamy a progressive enhancement v jedné sdílené komponentě.

PROJEKTY nextjsuxautodilyadminurl-state

Tlačítko „Filtrovat” v admin panelu je relikt. Pochází z doby, kdy formulář odesílal POST request na server a stránka se překreslila celá. V roce 2026, kdy máme server components a URL jako zdroj pravdy, je to zbytečný krok navíc — admin vybere filtr, pak musí zmáčknout tlačítko, pak čeká. Tři kroky místo jednoho.

V admin panelu J+P Autodíly jsme to vyřešili radikálně: žádné tlačítko Filtrovat není. Filtry se aplikují okamžitě.

URL je stav

Celý princip je jednoduchý: hodnota filtru se zapíše do query stringu a server-renderovaný seznam se překreslí sám. Žádný client-side stav, žádný useState s filtrem, žádný useEffect co hlídá změny. URL je single source of truth.

/admin/customers?q=novák&n=2
/admin/documents?customer=42&from=2026-07-01
/admin/audit?action=login&entity=customer

Když admin změní filtr, změní se URL. Když se změní URL, server vyrenderuje nový seznam. Když admin pošle odkaz kolegovi, kolega vidí přesně ten samý pohled. Když zmáčkne F5, filtr zůstává. Žádné překvapení.

Debounce u textového pole

U selectů a datumů je to přímočaré — změna hodnoty = okamžitý push do URL. U textového vyhledávání to tak jednoduché není, protože admin píše znak po znaku a každý znak by znamenal request.

Řešení je debounce 300 ms. Komponenta si drží lokální stav toho, co admin píše, a do URL pushuje až když se na 300 ms nic nezmění. Výsledek: tabulka reaguje na psaní, ale ne na každý stisk klávesy.

Admin píše: "n" → "no" → "nov" → "nová" → "novák"
URL se změní jednou: ?q=novák (po 300 ms ticha)

Progressive enhancement

Bez JavaScriptu formulář pořád funguje. Hidden inputy nesou ostatní parametry URL, textové pole má name atribut a Enter odešle formulář nativně. Není to SPA, která bez JS zobrazí bílou stránku — je to server-rendered stránka, která s JS funguje plynuleji.

Jedna komponenta, všechny výpisy

Celý pattern žije v jedné sdílené komponentě (live-filter.tsx) se třemi exporty:

  • LiveSearch — textové pole s debounce, pro hledání zákazníků a audit
  • LiveCombobox — select s vyhledáváním, okamžitá aplikace výběru
  • LiveDateInput — datum, aplikace po dokončení výběru

Všechny tři zapisují do URL přes stejný mechanismus: přečtou aktuální params, změní jeden klíč, smažou stránkovací countery (filtrovaný seznam začíná od první stránky) a pushnou novou URL bez scrollu.

Změna filtru resetuje stránkování, ale ostatní parametry — řazení, jiné filtry — zůstávají. Admin si nastaví řazení podle data, pak hledá jméno, a řazení mu nezmizí.

Proč ne client-side stav

Alternativa by byla: filtry v React state, useEffect co fetchuje data, loading spinners. Funguje to, ale:

  • Odkaz na vyfiltrovaný pohled nejde poslat dál
  • F5 resetuje filtry
  • Browser back/forward nefunguje intuitivně
  • Server component nemůže data načíst přímo — potřebuje client-side fetch

URL jako stav znamená, že server component čte searchParams a renderuje. Žádný fetch, žádný loading state, žádný waterfall. Data jsou v HTML, které přijde z serveru.

Výsledek

Tlačítko Filtrovat zmizelo ze všech admin výpisů — zákazníci, doklady, audit. Admin vybírá a tabulka reaguje. Žádný mezikrok, žádné „klikni a počkej”. Funguje to na desktopu, na tabletu i bez JavaScriptu.

A celý pattern má 124 řádků kódu.

// ALL_POSTS
ZPĚT NA BLOG