Guides / Building Search UI / UI & UX patterns

Infinite Scroll with React InstantSearch

We released React InstantSearch Hooks, a new InstantSearch library for React. We recommend using React InstantSearch Hooks in new projects or upgrading from React InstantSearch.

The infinite list is a very common pattern to display a list of results. This pattern has two variants:

  • With a button to click on at the end of the list of results
  • With a listener on the scroll event that is called when the list of results reaches the end

React InstantSearch can cover those two different implementations. The former is covered by the built-in InfiniteHits widget and the latter is covered by the connectInfiniteHits connector. This guide focuses on the second implementation using the Intersection Observer API. A browser API is used in the example, but the concepts can be applied to any kind of infinite scroll library. You can find the complete example on GitHub.

The Intersection Observer API isn’t yet widely supported.

Display a list of hits

The first step to creating the infinite scroll component is to render the results with the connectInfiniteHits connector. There’s an external Hit component but it’s not the point of this guide, the intent is to keep the code simple. You can find more information about the connectors API in the dedicated guide about widget customisation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react';
import { connectInfiniteHits } from 'react-instantsearch-dom';
import Hit from './Hit';

class InfiniteHits extends Component {
  render() {
    const { hits } = this.props;

    return (
      <div className="ais-InfiniteHits">
        <ul className="ais-InfiniteHits-list">
          {hits.map(hit => (
            <li key={hit.objectID} className="ais-InfiniteHits-item">
              <Hit hit={hit} />
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default connectInfiniteHits(InfiniteHits);

Track the scroll position

Once you have your list of results, the next step is to track the scroll position to determine when the rest of the content needs to be loaded. For this purpose, the Intersection Observer API is used. To track when the bottom of the list enters the viewport, you observe a “sentinel” element. This trick is used to avoid observing all the items of the results. You can reuse the same element across the different renders. You can find more information about this pattern on the Web Fundamentals website.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class InfiniteHits extends Component {
  sentinel = null;

  render() {
    const { hits } = this.props;

    return (
      <div className="ais-InfiniteHits">
        <ul className="ais-InfiniteHits-list">
          {hits.map(hit => (
            <li key={hit.objectID} className="ais-InfiniteHits-item">
              <Hit hit={hit} />
            </li>
          ))}
          <li
            className="ais-InfiniteHits-sentinel"
            ref={c => (this.sentinel = c)}
          />
        </ul>
      </div>
    );
  }
}

Once you have the ref of the “sentinel” element, you can create the Intersection Observer instance to observe when the element enters the page. You can provide options to the Intersection Observer API to adjust the example to your needs.

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
35
class InfiniteHits extends Component {
  onSentinelIntersection = entries => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // In that case we can refine
      }
    });
  };

  componentDidMount() {
    this.observer = new IntersectionObserver(this.onSentinelIntersection);

    this.observer.observe(this.sentinel);
  }

  render() {
    const { hits } = this.props;

    return (
      <div className="ais-InfiniteHits">
        <ul className="ais-InfiniteHits-list">
          {hits.map(hit => (
            <li key={hit.objectID} className="ais-InfiniteHits-item">
              <Hit hit={hit} />
            </li>
          ))}
          <li
            className="ais-InfiniteHits-sentinel"
            ref={c => (this.sentinel = c)}
          />
        </ul>
      </div>
    );
  }
}

Don’t forget to clear the observer once the component is unmounted.

1
2
3
4
5
6
7
8
9
class InfiniteHits extends Component {
  componentWillUnmount() {
    this.observer.disconnect();
  }

  render() {
    // ...
  }
}

Retrieve more results

Now that you can track when you reach the end of the results, you can use the refineNext function inside the callback function onSentinelIntersection. But you should only trigger the function when there are still have results to retrieve. For this use case, the connector provides a prop hasMore that indicates if you still have results or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class InfiniteHits extends Component {
  onSentinelIntersection = entries => {
    const { hasMore, refineNext } = this.props;

    entries.forEach(entry => {
      if (entry.isIntersecting && hasMore) {
        refineNext();
      }
    });
  };

  render() {
    // ...
  }
}

Go further than 1000 hits

By default Algolia limits the number of hits you can retrieve for a query to 1000; when doing an infinite scroll, you usually don’t want to go over this limit.

1
2
3
$index->setSettings([
  'paginationLimitedTo' => 1000
]);

Disabling the limit doesn’t mean that 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.

Now you should have a complete infinite scroll experience.

The complete source code of the example is on GitHub.

Did you find this page helpful?