Guides / Building Search UI / Going further

Access State Outside the Lifecycle

Each InstantSearch widget owns its part of the search state. While this fits the majority of use cases, sometimes you might want to read the state or refine the search outside of the InstantSearch lifecycle. For example, you may want to let users refine on a specific category from a product page, and not just from a refinement list.

InstantSearch.js lets you access the render state of each widget, which you can use to create custom widgets or refine the search outside the InstantSearch.js lifecycle.

The renderState property is available starting in InstantSearch.js v4.9.

Refining a search from the outside

In InstantSearch, the refinementList widget controls refinements for a given attribute. You can access its state via the renderState property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const search = instantsearch({
  indexName,
  searchClient,
});

search.addWidgets([
  // ...
  instantsearch.widgets.refinementList({
    container: '#brand',
    attribute: 'brand'
  }),
]);

search.renderState[indexName].refinementList.brand; // returns an object containing the render state

The exposed state matches the render options exposed to the connectRefinementList render options. You can access the state of brand, but also leverage the refine method to programmatically set a refinement.

1
search.renderState[indexName].refinementList.brand.refine('Apple');

This lets you refine on a brand from anywhere in your app, even outside of the InstantSearch lifecycle. For example, you might want to let users search in the related brand whenever they visit a product page.

1
2
3
<button id="apple-search-button">
  Search "Apple" products
</button>
1
2
3
4
5
document
  .querySelector('#apple-search-button')
  .addEventListener('click', () => {
    search.renderState[indexName].refinementList.brand.refine('Apple');
  });

Accessing the state of other widgets

When refining on a brand, you might end up with no results at all. By default, the hits widget displays a “No results” message. You can customize it using the connectHits connector along with the refinementList render state.

First, remove the default template for empty hits from the hits widget so it doesn’t display anything when there are no hits.

1
2
3
4
5
6
7
instantsearch.widgets.hits({
  container: '#hits',
  templates: {
    // ...
    empty: ''
  }
});

Second, add a button to reproduce the no results situation.

1
2
3
<button id="pear-search-button">
  Search "Pear" products
</button>
1
2
3
4
5
document
  .querySelector('#pear-search-button')
  .addEventListener('click', () => {
    search.renderState[indexName].refinementList.brand.refine('Pear');
  });

Then, you can add a dedicated custom widget to handle the no results situation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const renderer = ({ hits, widgetParams }) => {
  const container = document.querySelector(widgetParams.container);
  if (hits.length > 0) {
    container.innerHTML = '';
    return;
  }

  const brandState = search.renderState[indexName].refinementList.brand;
  const isPearRefined =
    brandState.items.filter((item) => item.label === 'Pear' && item.isRefined)
      .length > 0;

  if (!isPearRefined) {
    container.innerHTML = 'No results';
    return;
  }

  container.innerHTML = `
    <p>No results for Pear</p>
    <button>
      Remove "Pear" filter
    </button>
  `;
  container.querySelector('button').addEventListener('click', () => {
    search.renderState[indexName].refinementList.brand.refine('Pear');
  });
};

const emptyHits = instantsearch.connectors.connectHits(renderer);

search.addWidgets([
  emptyHits({
    container: '#empty-hits',
  })
]);

Even though you’re inside the connectHits connector, you can access the render state of the brand refinementList. It lets you figure out whether “Apple” is among the refined facets, you can display a button to remove it.

The refine method is a toggle function. When you call it with a value that’s already refined, it removes it from the search state, and vice versa.

If you’re not familiar with customizing widgets with connectors, please check the guide on customizing the complete UI of widgets.

Using the panel widget

The panel widget gives you easier access to the render state.

This is the basic implementation of a panel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const categories = panel({
  templates: {
    header: 'Categories',
  },
})(instantsearch.widgets.hierarchicalMenu);

search.addWidgets([
  categories({
    container: '#categories',
    attributes: [
      'hierarchicalCategories.lvl0',
      'hierarchicalCategories.lvl1',
      'hierarchicalCategories.lvl2',
      'hierarchicalCategories.lvl3',
    ],
  }),
])

The renderState of hierarchicalMenu for each attribute are available in the collapsed and hidden methods of widget.

1
2
3
4
5
6
7
8
9
10
11
const categories = panel({
  templates: {
    header: 'Categories',
  },
  collapsed(options) {
    // ...
  },
  hidden(options) {
    // ...
  }
})(instantsearch.widgets.hierarchicalMenu);

The provided options include the same state as you’d find under search.renderState[indexName].hierarchicalMenu['hierarchicalCategories.lvl0'].

For example, when there are no facets to display, you can hide or collapse the hierarchicalMenu:

1
2
3
4
5
6
const categories = panel({
  // ...
  hidden({ items }) {
    return items.length === 0;
  },
})(instantsearch.widgets.hierarchicalMenu);

You can see a live demo on CodeSandbox.

Did you find this page helpful?
InstantSearch.js v4