Skip to main content
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:
results
SearchResults
Current search results for the main index
scopedResults
ScopedResult[]
Results for all active indices (including nested <Index> components)
indexUiState
object
Current UI state for the current index
setIndexUiState
function
Update the UI state programmatically
uiState
object
UI state for all indices
setUiState
function
Update UI state for all indices
status
'idle' | 'loading' | 'stalled' | 'error'
Current search status
error
Error | undefined
Error object if status is ‘error’
refresh
() => void
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:
query
string
Current search query
refine
(value: string) => void
Update the search query
clear
() => void
Clear the query
isSearchStalled
boolean
Whether the search is stalled
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:
escapeHTML
boolean
default:"true"
Escape HTML entities in results
transformItems
(items: Hit[]) => Hit[]
Transform hits before rendering
Return Values:
hits
Hit[]
Array of search results
results
SearchResults
Full Algolia search response
sendEvent
Function
Track Insights events
banner
object | undefined
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:
attribute
string
required
Index attribute to filter on
operator
'or' | 'and'
default:"'or'"
How to combine multiple selections
limit
number
default:"10"
Number of items to show
showMore
boolean
default:"false"
Enable show more functionality
sortBy
string[]
Sort criteria (e.g., ['isRefined', 'count:desc'])

usePagination

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
  • 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

Custom Widget Hook

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

Build docs developers (and LLMs) love