react-resource-view

Filters

A filter form, defaults that survive the first request, and the URL.

The filter bar is a form#

There is no filter DSL. formFilter is an ordinary form description, built with saveOnChange so that editing it fires a request rather than waiting for a submit. Every controller in the catalogue is available — a date range, a multi-select, a remote search.

view: {
  formFilter: {
    inputs: {
      title:  { label: "Search a title" },
      status: {
        label: "Status",
        controller: SelectInputController,
        valueOptions: STATUSES,
      },
    },
  },
  // Applied as long as the URL carries no filter of its own.
  defaultFilter: { status: "published" },
}

Filter the articles — the values reach the repository

The values are cleaned of empties and merged into the collection request after preGetCollection has run, so a hook cannot overwrite what the reader typed.

defaultFilter, and why not defaultValue#

A list that should open showing only published articles needs the filter to be in the first request. That is the whole reason defaultFilter exists as a separate key.

// ❌ Does not do what it looks like.
formFilter: {
  inputs: { status: { label: "Status", defaultValue: "published" } },
}

The first request goes out before the filter form is built. A defaultValue would reach the form eventually, but the list would already have been fetched unfiltered — so the rows on screen would not match the filters shown above them.

// ✅ Goes out with the first request *and* pre-fills the form.
formFilter: { inputs: { status: { label: "Status" } } },
defaultFilter: { status: "published" },

It also defines the resting state

“Clear search” resets to defaultFilter, not to nothing. And a field whose value equals its default is not counted as a search, so the clear button is not offered permanently.

Filters in the URL#

The active filter is written into the query string as an encoded object, alongside the view context.

  1. A filtered list can be linked to, and the link reopens it filtered.
  2. The back button undoes a filter, because it undoes a URL.
  3. A reload keeps the filters — including the page number.

Pagination goes through the same channel: the current page is a filter key like any other, which is why the routing page treats them together.

Filters the reader cannot clear#

A sub-view showing “this author's articles” is filtered by the parent, and that filter is not the reader's to remove — clearing it would show every author's articles inside a page about one of them.

// A sub-view filtering on its parent, invisibly and permanently.
onInitViewResource: (view, parent) => ({
  ...view,
  filter: { author: parent?.data?.["@id"] },
})

Filters injected this way are marked generatedValue on the filter form, which has two effects: they survive “clear search”, and they are not counted when deciding whether a search is active.

Driving the filter yourself#

A summary card, a saved view, a tab bar — anything that sets filters from outside the bar reaches them through the list context.

import { useListViewContext } from "react-resource-view"

function MyFilterBar() {
  const { filterContext } = useListViewContext()
  const { filter, updateFilter, resetFilter, filterIsEmpty } = filterContext

  return (
    <button onClick={() => updateFilter({ status: "draft" })}>
      Only drafts
    </button>
  )
}
NameTypeDefaultDescription
filterFilterInterfaceThe filter currently applied, cleaned of empty values.
updateFilter(filter, merge?: boolean) => voidMerges by default; pass false to replace the whole filter.
resetFilter(filter?) => voidBack to defaultFilter, keeping injected filters.
filterIsEmptybooleanWhether the reader has searched for anything — defaults and injected filters do not count.
formContextFormContextOutputThe filter form itself, should you want to render it elsewhere.

One filter, one request

Changing the filter refetches; the layout, the selection and the scroll position do not reset. Switching layout keeps the filter for the same reason — they are separate keys of the same context.

  • Filter keys are sent to the API as they are, so name them the way your backend expects — "order[publishedAt]" is a perfectly good field name.
  • Against the localStorage repository, string filters match as case-insensitive substrings, which is what makes the demos on this site searchable.