How to build a react scroll loader component using Intersection Observer API.

This article will help you to build a re-usable infinite loader component.

Prerequisites

This is what we will be implementing. You can find all code in Github Repo

What is infinite loader?

Infinite loader is a mechanism to show data on page based on user scroll activity. It's pagination mechanism but instead of clicking on page number to navigate, user will keep scrolling to load more data on the page.

We will be using JavaScript Intersection Observer API to check if user has scrolled to the end of the page, and based on threshold we will perform data fetching functions.

For dummy data we will be using jsonplaceholder dummy api, it will return 10 records per page.

https://jsonplaceholder.typicode.com/photos?_limit=10&_page=1

Let's build a simple component which load data using dummy api and then will implement infinite loader to perform load more operation. We will be using axios for data fetching, so first install that if you haven't -

npm install axios

Now let's create a PhotoLoader component which will load photos from dummy api. This component has getPhotos method which will be excuted when component mounts. This method will make an api call using axios and save loaded data in photos state. getPhotos will also update the current page that user is on, based on that getPhotos will load latest data from server.

import React, { Component } from "react"; import axios from "axios"; import ScrollLoader from "./ScrollLoader"; class PhotoLoader extends Component { constructor(props) { super(props); this.state = { isLoading: false, page: 1, photos: [], }; } componentDidMount() { this.getPhotos(); } getPhotos = () => { const page = this.state.page; this.setState({ isLoading: true }); const url = `https://jsonplaceholder.typicode.com/photos?_limit=10&_page=${page}`; axios(url) .then((res) => { this.setState({ isLoading: false, photos: [...this.state.photos, ...res.data], page: page + 1, }); }) .catch((err) => { this.setState({ isLoading: false }); }); }; render() { const { isLoading, photos } = this.state; return ( <div style={{ paddingBottom: "20px" }}> {photos.map((photo) => ( <div key={photo.id} style={{ marginBottom: "20px" }}> <h3>{photo.title}</h3> <img loading="lazy" style={{ maxWidth: "100%" }} src={photo.url} alt="test" /> </div> ))} {isLoading ? <h4>Loading...</h4> : null} <ScrollLoader isLoading={isLoading} onLoad={this.getphotos} hasMoreData={true} /> </div> ); } } export default PhotoLoader;

In PhotoLoader we have imported ScrollLoader component, so let's build that

ScrollLoader Component should be the last component in your render function.

import React, { Component } from "react"; class ScrollLoader extends Component { constructor(props) { super(props); this.loaderEl = React.createRef(); this.prevRatio = 0; } componentDidMount() { this.createObserver(); } createObserver = () => { const options = { root: this.props.containerEl || null, // null means document viewport rootMargin: "0px", threshold: 1, // target element 100% visible }; const observer = new IntersectionObserver(this.handleIntersect, options); observer.observe(this.loaderEl.current); }; handleIntersect = (entries) => { // if no more data is available or api call is in progress then // no need to perform any operation if (this.props.isLoading || !this.props.hasMoreData) { return; } entries.forEach((entry) => { if (entry.intersectionRatio > this.prevRatio) { // loaderEl is in view, perform load more data this.props.onLoad(); } this.prevRatio = entry.intersectionRatio; }); }; render() { return <div ref={this.loaderEl}></div>; } } export default ScrollLoader;

In constructor we have Initialised two variables -

  • this.loaderEl this is target element, as soon as target element intersect viewport we will perform load more data operation.
  • this.prevRatio this will be used to record the latest visibility ratio of the element, and used to check if target element is becoming more visibile or less visible since last intersection.

createObserver method setup an observer for target element and setup an handleIntersect method which will executed on page scroll. It will check -

  • If load more operation is in progress, if yes then return from the method
  • If there is more data to be fetched, in our example it's always true but in real world application this will depend on the data.
  • If target element is in view port.

Based on these 3 condition handleIntersect will execute this.props.onLoad method which in turns makes an api call to get more data.

Conclusion

ScrollLoader requires minimal props and isolate the intersection logic from your main component, this makes it easy to re-use throughout the application. Feel free to make any modification to make it better and if you have any query let me know.

Say Hi on twitter and email

Designed and developed by Gulam Hussain.

Built with Gatsby. Hosted on Netlify.