Dynamic / backend-driven filters
Build the config map at runtime for faceted search and backend-described filters.
A config is just data — it doesn't have to be a
static literal. When the backend describes which filters exist (e-commerce faceted
search, a report builder), build the map at runtime and pass it in. The demo below
builds its two filters from a FACETS array:
/products{
"brand": null,
"color": null,
"page": 1,
"per_page": 10
}Map your data to a config
Turn the backend's description into a key → f.*() map, and memoize it on the
source data so filters stays referentially stable between renders:
function ProductFilters() {
const { data: facets } = useQuery(facetsQueryOptions());
const configs = useMemo(
() =>
Object.fromEntries(
(facets ?? []).map((facet) => {
switch (facet.type) {
case 'checkbox':
return [
facet.key,
f.multiSelect({
label: facet.label,
valueType: 'string',
options: facet.values.map((v) => ({
label: v.label,
value: v.value,
count: v.count // facet counts are first-class on FilterOption
}))
})
];
case 'range':
return [facet.key, f.numberRange({ label: facet.label })];
case 'toggle':
return [facet.key, f.boolean({ label: facet.label })];
default:
return [facet.key, f.text({ label: facet.label })];
}
})
),
[facets]
);
const { params, filters } = useFilters(configs);
// render `filters`, fetch with `params` — same as static filters
}Why it works
The hook subscribes to the URL once — a single internal call with a parser map,
not one hook per filter — so a changing number of filters is fine. The only thing
you give up is compile-time typing of params: you can't pass useFilters<P> for a
shape unknown until runtime, so params comes back as a loose record, which is
exactly what you feed your query anyway.
select / multiSelect require valueType up front —
exactly the kind of static declaration a backend-driven config needs, since the options themselves
(and their value type) may not be known until the facets have loaded.