Guides / Building Search UI / UI & UX patterns / Infinite scroll

Infinite scroll with React InstantSearch Hooks

Displaying an infinite list of hits is a common pattern to continuously load content when reaching the end. The user no longer needs to deal with pagination, making infinite hits well suited for mobile devices.

There are two variants of this pattern:

  • With a button to click on at the end of the list of results
  • With automatic loading of new hits when reaching the end of the list—or “infinite scroll”

You can implement both variants with React InstantSearch Hooks.

This guide focuses on building an infinite scroll widget using useInfiniteHits() and the Intersection Observer API.

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-hooks-web';

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

Now you have your list of results, you need to track the scroll position to determine when to load the rest of the content. To do so, you can use the Intersection Observer API.

To track when the bottom of the list enters the viewport, you observe a sentinel element. This element isn’t visible to the user.

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>
  );
}

You can now create an Intersection Observer instance to observe when the sentinel element enters the page. Don’t forget to 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, you can 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

By default, Algolia limits the total number of hits you can retrieve for a query to 1,000, which guarantees excellent performance. When building an infinite scroll, you should stay within this limit.

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 guarantee you can view all hits, but that Algolia goes as far as possible to retrieve results in a reasonable time span.

Next steps

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

Did you find this page helpful?