The useInfiniteHits hook provides the logic to build infinite scroll pagination for search results.
Import
import { useInfiniteHits } from 'react-instantsearch';
Parameters
Whether to escape HTML tags in hit string values.const { items } = useInfiniteHits({ escapeHTML: false });
Enable loading previous results.const { showPrevious, isFirstPage } = useInfiniteHits({
showPrevious: true
});
Function to transform the hits before rendering.const { items } = useInfiniteHits({
transformItems: (items) =>
items.map((item) => ({
...item,
formattedPrice: `$${item.price.toFixed(2)}`,
})),
});
Custom cache implementation for storing hits between page navigations.const cache = {
read: ({ state }) => {
return sessionStorage.getItem(JSON.stringify(state));
},
write: ({ state, hits }) => {
sessionStorage.setItem(JSON.stringify(state), JSON.stringify(hits));
},
};
const hook = useInfiniteHits({ cache });
Returns
All accumulated hits from the current and previous pages.const { items } = useInfiniteHits();
console.log(items.length); // Total accumulated hits
Alias for items (deprecated).Use items instead. The hits property is deprecated.
Hits for the current page only (not accumulated).const { currentPageHits } = useInfiniteHits();
console.log(currentPageHits.length); // Hits on current page
Function to load the next page of results.const { showMore, isLastPage } = useInfiniteHits();
<button onClick={showMore} disabled={isLastPage}>
Load More
</button>
Function to load the previous page of results.const { showPrevious, isFirstPage } = useInfiniteHits({
showPrevious: true
});
<button onClick={showPrevious} disabled={isFirstPage}>
Load Previous
</button>
Whether the current page is the first page.const { isFirstPage } = useInfiniteHits();
Whether the current page is the last page.const { isLastPage } = useInfiniteHits();
The complete search results object from Algolia.const { results } = useInfiniteHits();
console.log(results?.nbHits);
The banner to display above the hits.const { banner } = useInfiniteHits();
Function to send events to the Insights API.const { sendEvent } = useInfiniteHits();
sendEvent('click', items[0], 'Product Clicked');
Function to bind Insights events to HTML elements.const { bindEvent } = useInfiniteHits();
Examples
import { useInfiniteHits } from 'react-instantsearch';
function InfiniteHits() {
const { items, showMore, isLastPage } = useInfiniteHits();
return (
<div>
<div className="hits">
{items.map((item) => (
<div key={item.objectID}>
<h3>{item.name}</h3>
<p>{item.description}</p>
</div>
))}
</div>
{!isLastPage && (
<button onClick={showMore}>Load More</button>
)}
</div>
);
}
With Intersection Observer
import { useInfiniteHits } from 'react-instantsearch';
import { useEffect, useRef } from 'react';
function AutoLoadInfiniteHits() {
const { items, showMore, isLastPage } = useInfiniteHits();
const sentinelRef = useRef(null);
useEffect(() => {
if (!sentinelRef.current || isLastPage) return;
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
showMore();
}
});
observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [showMore, isLastPage]);
return (
<div>
<div className="hits">
{items.map((item) => (
<div key={item.objectID}>
<h3>{item.name}</h3>
</div>
))}
</div>
{!isLastPage && <div ref={sentinelRef} className="sentinel" />}
</div>
);
}
With Previous Results
import { useInfiniteHits } from 'react-instantsearch';
function BidirectionalInfiniteHits() {
const {
items,
showMore,
showPrevious,
isFirstPage,
isLastPage
} = useInfiniteHits({ showPrevious: true });
return (
<div>
{!isFirstPage && (
<button onClick={showPrevious}>Load Previous</button>
)}
<div className="hits">
{items.map((item) => (
<div key={item.objectID}>
<h3>{item.name}</h3>
</div>
))}
</div>
{!isLastPage && (
<button onClick={showMore}>Load More</button>
)}
</div>
);
}
With Loading State
import { useInfiniteHits, useInstantSearch } from 'react-instantsearch';
import { useState } from 'react';
function InfiniteHitsWithLoading() {
const { items, showMore, isLastPage } = useInfiniteHits();
const { status } = useInstantSearch();
const [isLoadingMore, setIsLoadingMore] = useState(false);
const handleShowMore = async () => {
setIsLoadingMore(true);
showMore();
// Reset loading state after a short delay
setTimeout(() => setIsLoadingMore(false), 300);
};
return (
<div>
<div className="hits">
{items.map((item) => (
<div key={item.objectID}>
<h3>{item.name}</h3>
</div>
))}
</div>
{status === 'loading' && items.length === 0 && (
<div>Loading initial results...</div>
)}
{!isLastPage && (
<button onClick={handleShowMore} disabled={isLoadingMore}>
{isLoadingMore ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
With TypeScript
import { useInfiniteHits } from 'react-instantsearch';
interface Product {
objectID: string;
name: string;
price: number;
image: string;
}
function ProductInfiniteHits() {
const { items, showMore, isLastPage } = useInfiniteHits<Product>();
return (
<div>
<div className="products">
{items.map((item) => (
<div key={item.objectID}>
<img src={item.image} alt={item.name} />
<h3>{item.name}</h3>
<p>${item.price.toFixed(2)}</p>
</div>
))}
</div>
{!isLastPage && (
<button onClick={showMore}>Load More</button>
)}
</div>
);
}
import { useInfiniteHits } from 'react-instantsearch';
import { useVirtual } from 'react-virtual';
import { useRef } from 'react';
function VirtualInfiniteHits() {
const { items, showMore, isLastPage } = useInfiniteHits();
const parentRef = useRef();
const rowVirtualizer = useVirtual({
size: items.length,
parentRef,
estimateSize: () => 100,
overscan: 5,
});
// Load more when approaching the end
const lastItem = rowVirtualizer.virtualItems[
rowVirtualizer.virtualItems.length - 1
];
if (lastItem && lastItem.index >= items.length - 5 && !isLastPage) {
showMore();
}
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: `${rowVirtualizer.totalSize}px`,
position: 'relative',
}}
>
{rowVirtualizer.virtualItems.map((virtualRow) => (
<div
key={items[virtualRow.index].objectID}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<h3>{items[virtualRow.index].name}</h3>
</div>
))}
</div>
</div>
);
}
With Cache
import { useInfiniteHits } from 'react-instantsearch';
const cache = {
read({ state }) {
const key = JSON.stringify(state);
const cached = sessionStorage.getItem(key);
return cached ? JSON.parse(cached) : null;
},
write({ state, hits }) {
const key = JSON.stringify(state);
sessionStorage.setItem(key, JSON.stringify(hits));
},
};
function CachedInfiniteHits() {
const { items, showMore, isLastPage } = useInfiniteHits({ cache });
return (
<div>
<div className="hits">
{items.map((item) => (
<div key={item.objectID}>
<h3>{item.name}</h3>
</div>
))}
</div>
{!isLastPage && (
<button onClick={showMore}>Load More</button>
)}
</div>
);
}
Notes
The hook accumulates all hits from previous pages. If you need only the current page hits, use the currentPageHits property.
Be mindful of memory usage when loading many pages. Consider implementing virtual scrolling or pagination for very large result sets.