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[] | nullFor 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:
| Option | Purpose |
|---|---|
label | Required. Human label for the control. |
placeholder | Optional; defaults to label. |
defaultValue | Value when the URL param is absent (above). |
hidden | Keep in params, omit from filters (above). |
commit | When a change reaches the URL. See Values & commits. |
meta | Your own typed UI hints. See UI metadata. |
nuqs | Per-filter nuqs options, e.g. { history: 'push' }. |