React InstantSearch provides hooks for building custom components while leveraging the library’s search state management.
Core Hooks
useInstantSearch
Access the global InstantSearch context:
import { useInstantSearch } from 'react-instantsearch';
function SearchStats() {
const {
results,
scopedResults,
indexUiState,
setIndexUiState,
renderState,
status,
error,
refresh,
} = useInstantSearch();
if (status === 'loading' || status === 'stalled') {
return <p>Loading...</p>;
}
if (status === 'error') {
return <p>Error: {error?.message}</p>;
}
return (
<div>
<p>{results?.nbHits.toLocaleString()} results</p>
<button onClick={() => refresh()}>
Refresh results
</button>
</div>
);
}
Return Values:
Current search results for the main index
Results for all active indices (including nested <Index> components)
Current UI state for the current index
Update the UI state programmatically
Update UI state for all indices
status
'idle' | 'loading' | 'stalled' | 'error'
Current search status
Error object if status is ‘error’
Clear cache and trigger a new search
Error Handling
Catch errors with the catchError option:
function SearchWithErrorBoundary() {
const { status, error } = useInstantSearch({ catchError: true });
if (status === 'error') {
return (
<div className="error">
<h2>Search Error</h2>
<p>{error?.message}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return <SearchResults />;
}
Connector Hooks
Connector hooks provide direct access to widget state and actions.
useSearchBox
Build a custom search input:
import { useSearchBox } from 'react-instantsearch';
import { useState, useRef, useEffect } from 'react';
function CustomSearchBox() {
const { query, refine, clear, isSearchStalled } = useSearchBox();
const [value, setValue] = useState(query);
const inputRef = useRef<HTMLInputElement>(null);
// Sync with InstantSearch query
useEffect(() => {
if (query !== value && document.activeElement !== inputRef.current) {
setValue(query);
}
}, [query]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
refine(value);
};
return (
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
type="search"
value={value}
onChange={(e) => {
setValue(e.target.value);
refine(e.target.value); // Search as you type
}}
placeholder="Search..."
/>
{isSearchStalled && <span>Loading...</span>}
<button type="button" onClick={() => {
setValue('');
clear();
}}>
Clear
</button>
</form>
);
}
Props:
queryHook
(query: string, search: (value: string) => void) => void
Modify or debounce queries before searching
Return Values:
Whether the search is stalled
Debounced Search
function DebouncedSearchBox() {
const { query, refine } = useSearchBox({
queryHook(query, search) {
// Debounce for 300ms
const timeoutId = setTimeout(() => search(query), 300);
return () => clearTimeout(timeoutId);
},
});
return (
<input
type="search"
value={query}
onChange={(e) => refine(e.target.value)}
/>
);
}
useHits
Custom results component:
import { useHits } from 'react-instantsearch';
import type { BaseHit } from 'instantsearch.js';
interface Product extends BaseHit {
name: string;
price: number;
image: string;
inStock: boolean;
}
function CustomHits() {
const { hits, results, sendEvent } = useHits<Product>({
escapeHTML: true,
transformItems: (items) =>
items.filter(item => item.inStock),
});
if (!results) {
return <p>No results</p>;
}
return (
<div className="grid">
{hits.map((hit) => (
<article
key={hit.objectID}
onClick={() => {
sendEvent('click', hit, 'Product Clicked');
}}
>
<img src={hit.image} alt={hit.name} />
<h3>{hit.name}</h3>
<p>${hit.price}</p>
</article>
))}
</div>
);
}
Props:
Escape HTML entities in results
Transform hits before rendering
Return Values:
Full Algolia search response
Banner from Query Rules (if any)
useRefinementList
Custom faceted filter:
import { useRefinementList } from 'react-instantsearch';
function CustomRefinementList({ attribute }: { attribute: string }) {
const {
items,
refine,
canRefine,
isFromSearch,
searchForItems,
canToggleShowMore,
isShowingMore,
toggleShowMore,
} = useRefinementList({
attribute,
limit: 5,
showMore: true,
showMoreLimit: 15,
sortBy: ['isRefined', 'count:desc', 'name:asc'],
});
const [query, setQuery] = useState('');
if (!canRefine) {
return null;
}
return (
<div>
<input
type="search"
value={query}
onChange={(e) => {
setQuery(e.target.value);
searchForItems(e.target.value);
}}
placeholder={`Search ${attribute}...`}
/>
<ul>
{items.map((item) => (
<li key={item.value}>
<label>
<input
type="checkbox"
checked={item.isRefined}
onChange={() => refine(item.value)}
/>
{item.label} ({item.count})
</label>
</li>
))}
</ul>
{canToggleShowMore && (
<button onClick={toggleShowMore}>
{isShowingMore ? 'Show less' : 'Show more'}
</button>
)}
{isFromSearch && items.length === 0 && (
<p>No results for "{query}"</p>
)}
</div>
);
}
Props:
Index attribute to filter on
operator
'or' | 'and'
default:"'or'"
How to combine multiple selections
Enable show more functionality
Sort criteria (e.g., ['isRefined', 'count:desc'])
Custom pagination:
import { usePagination } from 'react-instantsearch';
function CustomPagination() {
const {
currentRefinement,
nbPages,
pages,
isFirstPage,
isLastPage,
canRefine,
refine,
} = usePagination({ padding: 2 });
if (!canRefine) {
return null;
}
return (
<nav>
<button
onClick={() => refine(currentRefinement - 1)}
disabled={isFirstPage}
>
Previous
</button>
{pages.map((page) => (
<button
key={page}
onClick={() => refine(page)}
disabled={page === currentRefinement}
className={page === currentRefinement ? 'active' : ''}
>
{page + 1}
</button>
))}
<button
onClick={() => refine(currentRefinement + 1)}
disabled={isLastPage}
>
Next
</button>
</nav>
);
}
useRange
Numeric range filter:
import { useRange } from 'react-instantsearch';
function CustomPriceRange() {
const { start, range, canRefine, refine } = useRange({
attribute: 'price',
});
const [values, setValues] = useState([start[0] ?? 0, start[1] ?? 1000]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
refine(values);
};
if (!canRefine) {
return null;
}
return (
<form onSubmit={handleSubmit}>
<label>
Min: ${range.min}
<input
type="number"
min={range.min}
max={range.max}
value={values[0]}
onChange={(e) => setValues([+e.target.value, values[1]])}
/>
</label>
<label>
Max: ${range.max}
<input
type="number"
min={range.min}
max={range.max}
value={values[1]}
onChange={(e) => setValues([values[0], +e.target.value])}
/>
</label>
<button type="submit">Apply</button>
</form>
);
}
All Available Hooks
Search Hooks
useSearchBox - Search input
useAutocomplete - Autocomplete/suggestions
useVoiceSearch - Voice search (browser API)
Results Hooks
useHits - Search results
useInfiniteHits - Infinite scroll results
useStats - Result statistics
Filter Hooks
useRefinementList - Multi-select facets
useMenu - Single-select facets
useHierarchicalMenu - Nested categories
useRange - Numeric range
useNumericMenu - Predefined numeric ranges
useToggleRefinement - Boolean filter
useCurrentRefinements - Active filters
useClearRefinements - Clear filters
useBreadcrumb - Hierarchical breadcrumb
Navigation Hooks
usePagination - Page-based navigation
useSortBy - Sort order selection
Advanced Hooks
useConfigure - Set search parameters
useQueryRules - Access Query Rules data
useDynamicWidgets - Dynamically render widgets
Recommendation Hooks
useFrequentlyBoughtTogether - Product recommendations
useRelatedProducts - Related items
useTrendingItems - Trending content
useLookingSimilar - Similar items
For advanced use cases, create completely custom widgets with useConnector:
import { useConnector } from 'react-instantsearch';
import connectStats from 'instantsearch.js/es/connectors/stats/connectStats';
function useStats() {
return useConnector(connectStats);
}
function CustomStats() {
const { nbHits, processingTimeMS, query } = useStats();
return (
<p>
{query ? (
<>{nbHits.toLocaleString()} results for "{query}"</>
) : (
<>{nbHits.toLocaleString()} total results</>
)}
{' '}in {processingTimeMS}ms
</p>
);
}
Best Practices
1. Use TypeScript for Type Safety
import type { BaseHit } from 'instantsearch.js';
interface ProductRecord extends BaseHit {
name: string;
price: number;
}
const { hits } = useHits<ProductRecord>();
// `hits` is now typed as Hit<ProductRecord>[]
2. Memoize Callbacks
import { useCallback } from 'react';
const handleClick = useCallback((hit) => {
sendEvent('click', hit, 'Product Clicked');
}, [sendEvent]);
3. Handle Loading States
const { status, results } = useInstantSearch();
if (status === 'loading') return <Spinner />;
if (status === 'stalled') return <SlowSearchIndicator />;
if (status === 'error') return <ErrorMessage />;
4. Sync Local State Carefully
When syncing local state with search state, avoid infinite loops:
const { query, refine } = useSearchBox();
const [localValue, setLocalValue] = useState(query);
// Only sync when not focused
useEffect(() => {
if (document.activeElement !== inputRef.current) {
setLocalValue(query);
}
}, [query]);
Next Steps
Server Components
Use hooks with React Server Components
Next.js Integration
SSR and App Router patterns