The “Filter” button in an admin panel is a relic. It comes from the era when a form submitted a POST request and the entire page re-rendered. In 2026, with server components and the URL as the source of truth, it is an unnecessary extra step — the admin picks a filter, then has to click a button, then waits. Three steps instead of one.
In the J+P Autodíly admin panel, we solved this radically: there is no Filter button. Filters apply immediately.
The URL Is the State
The principle is simple: the filter value gets written to the query string and the server-rendered listing re-renders on its own. No client-side state, no useState holding the filter, no useEffect watching for changes. The URL is the 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
When the admin changes a filter, the URL changes. When the URL changes, the server renders a new listing. When the admin sends the link to a colleague, the colleague sees exactly the same view. When they hit F5, the filter persists. No surprises.
Debounce on the Text Field
For selects and date pickers it is straightforward — value change equals an immediate push to the URL. For text search it is not that simple, because the admin types character by character and every keystroke would mean a request.
The solution is a 300 ms debounce. The component keeps local state of what the admin is typing and pushes to the URL only after 300 ms of silence. Result: the table reacts to typing, but not to every keystroke.
Admin types: "n" → "no" → "nov" → "nová" → "novák"
URL changes once: ?q=novák (after 300 ms of silence)
Progressive Enhancement
Without JavaScript the form still works. Hidden inputs carry the other URL parameters, the text field has a name attribute and Enter submits the form natively. This is not an SPA that shows a blank page without JS — it is a server-rendered page that works more smoothly with JS.
One Component, Every Listing
The entire pattern lives in a single shared component (live-filter.tsx) with three exports:
- LiveSearch — debounced text field, for customer search and audit
- LiveCombobox — searchable select, applies the pick immediately
- LiveDateInput — date input, applies once the picker produces a value
All three write to the URL through the same mechanism: read the current params, change one key, delete the pagination counters (a filtered list starts at its first page) and push the new URL without scrolling.
Changing a filter resets pagination, but other parameters — sorting, other filters — ride along untouched. The admin sets sorting by date, then searches for a name, and the sorting does not disappear.
Why Not Client-Side State
The alternative would be: filters in React state, a useEffect that fetches data, loading spinners. It works, but:
- A link to the filtered view cannot be shared
- F5 resets the filters
- Browser back/forward does not behave intuitively
- A server component cannot load the data directly — it needs a client-side fetch
URL as state means the server component reads searchParams and renders. No fetch, no loading state, no waterfall. The data is in the HTML that comes from the server.
The Result
The Filter button disappeared from every admin listing — customers, documents, audit. The admin picks and the table reacts. No intermediate step, no “click and wait”. It works on desktop, on a tablet and without JavaScript.
And the entire pattern is 124 lines of code.