Quickstart

Build a working, URL-synced filtered list in a few lines.

This page builds a small filtered list end to end. Make sure you've mounted the nuqs adapter first.

Try it

The demo below is the real hook. Type in the box and pick a status — the URL bar and the params object update together, because both are derived from the URL:

/products
params
{
  "search": null,
  "status": null,
  "page": 1,
  "per_page": 10
}

Declare your filters

Call useFilters with a map of key → f.*(). Each key becomes a URL query param and a key in params; the f.* builder decides how the value is parsed.

import { f, useFilters } from '@mbsatimov/use-filters';

const statusOptions = [
  { label: 'Open', value: 'open' },
  { label: 'Closed', value: 'closed' }
];

function ProductsPage() {
  const { params, filters } = useFilters({
    search: f.text({ label: 'Search' }),
    status: f.select({ label: 'Status', valueType: 'string', options: statusOptions })
  });

  // ...
}

Fetch with params

params is { search, status, page, per_page }. Hand it to your fetcher, and use it as the query key so results refetch and cache correctly when a filter changes:

const { data } = useQuery({
  queryKey: ['products', params],
  queryFn: () => productApi.getAll(params)
});

params changes exactly when the URL does, so it's a correct cache key for React Query, SWR, or any fetcher — no extra dependency arrays to manage.

Render your filters

filters is an array of resolved filters — each one your config plus a live value and an onChange. Render them however you like:

{
  filters.map((filter) => {
    if (filter.type === 'text') {
      return (
        <input
          key={filter.key}
          placeholder={filter.label}
          value={filter.value ?? ''}
          onChange={(e) => filter.onChange(e.target.value || null)}
        />
      );
    }
    if (filter.type === 'select') {
      return (
        <select
          key={filter.key}
          value={filter.value ?? ''}
          onChange={(e) => filter.onChange(e.target.value || null)}
        >
          <option value=''>{filter.label}</option>
          {filter.options.map((o) => (
            <option key={o.value} value={o.value}>
              {o.label}
            </option>
          ))}
        </select>
      );
    }
    return null;
  });
}

That's the whole loop: declare → fetch with params → render filters.

Where to next

On this page