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.
On this page
Widgets do more than display a UI: they each modify the uiState
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
- Keep the widget mounted but hidden
- Persist the state on unmount using a virtual widget
- Use the Hook in a parent component
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.