🎉 Try the public beta of the new docs site at algolia.com/doc-beta! 🎉
Guides / Building Search UI / UI & UX patterns

Infinite scroll with React InstantSearch

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.

An “infinite list” is a common way of displaying results. It’s especially well-suited to mobile devices and has two variants:

  • Infinite hits with a “See more” button at the end of a batch of results. Implement this with InstantSearch’s <InfiniteHits> widget.
  • Infinite scroll uses a listener on the scroll event (called when the user has scrolled to the end of the first batch of results). The following guidance implements such an infinite scroll.

If there are no hits, you should display a message to users and clear filters so they can start over.

Display a list of hits

The first step is to render the results with the useInfiniteHits() Hook.

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 React from 'react';
import { Highlight, useInfiniteHits, Snippet } from 'react-instantsearch';

export function InfiniteHits(props) {
  const { hits } = useInfiniteHits(props);

  return (
    <div className="ais-InfiniteHits">
      <ul className="ais-InfiniteHits-list">
        {hits.map((hit) => (
          <li key={hit.objectID} className="ais-InfiniteHits-item">
            <article>
              <h2>
                <Highlight attribute="name" hit={hit} />
              </h2>
              <p>
                <Snippet attribute="description" hit={hit} />
              </p>
            </article>
          </li>
        ))}
      </ul>
    </div>
  );
}

Track the scroll position

Once you have your results, the next step is to track the scroll position to determine when the rest of the content needs to be loaded (using the Intersection Observer API). Use the API to track when the bottom of the list (the “sentinel” element) enters the viewport. You can reuse the same element across different renders.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useRef } from 'react';
// ...

export function InfiniteHits(props) {
  const { hits } = useInfiniteHits(props);
  const sentinelRef = useRef(null);

  return (
    <div className="ais-InfiniteHits">
      <ul className="ais-InfiniteHits-list">
        {/* ... */}
        <li ref={sentinelRef} aria-hidden="true" />
      </ul>
    </div>
  );
}

This implementation uses the Intersection Observer API. However, since the Intersection Observer API isn’t widely supported, consider using a polyfill. A browser API is used in the example, but you can apply the concepts to any infinite scroll library.

Now create an Intersection Observer instance to observe when the sentinel element enters the page. Disconnect the observer in the effect’s cleanup.

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
import React, { useEffect, useRef } from 'react';
// ...

export function InfiniteHits(props) {
  const { hits, isLastPage, showMore } = useInfiniteHits(props);
  const sentinelRef = useRef(null);

  useEffect(() => {
    if (sentinelRef.current !== null) {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !isLastPage) {
            // Load more hits
          }
        });
      });

      observer.observe(sentinelRef.current);

      return () => {
        observer.disconnect();
      };
    }
  }, [isLastPage, showMore]);

  return (
    <div className="ais-InfiniteHits">
      <ul className="ais-InfiniteHits-list">
        {/* ... */}
        <li ref={sentinelRef} aria-hidden="true" />
      </ul>
    </div>
  );
}

Retrieve more results

Now that you can track when you reach the bottom of the list, call the showMore function inside the observer’s callback. The Hook exposes whether there are more results to load with isLastPage, allowing you to conditionally call showMore.

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 InfiniteHits(props) {
  const { hits, isLastPage, showMore } = useInfiniteHits(props);
  const sentinelRef = useRef(null);
  
  useEffect(() => {
    if (sentinelRef.current !== null) {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !isLastPage) {
            showMore();
          }
        });
      });

      observer.observe(sentinelRef.current);

      return () => {
        observer.disconnect();
      };
    }
  }, [isLastPage, showMore]);

  // ...
}

Show more than 1,000 hits

To ensure excellent performance, the default limit for the number of hits you can retrieve for a query is 1,000.

If you need to show more than 1,000 hits, you can set a different limit using the paginationLimitedTo parameter. The higher you set this limit, the slower your search performance can become.

Increasing the limit doesn’t mean you can go until the end of the hits, but just that Algolia will go as far as possible in the index to retrieve results in a reasonable time.

Next steps

You now have a good starting point to create an even richer experience with React InstantSearch. Next, you could improve this app by:

Did you find this page helpful?