# Hierarchical Filter

Having a filter with sub categories can be a great tool in helping the customer finding exactly what they want from your store. This wiki will show how to build one using Algolia React Instant Search but the concepts should also work for other technologies:

## **The Problem**

Using `<RefinementList />` out of the box to display the attribute `root_types` will result in something like:

```tsx
 <RefinementList
  transformItems={(items: any) => orderBy(items, "label", "asc")}
  attribute="root_types"
  // There is a default limit value of 10 here to increase it need
  // limit={30} 
  // to show more types
/>
```

![](/files/f1PdwBt81r6w1DhhYriW)

\
There are basically two problems here, one of course is how we have all subcategories flat at the top level, but also the entry `best_selling` which has its specific use but it is also a root\_type.\
\
The data needs to be prepared in two steps:

* Filtering out what we don't want shown in the menu
* Grouping the subcategories together

## The Solution

### Getting started

The first thing we need to do is have access to the data so we can change it, to do so we are going to ditch `<RefinementList />` and use [connectRefinementList](https://www.algolia.com/doc/api-reference/widgets/refinement-list/react/#connector)

```tsx
const RefinementList = ({ items, currentRefinement, refine }: any) => {
  return <>Our custom list</>
}

// 2. Connect the component using the connector
const CustomRefinementList = connectRefinementList(RefinementList);

export default CustomRefinementList;

```

Following the two steps listed above, lets prepare the data:

```tsx
// Some types for clarity
// This is from algolia-search-dom
interface RefinementItem<T> {
  value: T;
  label: string;
}

// The items we will be dealing with
interface JaneRefinementItem extends RefinementItem<string[]> {
  count: number;
  isRefined: boolean;
  customLabel?: string | null;
}


const RefinementList = ({ items, currentRefinement, refine }: any) => {
  // If the search has no items for this category, don't show it
  if (items.length === 0) {
    return null;
  }

  // Parses list of items and creates a parent -> children[] structure
  const groupCategories = (
    items: JaneRefinementItem[]
  ): (JaneRefinementItem & { children: JaneRefinementItem[] })[] =>
    items
      .filter((item: JaneRefinementItem) => !item.label.includes(":"))
      .map((parentItem: JaneRefinementItem) => ({
        ...parentItem,
        children: items.filter(
          (item: JaneRefinementItem) =>
            item.label.includes(":") &&
            item.label.split(":")[0] === parentItem.label
        ),
      }));

  const groupedList = groupCategories(items);

  // Need to filter out `best_selling`, `sale` and any other undesired entries
  const toBeRemoved = ["best_selling", "sale"];

  const hierarchicalItems = groupedList.filter(
    (item) => !toBeRemoved.includes(item.label)
  );


  // TODO: We need to build the list of options

  return <>Our custom list</>
}
```

### Creating the list

With the data sorted out we need to create the list of options. To follow the pattern iheartjane has our list will have:

1. A header ('Categories', 'Kind', 'Activities')
2. A list of options with
   1. An option for "All"
   2. Each type that has subtypes&#x20;
3. Each subtype when opened have
   1. An option for "All"
   2. Each of its subtypes&#x20;

Following the last example, replace the `// TODO` and the `return` with the following:

```tsx
// For each of the toplevel categories we will render 
// the following component that will take care of 2.2 and 3.1 and 3.2
// We will define FilterOptionWithChildren next:
const categoriesListMarkup = hierarchicalItems.map((item) => {
    return (
      <FilterOptionWithChildren
        item={item}
        currentRefinement={currentRefinement}
        refine={refine}
      />
    );
  });

  
  return (
    <div>
      // This covers point 1
      <h2>Categories</h2>
      // and 2.1
      <label>
        <input
          type="checkbox"
          onChange={() => refine("")}
          checked={currentRefinement.length === 0}
        />
        All
      </label>
      <div>{categoriesListMarkup}</div>
    </div>
  );
```

<details>

<summary>Full HierarchicalFilter.tsx file</summary>

{% code title="HierarchicalFilter.tsx" %}

```tsx
import { connectRefinementList, RefinementItem } from "react-instantsearch-dom";
import FilterOptionWithChildren from "./FilterOptionWithChildren";

interface JaneRefinementItem extends RefinementItem<string[]> {
  count: number;
  isRefined: boolean;
  customLabel?: string | null;
}

const RefinementList = ({ items, currentRefinement, refine }: any) => {
  if (items.length === 0) {
    return null;
  }

  // parsing items
  const groupCategories = (
    items: JaneRefinementItem[]
  ): (JaneRefinementItem & { children: JaneRefinementItem[] })[] =>
    items
      .filter((item: JaneRefinementItem) => !item.label.includes(":"))
      .map((parentItem: JaneRefinementItem) => ({
        ...parentItem,
        children: items.filter(
          (item: JaneRefinementItem) =>
            item.label.includes(":") &&
            item.label.split(":")[0] === parentItem.label
        ),
      }));

  const groupedList = groupCategories(items);

  // Need to filter out `best_selling`, `sale` and any other undesired entry
  const toBeRemoved = ["best_selling", "sale"];

  const hierarchicalItems = groupedList.filter(
    (item) => !toBeRemoved.includes(item.label)
  );

  const categoriesListMarkup = hierarchicalItems.map((item) => {
    return (
      <FilterOptionWithChildren
        item={item}
        currentRefinement={currentRefinement}
        refine={refine}
      />
    );
  });

  return (
    <div>
      <h2>Categories</h2>
      <label>
        <input
          type="checkbox"
          onChange={() => refine("")}
          checked={currentRefinement.length === 0}
        />
        All
      </label>
      <div>{categoriesListMarkup}</div>
    </div>
  );
};

// 2. Connect the component using the connector
const CustomRefinementList = connectRefinementList(RefinementList);

export default CustomRefinementList;

```

{% endcode %}

</details>

### The list items

Now we need to build `FilterOptionWithChildren` component, taking the iheartjane live menu as a guide, we will need to build:

![](/files/xQklK9QITzbFW6Ld3E7d)

First step, controlling opening and closing of categories

```tsx
function FilterOptionWithChildren({
  currentRefinement,
  item,
  refine,
  showCounts,
}: any) {
  const [open, setOpen] = useState(false);

  // Here just creating a simple header with a text and a + and - symbols
  // to indicate open/close state. 
  const closedHeaderMarkup = (
    <div>
      <span style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
        + {item.label} {item.count}
      </span>
    </div>
  );

  const openHeaderMarkup = (
    <div>
      <span style={{ cursor: "pointer" }} onClick={() => setOpen(false)}>
        - {item.label}
      </span>
    </div>
  );
  
  return (
    <>
      {open ? (
        <div>
          {openHeaderMarkup}
          <>Children will show here</>
        </div>
      ) : (
        closedHeaderMarkup
      )}
    </>
  );
}
```

This intermediary code renders as:\
![](/files/be4V4ntVQPw4kSB6eq0a)

We need to add the `All` button for all categories:

```tsx
import { includes } from "lodash";

{
  // Still inside the function FilterOptionWithChildren
  //{...}
  
  const allOptionsMarkup = (
    <div>
      <label>
        <input
          type="checkbox"
          onChange={() => refine(currentRefinement.concat(item.label))}
          checked={includes(currentRefinement, item.label)}
        />
        All
      </label>
    </div>
  );
  
  // add it to the return statement
  return (
    <>
      {open ? (
        <div>
          {openHeaderMarkup}
          {allOptionsMarkup}
          <>Children will show here</>
        </div>
      ) : (
        closedHeaderMarkup
      )}
    </>
  );

```

This next section goes over the data we get from Algolia regarding filter values, if you don't need/have the time to understand this, just skip to the next [header](#adding-the-child-options).

#### About filter values

By default the filter values are represented by a list of strings, each representing a selection that was made, eg:

`const currentRefinement = ['edible: Baked Goods', 'extract: Live Resins']`

This means the filters are additive and for that reason any operation in this refinement will require looking for, adding and removing items for that list. (versus simply throwing the old filter away and applying a new one).

Another thing that comes into play is when we select a whole category by pressing the `All` option. Here the data looks funny if not treated:

`const currentRefinement = ['edible', 'edible: Baked Goods', 'extract: Live Resins']`

The selection here has both a **category ("edible")** and a **subcategory ("edible: Baked Goods")**, which will lead to some confusion. The expected behaviour is to unselect all subcategories of that type, this requires one of the functions presented in the next section.

So, in the following section the usage of `filter` , `concat`, and `some` are all operations to manipulate this list of options.

#### Adding the child options <a href="#adding-the-child-options" id="adding-the-child-options"></a>

To build the sub categories we iterate over the children of our parent category and create two functions: one to handle a click in that option and one to check if the option is clicked.&#x20;

{% code title="FilterOptionWithChildren" %}

```tsx
import React, { useState } from "react";
import { includes } from "lodash";

function FilterOptionWithChildren({
  currentRefinement,
  item,
  refine,
  showCounts,
}: any) {
  const [open, setOpen] = useState(false);

  // Function that removes the selection from all subcategories
  // when the parent category `All` option is selected
  const removeSelectedChildrenOf = (category: string): string[] => {
    return currentRefinement.filter((refinement: string) => {
      const [targetCategory] = refinement.split(":");
      return category !== targetCategory;
    });
  };

  // As stated above, currentRefinement is a list of selected filters
  // We check the whole list against the current one
  const isChildChecked = (value: string) => {
    return currentRefinement.some((refinement: string) => {
      return value === refinement;
    });
  };

  // When the option is clicked
  const onChildItemClick = (value: string) => {
    // First, if it is already selected, the user is trying to unselect
    // We filter it out
    if (isChildChecked(value)) {
      const newRefinement = currentRefinement.filter(
        (refinement: string) => refinement !== value
      );

      refine(newRefinement);
      return;
    }

    // Second, this is the case where the 'All' for the category is selected
    // and the user selects a subcategory. We remove the 'All' and add the selected
    // filter value
    if (includes(currentRefinement, item.label)) {
      const newRefinement = currentRefinement
        .filter((refinement: string) => refinement !== item.label)
        .concat(value);

      refine(newRefinement);
      return;
    }
    
    // If none of those special cases we take the current filter content and add
    // the newly selected filter to it
    refine(currentRefinement.concat(value));
  };

  const childrenMarkup = item.children.map(
    // isRefined is a handy attribute since it tells us, without other processing
    // if the current filter is currently selected
    ({ label, count, value, isRefined }: any) => {
     // splitting the string in the format "category:subcategory"
      const [, subcategoryLabel] = label.split(":");
      return (
        <div>
          <label key={value}>
            <input
              aria-label={label}
              type="checkbox"
              onChange={() => onChildItemClick(label)}
              checked={isRefined}
            />
            {subcategoryLabel} {count}
          </label>
        </div>
      );
    }
  );

  const closedHeaderMarkup = (
    <div>
      <span style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
        + {item.label} {item.count}
      </span>
    </div>
  );

  const openHeaderMarkup = (
    <div>
      <span style={{ cursor: "pointer" }} onClick={() => setOpen(false)}>
        - {item.label}
      </span>
    </div>
  );

  const allOptionsMarkup = (
    <div>
      <label>
        <input
          type="checkbox"
          // Here we remove all selection from subcategories of the category
          // the uses clicks to see `All`
          onChange={() => 
            refine(removeSelectedChildrenOf(item.label).concat(item.label))
          }
          checked={includes(currentRefinement, item.label)}
        />
        All
      </label>
    </div>
  );

  return (
    <>
      {open ? (
        <div>
          {openHeaderMarkup}
          {allOptionsMarkup}
          {childrenMarkup}
        </div>
      ) : (
        closedHeaderMarkup
      )}
    </>
  );
}

export default FilterOptionWithChildren;

```

{% endcode %}

## Full files and usage

<details>

<summary>HierarchicalFilter.tsx</summary>

```tsx
import { connectRefinementList, RefinementItem } from "react-instantsearch-dom";
import FilterOptionWithChildren from "./FilterOptionWithChildren";

interface JaneRefinementItem extends RefinementItem<string[]> {
  count: number;
  isRefined: boolean;
  customLabel?: string | null;
}

const RefinementList = ({ items, currentRefinement, refine }: any) => {
  if (items.length === 0) {
    return null;
  }

  // parsing items
  const groupCategories = (
    items: JaneRefinementItem[]
  ): (JaneRefinementItem & { children: JaneRefinementItem[] })[] =>
    items
      .filter((item: JaneRefinementItem) => !item.label.includes(":"))
      .map((parentItem: JaneRefinementItem) => ({
        ...parentItem,
        children: items.filter(
          (item: JaneRefinementItem) =>
            item.label.includes(":") &&
            item.label.split(":")[0] === parentItem.label
        ),
      }));

  const groupedList = groupCategories(items);

  // Need to filter out `best_selling`, `sale` and any other undesired entry
  const toBeRemoved = ["best_selling", "sale"];

  const hierarchicalItems = groupedList.filter(
    (item) => !toBeRemoved.includes(item.label)
  );

  const categoriesListMarkup = hierarchicalItems.map((item) => {
    return (
      <FilterOptionWithChildren
        item={item}
        currentRefinement={currentRefinement}
        refine={refine}
        showCounts={true}
      />
    );
  });

  return (
    <div>
      <h2>Categories</h2>
      <label>
        <input
          type="checkbox"
          onChange={() => refine("")}
          checked={currentRefinement.length === 0}
        />
        All
      </label>
      <div>{categoriesListMarkup}</div>
    </div>
  );
};

// 2. Connect the component using the connector
const CustomRefinementList = connectRefinementList(RefinementList);

export default CustomRefinementList;

```

</details>

<details>

<summary>FilterOptionWithChildren.tsx</summary>

```tsx
import React, { useState } from "react";
import { includes } from "lodash";

function FilterOptionWithChildren({
  currentRefinement,
  item,
  refine,
  showCounts,
}: any) {
  const [open, setOpen] = useState(false);

  // Function that removes the selection from all subcategories
  // when the parent category `All` option is selected
  const removeSelectedChildrenOf = (category: string): string[] => {
    return currentRefinement.filter((refinement: string) => {
      const [targetCategory] = refinement.split(":");
      return category !== targetCategory;
    });
  };


  // As stated above, currentRefinement is a list of selected filters
  // We check the whole list agains the current one
  const isChildChecked = (value: string) => {
    return currentRefinement.some((refinement: string) => {
      return value === refinement;
    });
  };

  // When the option is clicked
  const onChildItemClick = (value: string) => {
    // First, if it is already selected, the user is trying to unselect
    // We filter it out
    if (isChildChecked(value)) {
      const newRefinement = currentRefinement.filter(
        (refinement: string) => refinement !== value
      );

      refine(newRefinement);
      return;
    }

    // Second, this is the case where the 'All' for the category is selected
    // and the user selects a subcategory. We remove the 'All' and add the selected
    // filter value
    if (includes(currentRefinement, item.label)) {
      const newRefinement = currentRefinement
        .filter((refinement: string) => refinement !== item.label)
        .concat(value);

      refine(newRefinement);
      return;
    }
    
    // if none of those special cases we take the current filter content and add
    // the newly selected filter to it
    refine(currentRefinement.concat(value));
  };

  const childrenMarkup = item.children.map(
    // isRefined is a handy attribute since it tells us, without other processing
    // if the current filter is currently selected
    ({ label, count, value, isRefined }: any) => {
     // splitting the string in the format "category:subcategory"
      const [, subcategoryLabel] = label.split(":");
      return (
        <div>
          <label key={value}>
            <input
              aria-label={label}
              type="checkbox"
              onChange={() => onChildItemClick(label)}
              checked={isRefined}
            />
            {subcategoryLabel} {count}
          </label>
        </div>
      );
    }
  );

  const closedHeaderMarkup = (
    <div>
      <span style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
        + {item.label} {item.count}
      </span>
    </div>
  );

  const openHeaderMarkup = (
    <div>
      <span style={{ cursor: "pointer" }} onClick={() => setOpen(false)}>
        - {item.label}
      </span>
    </div>
  );

  const allOptionsMarkup = (
    <div>
      <label>
        <input
          type="checkbox"
          // Here we remove all selection from subcategories of the category
          // the uses clicks to see `All`
          onChange={() => 
            refine(removeSelectedChildrenOf(item.label).concat(item.label))
          }
          checked={includes(currentRefinement, item.label)}
        />
        All
      </label>
    </div>
  );

  return (
    <>
      {open ? (
        <div>
          {openHeaderMarkup}
          {allOptionsMarkup}
          {childrenMarkup}
        </div>
      ) : (
        closedHeaderMarkup
      )}
    </>
  );
}

export default FilterOptionWithChildren;

```

</details>

To put all this to use, in your `<InstantSearch />` component we would need:

```tsx
<InstantSearch searchClient={searchClient} indexName="your-index-name">
// Your other components
  <HierarchicalFilter
    transformItems={(items: any) => orderBy(items, "label", "asc")}
    attribute="root_types"
    limit={50}
/>
</InstantSearch>
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.iheartjane.com/jane-docs/implementing-roots/building-your-menu/using-react-and-instant-search/filter-tutorials/hierarchical-filter.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
