Guides / Building Search UI / Widgets / Show and hide widgets

Show and hide React InstantSearch Hooks widgets

Widgets do more than displaying 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, etc. When mounting a widget or using the corresponding Hook, it’s reflected in the InstantSearch UI state and included in search requests.

Imagine a mobile search interface with filters (using a <RefinementList>) in a dialog. When you click on a Filters button, the dialog opens and displays the filters.

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

If you had a <RefinementList> nested in such a component, it wouldn’t be mounted on first app load, because the <Dialog> would be closed. When opening the dialog, the <RefinementList> would mount, adding it to the InstantSearch state and triggering a new request even before selecting a refinement. When closing the dialog, the <RefinementList> would unmount, removing it from the InstantSearch state, thus losing all applied refinements.

To prevent this from happening, you can either:

Keep the widget mounted but hidden

The most straightforward way to avoid state losing refinements when widgets unmount is to avoid unmounting widgets.

You can, for example, hide the content of the dialog with CSS instead of rendering it conditionally.

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-hooks-web';

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;
}

There are two components here: <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 the user toggles 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, so the user doesn’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-hooks-web';

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?
React InstantSearch Hooks v6