Core concepts

The filter config

A config is plain data built with f.*: value types, defaults, and hidden filters.

The object you pass to useFilters is just data. Each entry is built with an f.* helper that records the filter's kind and options. Nothing is rendered, and nothing runs until the hook resolves it.

const configs = {
  search: f.text({ label: 'Search' }),
  status: f.select({ label: 'Status', valueType: 'string', options: statusOptions }),
  min_price: f.number({ label: 'Min price' })
};

Because it's plain data, a config can be shared (exported from a module), built at runtime from a backend response, or handed to a route loader. The same object drives the hook and its loader twin.

Value types

The builder you pick determines the type of that filter's value in params:

f.text(...)         // string | null
f.number(...)       // number | null
f.boolean(...)      // boolean | null
f.select(...)       // V | null       (V driven by valueType, checked against options)
f.multiSelect(...)  // V[] | null

For a select or multiSelect, V is driven by the required valueType ('number' | 'string'). Numeric options give number | null, a string union gives 'open' | 'closed' | null, and options are checked against it (a mismatched option is a compile error). See Filter types for the full list.

valueType is required, rather than inferred from options, so a route loader calling resolveFilterParams (which never sees options) always parses the same type the hook does. See Async filters.

Defaults

defaultValue sets the value used when the URL has no param for that filter. A filter sitting at its default counts as inactive: it's excluded from isFiltered and, with clearOnDefault (the default), kept out of the URL entirely:

status: f.select({
  label: 'Status',
  valueType: 'string',
  options: statusOptions,
  defaultValue: 'open'
});
// No `?status` in the URL → params.status is 'open', and isFiltered ignores it.

Because a defaulted filter falls back to its default when unset (and again when cleared), it can never be null, so its value type drops the | null:

f.select({ label: 'Status', valueType: 'string', options }); //                   'open' | 'closed' | null
f.select({ label: 'Status', valueType: 'string', options, defaultValue: 'open' }); // 'open' | 'closed'  (never null)

This holds for every kind, and everywhere params is derived: the hook and resolveFilterParams alike. A defaulted filter is still clearable in the UI: clearing (or onChange(null)) resets it to its default.

Hidden filters

hidden: true keeps a filter's value in params (and filterMap) but leaves it out of the filters array. Use it for a value you manage somewhere other than the main toolbar: a search box in the header, or a fixed value you set imperatively:

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

More on placing filters in different parts of your UI in Search, sort & placement.

Shared per-filter options

Every kind accepts these, regardless of type:

OptionPurpose
labelRequired. Human label for the control.
placeholderOptional; defaults to label.
defaultValueValue when the URL param is absent (above).
hiddenKeep in params, omit from filters (above).
commitWhen a change reaches the URL. See Values & commits.
metaYour own typed UI hints. See UI metadata.
nuqsPer-filter nuqs options, e.g. { history: 'push' }.

On this page