Guides / Building Search UI / Widgets

Show and hide React InstantSearch widgets

This is the React InstantSearch v7 documentation. React InstantSearch v7 is the latest version of React InstantSearch and the stable version of React InstantSearch Hooks.

If you were using React InstantSearch v6, you can upgrade to v7.

If you were using React InstantSearch Hooks, you can still use the React InstantSearch v7 documentation, but you should check the upgrade guide for necessary changes.

If you want to keep using React InstantSearch v6, you can find the archived documentation.

Widgets do more than display a UI—they each interact with a piece of the UI state that maps to one or more Algolia search parameters.

For example, the <SearchBox> controls the query, the <RefinementList> interacts with facetFilters, parameter.

When a widget is mounted or unmounted, this is reflected in the InstantSearch UI state and included in search requests.

Consider what happens in a mobile search interface with filters (<RefinementList>): when a user clicks the Filters button, a dialog opens and displays the refinements.

In many component libraries, the <Dialog> component mounts and unmounts its content when toggled. This is problematic when used with InstantSearch components.

For example, if you have a <RefinementList> widget nested in the dialog:

  • The widget wouldn’t be mounted on the first app load because the dialog box is closed.
  • When the dialog box opens, the widget mounts, adding it to the InstantSearch state and triggering a new request, even before a refinement has been selected.
  • When the dialog box closes, the widget unmounts, removing it from the InstantSearch state and losing all selected refinements.

To keep the state available after unmount, you can either:

Enable preserveSharedStateOnUnmount

Enabling preserveSharedStateOnUnmount prevents a widget’s state from being cleared when it’s unmounted, as long as other widgets share the same refinements. Enabling this option changes how dispose is used in the InstantSearch lifecycle.

By default when a widget is removed, it provides a cleared version of the state that will be propagated throughout the other widgets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState } from 'react';
import {
  RefinementList,
  useRefinementList,
} from 'react-instantsearch';

export function Search() {
  useRefinementList({ attribute: 'brand' });
  const [showMenu, setShowMenu] = useState(false);

  return (
    <>
      <button
        type="button"
        onClick={() => setShowMenu(!showMenu)}
      >
        Filters
      </button>
      {showMenu && <RefinementList attribute="brand" />}
      {/* ... */}
    </>
  );
}

This option is available from v7.2.0 and will be the only option from React InstantSearch v8.

Find out more about this option in the API reference.

Keep the widget mounted but hidden

The most straightforward way to retain a widget’s state and refinements is to avoid unmounting it.

You can, for example, hide the content of the dialog with CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';

export function Dialog({ open, children }) {
  return (
    <div
      style={{
        display: open ? 'block' : 'none',
      }}
    >
      {children}
    </div>
  );
}

If you’re using a component library, you can verify whether the dialog component lets you avoid unmounting its content. For example, the <Dialog> component from Headless UI lets you turn off unmounting with the unmount option.

If you can’t avoid unmounting, you can try persisting the state on unmount.

Persist the state on unmount

If you can’t prevent unmounting a widget, you can keep track of the InstantSearch UI state to preserve it and apply it back when the dialog unmounts.

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
36
37
38
39
40
41
import React, { useEffect, useRef } from 'react';
import {
  RangeInput,
  RefinementList,
  useInstantSearch,
  useRange,
  useRefinementList,
} from 'react-instantsearch';

export function Filters() {
  const { uiState, setUiState } = useInstantSearch();
  const uiStateRef = useRef(uiState);

  // Keep up to date uiState in a reference
  useEffect(() => {
    uiStateRef.current = uiState;
  }, [uiState]);

  // Apply latest uiState to InstantSearch as the component is unmounted
  useEffect(() => {
    return () => {
      setTimeout(() => setUiState(uiStateRef.current));
    };
  }, [setUiState]);

  return (
    <div>
      <h2>Brands</h2>
      <RefinementList attribute="brand" />
      <h2>Price range</h2>
      <RangeInput attribute="price" />
    </div>
  );
}

export function VirtualFilters() {
  useRefinementList({ attribute: 'brand' });
  useRange({ attribute: 'price' });

  return null;
}

Two components are used: <Filters> and <VirtualFilters>. <Filters> renders <RefinementList> and <RangeInput> for the brand and price attributes. This component is nested in <Dialog> (see App.jsx), so the widgets are mounted and unmounted as users toggle the dialog.

To avoid losing applied filters, the <VirtualFilters> uses useRefinementList() and useRange() which register themselves in InstantSearch for the same brand and price attributes as the widgets. The component is “renderless”, users don’t interact with it, but it allows persisting the state for brand and price within InstantSearch even when the widgets are unmounted.

Use the Hook in a parent component

In some situations you may want to use Hooks instead of widgets—for example, if you’re using React Native, or if you want to fully control what’s rendered. In this case you can use Hooks in a parent component which isn’t subject to being mounted and unmounted.

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
import React, { useState } from 'react';
import { useRefinementList } from 'react-instantsearch';

import { Dialog } from './Dialog';

export function Filters() {
  const { items, refine } = useRefinementList({ attribute: 'brand' });
  const [dialogOpen, setDialogOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setDialogOpen(!dialogOpen)}>Filters</button>
      <Dialog open={dialogOpen}>
        <ul>
          {items.map((item) => (
            <li key={item.value}>
              <label>
                <input
                  type="checkbox"
                  onChange={() => refine(item.value)}
                  checked={item.isRefined}
                />
                <span>{item.label}</span>
              </label>
            </li>
          ))}
        </ul>
      </Dialog>
    </div>
  );
}

When using Hooks, you’re in charge of rendering the UI and setting up UI events. While this provides full control, it also requires more work on your end. If the only reason for you to use Hooks is to fix mounting and unmounting issues, it’s strongly recommended try hiding the widget or persisting the state on unmount instead.

Did you find this page helpful?