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. views.list.filters 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.

views: {
  list: {
    filters: {
      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.
      initial: { status: "published" },
    },
  },
}

Filter the articles — the values reach the repository

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

filters.initial, 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 initial exists as a separate key.

// ❌ Does not do what it looks like.
filters: {
  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.
filters: {
  inputs: { status: { label: "Status" } },
  initial: { status: "published" },
}

It also defines the resting state

“Clear search” resets to filters.initial, 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 tab 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 tab filtering on its parent, invisibly and permanently.
tabs: {
  items: [
    {
      resource: articles,
      filter: ({ item }) => ({ author: item["@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
filterFilterInterface—The filter currently applied, cleaned of empty values.
updateFilter(filter, merge?: boolean) => void—Merges by default; pass false to replace the whole filter.
resetFilter(filter?) => void—Back to filters.initial, keeping injected filters.
filterIsEmptyboolean—Whether the reader has searched for anything — defaults and injected filters do not count.
formContextFormContextOutput—The 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.