Deduped filter state
One hook owns the filter list. Controls add, remove, and ask by partial criteria. Duplicates can't happen.
A query builder has many separate controls (a status checkbox, a date picker, a search box) that all write to one filter list. If each control edits the array itself, you get duplicates, lost filters, and .filter() calls spread across every component.
Instead, one hook owns the list. Controls describe what they want, and every write goes through dedupe.
type Filter = { field: string; op: string; value: unknown };
const same = (a: Filter, b: Filter) =>
a.field === b.field && a.op === b.op && a.value === b.value;
const dedupe = (fs: Filter[]) => fs.filter((f, i) => fs.findIndex((o) => same(o, f)) === i);
const matches = (f: Filter, c: Partial<Filter>) =>
Object.entries(c).every(([k, v]) => f[k as keyof Filter] === v);
export function useFilters() {
const [filters, set] = useState<Filter[]>([]);
const write = (fn: (prev: Filter[]) => Filter[]) => set((prev) => dedupe(fn(prev)));
return {
filters,
add: (f: Filter) => write((p) => [...p, f]),
remove: (c: Partial<Filter>) => write((p) => p.filter((f) => !matches(f, c))),
has: (c: Partial<Filter>) => filters.some((f) => matches(f, c)),
};
}
Partial criteria
remove and has take any subset of a filter, so one API covers every case:
has({ field: "status", value: "active" }); // is this checkbox checked?
remove({ field: "status" }); // clear the whole section
remove({ field: "status", value: "active" }); // uncheck one box
The filter list then feeds a config-driven filter panel.