Search documentation

Search for a page or heading...

tablecn
0

Build your own filter UI with the hooks behind the ready-made one.

The ready-made filter UI is built entirely on the hooks from @querycn/filter-react. Use them to build your own: a sidebar of checkboxes, quick-filter buttons, a different builder. The hooks handle the state, validation, the URL and the API request; you only render.

Need small changes? It's usually easier to edit the copied components.

Which hook do I need?

You want to…Use
Load or show data for the applied filteruseAppliedFilter
Build the editing panel: add rules, Apply, Clear alluseFilter
Render one rule row: field, operator and value pickersuseFilterRule
Show warnings on a ruleuseRuleWarnings
Show options for a select field, static or loadeduseFieldOptions

All of them must be used inside a FilterProvider.

A minimal builder

A panel with rule rows, using plain HTML elements:

components/my-filter-panel.tsx
import type { FilterRule } from "@querycn/filter-core"
import { useFilter, useFilterRule } from "@querycn/filter-react"

export function MyFilterPanel() {
  const { state, isDirty, addRule, apply, reset } = useFilter()
  return (
    <form onSubmit={(event) => { event.preventDefault(); apply() }}>
      {state.rules.map((rule) => <MyRuleRow key={rule.id} rule={rule} />)}
      <button type="button" onClick={() => addRule()}>Add filter</button>
      <button type="button" onClick={reset}>Clear all</button>
      <button type="submit" disabled={!isDirty}>Apply</button>
    </form>
  )
}

const MyRuleRow = React.memo(function MyRuleRow({ rule }: { rule: FilterRule }) {
  const { fields, operators, arity, setField, setOperator, setValue, remove } = useFilterRule(rule)

  return (
    <div>
      <select value={rule.field} onChange={(e) => setField(e.target.value)}>
        <option value="" disabled>Field</option>
        {fields.map((f) => <option key={f.name} value={f.name}>{f.label}</option>)}
      </select>
      <select value={rule.operator ?? ""} onChange={(e) => setOperator(e.target.value)}>
        <option value="" disabled>Operator</option>
        {operators.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
      </select>
      {arity === "single" && (
        <input
          value={typeof rule.value === "string" || typeof rule.value === "number" ? rule.value : ""}
          onChange={(e) => setValue(e.target.value)}
        />
      )}
      <button type="button" onClick={remove}>×</button>
    </div>
  )
})

Draft and applied

The provider keeps two versions of the filter:

  • The draft is what users are editing. Rules can be incomplete. useFilter and useFilterRule work on it.
  • The applied filter is what's in the URL, and what your data follows. useAppliedFilter reads it.

apply() copies the complete draft rules to the applied filter. discard() throws the draft away and starts again from the applied filter; the ready-made popover calls it when closed without applying. The applied filter also changes on reset(), on a chip's ×, and when the URL changes (Back / Forward, a shared link).

FilterProvider

Usage
import { FilterProvider } from "@querycn/filter-react"

<FilterProvider fields={orderFields} adapter={adapter}>
  {children}
</FilterProvider>

Prop

Type

Define fields, serializer, messages and registry outside your components (or in useMemo). A new one on every render rebuilds everything.

useAppliedFilter

The applied filter and what's built from it. Use it where you load or show data.

Usage
const { query, queryKey, activeCount } = useAppliedFilter()
PropertyDescription
queryThe API request params, from the serializer.
queryKeyA short string of the filter's URL params (status__eq=paid, "" when empty). Use it as a cache key.
activeCountHow many rules are applied, e.g. for a badge.
stateThe applied FilterState: complete rules only.
removeRule(id)Removes one applied rule right away, like a chip's ×.
ruleIssuesWarnings per rule id: "conflict" or "unsupported".
context{ fields, registry }, ready for core functions like applyFilter.
messagesThe UI text.

With a custom serializer, pass its output type: useAppliedFilter<Prisma.OrderWhereInput>().

Outside a provider, it returns an empty filter instead of throwing, so a table can call it whether or not a filter is set up.

useFilter

The draft and every editing action. It re-renders on each keystroke, so only use it in the editing UI.

PropertyDescription
stateThe draft FilterState, incomplete rules included.
isDirtyApplying would change the applied filter.
canAddRulefalse once maxRules is reached.
addRule(field?)Adds a rule, optionally with a field already picked.
removeRule(id)Removes a draft rule.
setField(id, field)Changes the field. The operator goes back to the field's default, and the value is cleared.
setOperator(id, operator)Changes the operator. The value is kept if its shape stays the same.
setValue(id, value)Sets the value. Raw text is fine, like "1.": it's checked on apply.
setJoin(join)"and" or "or".
apply()Applies the complete rules and writes them to the URL.
reset()Clears the filter and applies at once.
discard()Drops the edits that weren't applied.
supportsOperator(id)Whether the serializer can send this operator.

Actions always read the latest draft, so setValue(…) followed by apply() in the same handler applies the new value.

useFilterActions() returns the same actions without state, isDirty and canAddRule. It doesn't change while users type, so memoized components that only need actions don't re-render.

useFilterRule

Everything one rule row needs, bound to that rule:

PropertyDescription
fieldThe rule's field definition, or undefined before one is picked.
fieldsThe fields to offer: those with at least one operator the serializer can send.
operators{ id, label, supported }[] for the field. An operator from the URL that the serializer can't send is listed last, with supported: false.
arityThe operator's value shape ("none", "single", "range", "multi"), or null before one is picked. Pick the value input from it.
setField, setOperator, setValue, removeActions for this rule.

useRuleWarnings

useRuleWarnings(rule) returns the warnings for one draft rule, like ["reversedRange"]. Get the text from messages.warnings[key]. It reads the draft, so use it in a small component of its own rather than in a memoized row.

useFieldOptions

Options for a select or multiSelect field, static or loaded from your API:

Usage
const { options, loading, error, query, search, retry, getLabel } = useFieldOptions(field, {
  selected: currentValues, // values that need a label
  enabled: open, // only load the list while the dropdown is open
})
OptionDefaultDescription
selected[]Values to fetch labels for, with resolveLabels.
enabledtrueSet to false while the list is closed: labels still load, the list doesn't.
debounceMs300How long to wait after typing before loading.
ResultDescription
optionsOptions for the current search. The previous list stays while the next one loads.
loading, errorThe current request's state.
query, search(text)The search text and its setter.
retry()Loads the current search again after an error.
getLabel(value)A value's label, or the value itself until it's known.