Create your own 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
React InstantSearch comes with multiple widgets that you can extensively customize, but you can also use connectors to completely change your rendering.
If a widget and its connector don’t cover your needs, you can create your own widget. Making widgets is the most advanced way of customizing your search experience and it requires a deeper knowledge of InstantSearch and Algolia.
This guide covers how to build a custom widget :
You’re trying to create your own InstantSearch widget and that’s awesome, but it also means that you couldn’t find what you were looking for. InstantSearch aims at offering the best out-of-the-box experience, so we’d love to hear about your use case.
Don’t hesitate to open a feature request explaining what you’re trying to achieve.
When to create custom widgets
You can create a new widget when none of the existing widgets fit your functional needs. However, if you’re trying to redefine the UI or DOM output of a widget, you should, instead, extend it by using its connector counterpart.
Existing widgets and connectors should fit most of your use cases and you should look into them before creating custom connectors. For example, to create buttons that set predefined queries, you could use useSearchBox()
. Although you’re not rendering a search box, the connector provides the necessary APIs for this, so there’s no need to re-develop it.
For help, explain your situation and ask questions on GitHub.
You’ll see references to InstantSearch.js and its APIs throughout the guide. React InstantSearch relies on this core library and bridges them with a thin adapter.
If you’re using TypeScript, install algoliasearch-helper
and instantsearch.js
as development dependencies to access the necessary types. Make sure to use the same versions as the ones in your React InstantSearch version.
Once you’re done building an InstantSearch.js connector, you’ll turn it into a Hook.
Building a custom connector
When creating a custom widget, start by writing a connector that encapsulates all the logic of your widget, yet keeps the rendering separate.
This guide uses the example of a negative refinement list widget. It’s similar to a <RefinementList>
, but instead of filtering on the selected items, it excludes them from the search. For example, selecting the brand “Apple” would filter results to all matching records that aren’t Apple products.
Write the connector function
First, create a connectNegativeRefinementList
function that takes a render and an unmount function. It should return a negativeRefinementList
function (the widget factory) that takes widget parameters and returns an object (the widget).
For the sake of simplicity, the only parameter that the widget accepts is attribute
. This lets users specify which record attribute to filter on.
1
2
3
4
5
6
7
8
9
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
};
};
}
const noop = () => {};
Your custom connector needs an identifier. The naming convention is "myOrganization.myWidget"
(for example, "microsoft.negativeRefinementList"
). If you don’t have an organization, you can use your name.
1
2
3
4
5
6
7
8
9
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
$$type: 'myOrganization.negativeRefinementList',
};
};
}
// …
Compute the render state
For now, the widget isn’t doing much. The whole point of writing a widget is to hook into the InstantSearch lifecycle to alter the search call with new parameters, pick data from the search response, and expose it for users to render it the way they want.
Widget render state
You first need to implement the getWidgetRenderState
method. This is where you consume data from the API’s response.
It should return an object with the data and APIs you want to expose to the render function.
For the negative refinement list, you need to expose:
- The
items
to display in the list. - A
refine
function to trigger a new search from the UI with new items to exclude.
The widget parameters are also passed under the widgetParams
key. This is necessary for internal purposes.
In the returned object from the negativeRefinementList
function, add a function getWidgetRenderState
.
1
2
3
4
5
6
7
8
9
10
11
12
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
getWidgetRenderState({ results, helper }) {
// …
}
}
}
}
// …
You need to be able to refine results when selecting a refinement. To do this, you can expose a refine
function to toggle the exclusion of a facet value for the given attribute.
The refine
function must keep the same reference whenever getWidgetRenderState
is called so that UI frameworks can consider it stable between renders. To do so, create a connectorState
object outside the widget and attach the refine
function to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
// An empty `connectorState` object is used to store information
// that needs to be shared across multiple method calls.
const connectorState = {};
return {
getWidgetRenderState({ results, helper }) {
// To ensure `refine` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.refine) {
connectorState.refine = (value) =>
helper.toggleFacetExclusion(attribute, value).search();
}
// …
}
}
}
}
// …
Next up, you need to be able to consume the facet values from the API’s response so you can display them on the UI, let users click on them, etc.
You can find them on the exposed results
and store them in an items
variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// Retrieve the facet values for the given attribute and sort them by
// ascending name. Store the facet values in the `items` variable.
const items = results.getFacetValues(attribute, {
sortBy: ['name:asc'],
}) || [];
// …
}
}
}
}
// …
You also need to handle what happens when there are no search results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// When there are no results, return the API with default values.
// It's helpful to render a default UI until results are available.
if (!results) {
return { items: [], refine: connectorState.refine, widgetParams };
}
// …
}
}
}
}
// …
Finally, you can return the items
containing the facet values and the refine
function. By convention, it’s recommended to also return the parameters passed to the widget under the widgetParams
key.
You can use all this data and APIs in the render function later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
return {
items,
// A function to toggle a value when selected.
// If the value is already excluded, the exclusion is unset.
// Otherwise, it's added to the exclusion list.
// Then, a search is triggered.
refine: connectorState.refine,
widgetParams,
};
}
}
}
}
// …
Global render state
In InstantSearch, each widget you add registers its render state in one global object. You need to specify how to store your widget render state in this global tree by implementing the getRenderState
method.
You might use multiple negative refinement lists in your application but with different attributes—for example, you might want to exclude by brand and by categories. Here, you want to store each widget’s render state individually so they don’t override each other.
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
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getRenderState(renderState, renderOptions) {
// The global render state is merged with a new one to store the render
// state of the current widget.
return {
...renderState,
negativeRefinementList: {
...renderState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's render state is stored
// by the `attribute` it impacts.
[attribute]: this.getWidgetRenderState(renderOptions),
},
};
},
};
};
}
// …
Set up the lifecycle
When you add InstantSearch widgets to your app, they go through several steps in response to internal events. These steps are the InstantSearch lifecycle.
You must register lifecycle hooks on your widget to run code at the init
, render
, and dispose
stages.
Use these functions to call the user-provided render and unmount functions with the correct information.
The lifecycle code shouldn’t change, so you can copy/paste the code from this step without worrying about modifying it. Most of the custom logic happens in getWidgetRenderState
.
The init
step runs when the app starts (before the initial search is performed). Don’t use this function to add Algolia-related logic. Instead, use getWidgetSearchParameters
, getWidgetUiState
, or getWidgetRenderState
.
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
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=true` lets you perform
// conditional logic in the render function.
true
);
},
// …
};
};
}
// …
The render
step runs whenever new results come back from Algolia. It’s usually triggered by search state changes, such as when a user submits a new query or clicks on a filter.
During this step, the widget can react to the updated search results by re-rendering with the new information. This lets the widget remain synchronized with the current state of the search experience.
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
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=false` lets you perform
// conditional logic in the render function.
false
);
},
// …
};
};
}
// …
The dispose
step runs when removing the widget. Use it to clean up anything the widget created during its “lifetime” such as search parameters, UI, and event listeners. This helps prevent memory leaks and ensures the widget doesn’t continue affecting the search experience once it’s no longer in use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
dispose(disposeOptions) {
unmountFn();
},
// …
};
};
}
// …
Interact with routing
An important aspect of building an InstantSearch widget is how to make it work with routing. Your custom widget should be able to synchronize its state with the browser URL so you can share a link to your search experience in any given state.
Setting the widget UI state
In InstantSearch, routing uses an internal uiState
object to derive the route. As with the render state, you need to specify how to store your widget UI state in the global UI state by implementing the getWidgetUiState
method.
As with getRenderState
, since you might use the widget multiple times with different attributes, you need to store each widget’s UI state individually so they don’t override each other.
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
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetUiState(uiState, { searchParameters }) {
// The global UI state is merged with a new one to store the UI
// state of the current widget.
return {
...uiState,
negativeRefinementList: {
...uiState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's UI state is stored by
// the `attribute` it impacts.
[attribute]: searchParameters.getExcludeRefinements(attribute),
},
};
},
};
};
}
// …
Setting the widget search parameters
When you initialize its state from a URL, InstantSearch needs to know how to convert it into search parameters so it can trigger its first search.
You can specify how to derive search parameters from the current UI state by implementing the getWidgetSearchParameters
method. It gives you access to the current search parameters that you can modify using the widget parameters and current UI state, then return.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetSearchParameters(searchParameters, { uiState }) {
const state = searchParameters.addFacet(attribute);
const values = uiState.negativeRefinementList?.[attribute];
if (Array.isArray(values)) {
return values.reduce(
(acc, curr) => acc.addExcludeRefinement(attribute, curr),
state
);
}
return state;
},
};
};
}
// …
Sending events to the Insights API
To better understand your users, you could capture when they use the widget to exclude refinements. The Insights API lets you collect such events from the frontend so that you can, later on, unlock features such as Algolia Recommend, Click, conversion, and revenue analytics, and many more.
You can set up your widget so it automatically sends the right events to Algolia Insights when using the Insights middleware.
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
42
43
44
45
46
47
48
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// To ensure `sendEvent` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.sendEvent) {
connectorState.sendEvent = (
eventType,
facetValue,
eventName = 'Negative Filter Applied'
) => {
if (helper.state.isExcludeRefined(attribute, facetValue)) {
instantSearchInstance.sendEventToInsights({
insightsMethod: 'clickedFilters',
widgetType: this.$$type,
eventType,
payload: {
eventName,
index: helper.getIndex(),
filters: [`${attribute}:-${facetValue}`],
},
attribute,
});
}
};
}
if (!connectorState.refine) {
connectorState.refine = (value) => {
helper.toggleFacetExclusion(attribute, value);
// Send `click` event once the facet is toggled.
connectorState.sendEvent('click', value);
return helper.search();
};
}
// …
},
};
};
}
// …
Now when clicking on a refinement, it automatically sends an event to Algolia Insights. Note that this can only work when providing an Insights client with the Insights middleware.
You can make the connector even more flexible by providing the sendEvent
function to the render function. This lets you customize events depending on the use case.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// …
return {
// …
sendEvent: connectorState.sendEvent,
};
},
};
};
}
// …
Using a custom connector as a Hook
To make a connector more idiomatically consumable in a React context, you need to turn it into a Hook.
React InstantSearch exposes useConnector()
to use InstantSearch.js connectors as Hooks.
1
2
3
4
5
6
7
8
9
10
import { useConnector } from 'react-instantsearch';
import { connectNegativeRefinementList } from './connectNegativeRefinementList';
export function useNegativeRefinementList(props, additionalWidgetProperties) {
return useConnector(
connectNegativeRefinementList,
props,
additionalWidgetProperties
);
}
You can use the Hook in any React component nested under <InstantSearch>
to consume the uiState
from your negative refinement and interact with it.
1
2
3
4
5
6
7
function NegativeCategoriesList() {
const { items, refine } = useNegativeRefinementList({
attribute: 'brand',
});
return <>{/* Your JSX */}</>;
}
Rendering a custom user interface
An InstantSearch widget is a custom connector with a render function. In a React context, it translates to a component that consumes a Hook and renders a UI.
In this example, the <NegativeRefinementList>
component uses the useNegativeRefinementList()
Hook to build a reactive, stateful UI. This widget is usable in any React InstantSearch app.
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
42
43
44
45
46
47
import React from 'react';
import { useNegativeRefinementList } from './useNegativeRefinementList';
export function NegativeRefinementList(props) {
const { items, refine, canRefine } = useNegativeRefinementList(props, {
// This is helpful for debugging purposes and allows to differentiate
// between the connector and the widget.
$$widgetType: 'myOrganization.negativeRefinementList',
});
return (
<div
className={cx(
'ais-NegativeRefinementList',
!canRefine && 'ais-NegativeRefinementList--noRefinement'
)}
>
<ul className="ais-NegativeRefinementList-list">
{items.map((item) => (
<li
key={item.name}
className={cx(
'ais-NegativeRefinementList-item',
item.isExcluded && 'ais-NegativeRefinementList-item--selected'
)}
>
<label className="ais-NegativeRefinementList-label">
<input
checked={item.isExcluded}
type="checkbox"
className="ais-NegativeRefinementList-checkbox"
value={item.name}
onChange={() => refine(item.name)}
/>
<span className="ais-NegativeRefinementList-labelText">{item.name}</span>
<span className="ais-NegativeRefinementList-count">{item.count}</span>
</label>
</li>
))}
</ul>
</div>
);
}
function cx(...classNames) {
return classNames.filter(Boolean).join(' ');
}
InstantSearch widgets use a standardized class naming convention.
The widget is now usable in an InstantSearch application. Still, if you want to reuse or distribute the widget, you can further tweak the API to use the same standards as the built-in React InstantSearch widgets.
Making the widget reusable
You might want to reuse your widget within your app, share it across multiple projects, or even publish it on npm for others to enjoy. To do so, you can provide APIs to allow customization while abstracting the complexity away.
InstantSearch exposes consistent APIs. You can follow the same guidelines and conventions in your own widgets and connectors.
Forwarding root props
A good first step is to let users forward props to the root element. This is useful for basic styling, accessibility, and testing.
1
2
3
4
5
6
7
8
9
export function NegativeRefinementList({ attribute, ...props }) {
// …
return (
+ <div {...props} className={{/* … */}}>
{/* … */}
</div>
);
}
Exposing standard classes
Widgets expose classes on every DOM element to help users style them easily.
Built-in InstantSearch widgets use the SUITCSS component syntax:
1
[<namespace>-]<ComponentName>[-descendentName][--modifierName]
- Every class starts with the
ais-
namespace (for Algolia InstantSearch). This helps target all InstantSearch elements with selectors like[class^="ais-"]
. - Every class has a component name mapped to the widget name. In the example on this page, the widget uses the
NegativeRefinementList
component name. Component names are always in Pascal case. - Since every element has its own classes, they must be identified with a “descendent name”. In the example on this page, each item of the widget uses the
-item
descendent name. Descendent names are always in camel case. - If an element has multiple states, identify each state with a “modifier”. In the example on this page, the selected item of the widget uses the
--selected
modifier. Modifiers are always in camel case and prefixed with two hyphens. You should include the modified class on the element in addition to the base component class (for example,ais-NegativeRefinementList-item
andais-NegativeRefinementList-item--selected
).
See these conventions in action.
If you’re using a CSS linter like Stylelint, you can validate your InstantSearch classes with the following regular expression:
1
^ais-(?:[A-Z][a-z]+)+(?:-[a-z]+(?:[A-Z][a-z]+)*)?(?:--[a-z]+(?:[A-Z][a-z]+)*)?$
Passing custom classes
Widgets expose standardized class names to let users write custom CSS, but you could even open the styling API further to allow passing classes directly on each element. This lets users of class-based CSS frameworks like Bootstrap or Tailwind CSS leverage them without friction or workarounds.
In built-in widgets, the convention is to provide a prop that takes an object of named classes.
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export function NegativeRefinementList({
attribute,
classNames = {},
...props
}) {
// …
return (
<div
{...props}
className={cx(
'ais-NegativeRefinementList',
classNames.root,
!canRefine &&
cx('ais-NegativeRefinementList--noRefinement', classNames.noRefinementRoot),
props.className
)}
>
<ul className={cx('ais-NegativeRefinementList-list', classNames.list)}>
{items.map((item) => (
<li
key={item.name}
className={cx(
'ais-NegativeRefinementList-item',
classNames.item,
item.isExcluded &&
cx('ais-NegativeRefinementList-item--selected', classNames.selectedItem)
)}
>
<label className={cx('ais-NegativeRefinementList-label', classNames.label)}>
<input
checked={item.isExcluded}
type="checkbox"
className={cx(
'ais-NegativeRefinementList-checkbox',
classNames.checkbox
)}
value={item.name}
onChange={() => refine(item.name)}
/>
<span
className={cx(
'ais-NegativeRefinementList-labelText',
classNames.labelText
)}
>
{item.name}
</span>
<span
className={cx('ais-NegativeRefinementList-count', classNames.count)}
>
{item.count}
</span>
</label>
</li>
))}
</ul>
</div>
);
}
Customizing the UI
UI customizability is an important aspect of building reusable InstantSearch widgets. If you need to internationalize your app, control the markup, or change icons, you shouldn’t have to opt out of using widgets and resort to using connectors just to tweak the UI.
Translations
In React InstantSearch, widgets expose a translations
prop—a dictionary to customize the UI text and support internationalization.
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
export function NegativeRefinementList({
attribute,
translations,
classNames = {},
...props
}) {
// …
const { countText } = {
countText({ count }) {
return `${count} hits`;
},
...translations,
};
return (
<div className={cx('ais-NegativeRefinementList' /* … */)}>
<ul className="ais-NegativeRefinementList-list">
{items.map((item) => (
<li
key={item.name}
className={cx('ais-NegativeRefinementList-item' /* … */)}
>
<label className="ais-NegativeRefinementList-label">
{/* … */}
<span className="ais-NegativeRefinementList-count">
{countText({ count: item.count })}
</span>
</label>
</li>
))}
</ul>
</div>
);
}
Translations are usually strings, but you can use functions that return strings if you need to expose data.
Templates
If users need to control the markup, they shouldn’t have to immediately reach out to connectors. Instead, you can let them template parts of the UI. For example, users may want to change the rendering of each item to display a custom checkbox that requires additional markup.
In React InstantSearch, widgets expose component props that accept React components.
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
42
43
export function NegativeRefinementList({
attribute,
listItemComponent: ListItemIcon = DefaultListItemIcon,
translations,
classNames = {},
...props
}) {
// …
return (
<div className={cx('ais-NegativeRefinementList' /* … */)}>
<ul className={cx('ais-NegativeRefinementList-list' /* … */)}>
{items.map((item) => (
<ListItemIcon
key={item.name}
item={item}
translations={{/* … */}}
classNames={{/* … */}}
/>
))}
</ul>
</div>
);
}
function DefaultListItemIcon({ item, classNames, translations }) {
return (
<li
className={classNames.item}
className={cx(
classNames.item,
item.isExcluded && classNames.selectedItem
)}
>
<label className={classNames.label}>
{/* … */}
<span className={classNames.count}>
{translations.countText({ count: item.count })}
</span>
</label>
</li>
);
}
Exposing standard options
Each widget and connector exposes options that cater to their unique use case. Still, some options are consistent across several widgets and connectors to fix common issues and provide a homogenous experience.
Beyond root props, classes, translations and templates, here’s a list of options you can expose on your widgets and connectors when they make sense.
Targeting a record attribute
When a widget needs to target a specific record attribute, you should expose an attribute
option in the connector. This is the case for most refinement widgets like <RefinementList>
so you can select the specific attribute to refine.
The attribute
option typically accepts a string. For deeply nested objects, you can accept dot-separated values like "brand.name"
or an array of strings like ["brand", "name"]
.
Including or excluding attributes
When a widget manipulates selected refinements, you may want to let users select what attributes to include or exclude. This is the case with widgets like <CurrentRefinements>
or <ClearRefinements>
so you can hand-pick exactly what attributes to manipulate.
Limiting items
When a widget manipulates items, notably facet refinements, you may want to let users limit how many of them to retrieve. Widgets like <RefinementList>
or <Menu>
expose a limit
option, which is directly forwarded to the Algolia search engine with the maxValuesPerFacet
search parameter.
Such widgets usually expose two extra options to let users toggle more facets: showMore
, a boolean option to enable the feature, and showMoreLimit
, to define the maximum number of items to display if the widget is showing more items.
Transforming items
When a widget manipulates items like hits or facets, users may want to change them before rendering. To do so, you should expose a transformItems
option for transforming, removing, or reordering items on the connector.
The transformItems
option is a function that receives the items and should return a new array of the same shape. The default value is an identity function.
Using the custom widget
You can use a custom widget like any widget provided by the library. It takes the parameters to forward to the connectors, HTML props for the root element, and an object for classes.
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
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch';
import { NegativeRefinementList } from './NegativeRefinementList';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
function App() {
return (
<InstantSearch searchClient={searchClient} indexName="instant_search">
{/* … */}
<NegativeRefinementList
// Hook parameters
attribute="brand"
// HTML root props
title="My custom title"
// Custom class names
classNames={{
root: 'MyCustomNegativeRefinementList',
item: 'MyCustomNegativeRefinementListItem',
}}
/>
</InstantSearch>
);
}
This is what the widget looks like in an application.
Next steps
You now have a good starting point to take full control of your InstantSearch experience. Next up, you could go further by:
- Improving the widget’s API with useful parameters like the
connectRefinementList
ones. - Open sourcing your custom widget on GitHub and publish it on npm (if you do, let us know!).