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

Infinite scroll with InstantSearch.js

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. Find the complete example on GitHub.

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 infiniteHits connector.

Read more about connectors in the customizing widgets guide.

All examples in this guide assume you’ve included InstantSearch.js in your web page from a CDN. If, instead, you’re using it with a package manager, adjust how you import InstantSearch.js and its widgets for more information.

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
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
  (renderArgs, isFirstRender) => {
    const { hits, showMore, widgetParams } = renderArgs;
    const { container } = widgetParams;

    if (isFirstRender) {
      container.appendChild(document.createElement('ul'));

      return;
    }

    container.querySelector('ul').innerHTML = hits
      .map(
        hit =>
          `<li>
            <div class="ais-Hits-item">
              <header class="hit-name">
                ${instantsearch.highlight({ attribute: 'name', hit })}
              </header>
              <img src="${hit.image}" align="left" />
              <p class="hit-description">
                ${instantsearch.highlight({ attribute: 'description', hit })}
              </p>
              <p class="hit-price">$${hit.price}</p>
            </div>
          </li>`
      )
      .join('');
  }
);

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. The Web Fundamentals website discusses the use of this API in more detail.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
  (renderArgs, isFirstRender) => {
    const { hits, showMore, widgetParams } = renderArgs;
    const { container } = widgetParams;

    if (isFirstRender) {
      const sentinel = document.createElement('div');
      container.appendChild(document.createElement('ul'));
      container.appendChild(sentinel);

      return;
    }

    // ...
  }
);

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.

Once you have the reference to the “sentinel” element, create the Intersection Observer instance to determine when this element enters the page. Update the Intersection Observer’s options object to adjust this code 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
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
  (renderArgs, isFirstRender) => {
    const { hits, showMore, widgetParams } = renderArgs;
    const { container } = widgetParams;

    if (isFirstRender) {
      const sentinel = document.createElement('div');
      container.appendChild(document.createElement('ul'));
      container.appendChild(sentinel);

      const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            // In that case, you can refine
          }
        });
      });

      observer.observe(sentinel);

      return;
    }

    // ...
  }
);

Retrieve more results

Now that you can track when you reach the end of the results, use the showMore function inside the callback function of the observer. Only trigger the function when there are still results to retrieve. For this use case, the connector provides a parameter isLastPage that indicates if you still have results.

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
let lastRenderArgs;

const infiniteHits = instantsearch.connectors.connectInfiniteHits(
  (renderArgs, isFirstRender) => {
    const { hits, showMore, widgetParams } = renderArgs;
    const { container } = widgetParams;

    lastRenderArgs = renderArgs;

    if (isFirstRender) {
      const sentinel = document.createElement('div');
      container.appendChild(document.createElement('ul'));
      container.appendChild(sentinel);

      const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting && !lastRenderArgs.isLastPage) {
            showMore();
          }
        });
      });

      observer.observe(sentinel);

      return;
    }

    // ...
  }
);

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.

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

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.

Did you find this page helpful?