Guides

Search, sort & placement

Render filters outside the toolbar — hidden filters, filterMap, setFilter, and pass-through props.

Declaring a filter puts its value in the URL and params — it says nothing about where you render it. The filters array is just "the default toolbar"; anything shown elsewhere uses filterMap and setFilter.

A search box in the header

Keep search in your config so it flows into params, the query key, and the loader — but render it in the header and mark it hidden so it's not in the toolbar and doesn't light up "N filters applied":

const { filterMap, filters } = useFilters({
  search: f.text({ label: 'Search', hidden: true }), // in params, not in `filters`
  status: f.select({ label: 'Status', valueType: 'string', options: statusOptions })
});

// In the header:
const search = filterMap.search;
<input value={search.value ?? ''} onChange={(e) => search.onChange(e.target.value || null)} />;

// The toolbar only shows `status`:
<Toolbar filters={filters} />;

Setting a filter imperatively

For a preset button or a tab bar that isn't a "control", set a filter directly with setFilter. It commits immediately, bypassing any commit deferral:

const { setFilter } = useFilters(configs);

<button onClick={() => setFilter('status', 'open')}>Only open</button>;

Sort

Sort is a different shape — usually a compound sort_by + sort_order driven from table headers — so it's intentionally not a filter. Drive it with nuqs directly and spread it into your query key next to params.

Pass-through components

A shared component that receives a whole useFilters return as a prop — a design-system toolbar, a mobile sheet, a debug panel — doesn't know which config produced it. Type the prop AnyUseFiltersReturn and skip the generics:

import type { AnyUseFiltersReturn } from '@mbsatimov/use-filters';

function FilterSheet({ filters }: { filters: AnyUseFiltersReturn }) {
  return (
    <>
      {filters.filters.map((f) => (
        <Control key={f.key} filter={f} />
      ))}
      <button disabled={!filters.isDirty} onClick={filters.apply}>
        Apply
      </button>
    </>
  );
}

// Any call site passes its (fully typed) return:
const filters = useFilters(configs);
<FilterSheet filters={filters} />;

Reads stay typed — filters.filters is ResolvedFilter[]. What loosens is only what a key-agnostic component couldn't use anyway: params values are unknown and setFilter is uncallable (change values through each filter's own handlers). The call site keeps its full types; this only describes the prop.

On this page