Tutorial

How To Implement Infinite Scroll in React

React

Introduction

Infinite scrolling is when a user reaches the bottom of a page and new content is fetched and loaded so the user can continue to scroll in a relatively seamless experience. This is an alternative to other pagination solutions which use numbered pages or buttons that load more content.

You may have encountered infinite scrolling in applications like Instagram. You are presented with a feed of images and as you scroll down, more images keep showing up. Over and over and over until they run out of content to give you.

In this tutorial, you will touch upon the two key concepts that allow infinite scrolling to work - detecting when the user has reached the bottom of the page and loading the next batch of content to display. You will use these concepts to construct a display of astronomy photos and videos.

Prerequisites

To complete this tutorial, you’ll need:

This tutorial was verified with Node v14.12.0, npm v6.14.8, react v16.13.1, superagent v6.1.0, and lodash.debounce v2.7.1.

Step 1 — Setting Up the Project

Start with using create-react-app to generate a React App and then install dependecies:

  • npx create-react-app react-infinite-scroll-example

Change into the new project directory:

  • cd react-infinite-scroll-example

For loading data from the APOD API, you will be using superagent.

For debouncing the events, you will be using lodash.

To add superagent and lodash.debounce to your project via npm run:

  • npm install superagent@6.1.0 lodash.debounce@4.0.8

Now, you can run the React application:

  • npm start

Fix any errors or issues with your project. And visit localhost:3000 in a web browser.

Once you have a working React application, you can start building your infinite scroll functionality.

Step 2 — Implementing onscroll and loadApods

Infinite scrolling will require two key parts. One part will be a check for the window scroll position and the height of the window to determine if a user has reached the bottom of the page. Another part will be handling the request for additional information to display.

Let’s start by creating an InfiniteSpace.js file:

  • nano src/InfiniteSpace.js

Construct your InfiniteSpace component:

src/InfiniteSpace.js
import React from 'react';
import request from 'superagent';
import debounce from 'lodash.debounce';

class InfiniteSpace extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      apods: [],
    };
  }

  render() {
    return (
      <div>
        <h1>Infinite Space!</h1>
        <p>Scroll down to load more!!</p>
      </div>
    )
  }
}

export default InfiniteSpace;

The crux of the infinite scroll component is going to be an onscroll event that will check to see if the user has scrolled to the bottom of the page. Upon reaching the bottom of the page, the event will attempt to load additional content.

When binding events, especially to scroll events, it is good practice to debounce the events. Debouncing is when you only run a function once a specified amount of time has passed since it was last called.

Debouncing improves performance for your user by limiting how often an event is fired and also helps take some strain off of any services you may be calling from the event handler.

src/InfiniteSpace.js
class InfiniteSpace extends Component {
  constructor(props) {
    super(props);

    this.state = {
      apods: [],
    };

    window.onscroll = debounce(() => {
      const {
        loadApods
      } = this;

      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
        loadApods();
      }
    }, 100);
  }

  // ...
}

This code establishes a debounce iteration of 100 milliseconds.

The loadApods function will use superagent’s request to GET the Astronomy Picture of the Day:

class InfiniteSpace extends Component {
  constructor(props) {
    // ...
  }

  dayOffset = () => {
    let today = new Date();
    let day = today.setDate(-1 * this.state.apods.length);
    return new Date(day).toISOString().split('T')[0];
  }

  <^>loadApods = () => {
    request
      .get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
      .then((results) => {
        const nextApod = {
          date: results.body.date,
          title: results.body.title,
          explanation: results.body.explanation,
          copyright: results.body.copyright,
          media_type: results.body.media_type,
          url: results.body.url
        };

        this.setState({
          apods: [
            ...this.state.apods,
            nextApod
          ]
        });
      });
  }

  render() {
    // ...
  }
}

A dayOffset function will be used to calculate the previous Astronomy Picture of the Day.

This code will map the response from APOD to store values for date, title, explanation, copyright, media_type, and url.

The data that has been loaded will be appended to an array in the component’s state and will be iterated through in the component’s render method.

To verify that your two pieces work together, let’s render the response:

class InfiniteSpace extends Component {
  // ...

  render() {
    return(
      <div>
        <h1>Infinite Space!</h1>
        <p>Scroll down to load more!!</p>

        {apods.map(apod => (
          <React.Fragment key={apod.date}>
            <hr />
            <div>
              <h2>{apod.title}</h2>
              {apod.media_type === 'image' &&
                <img
                  alt={`NASA APOD for {apod.date}`}
                  src={apod.url}
                  style={{
                    maxWidth: '100%',
                    height: 'auto'
                  }}
                />
              }
              {apod.media_type === 'video' &&
                <iframe
                  src={apod.url}
                  width='640'
                  height='360'
                  style={{
                    maxWidth: '100%'
                  }}
                ></iframe>
              }
              <div>{apod.explanation}</div>
              <div>{apod.copyright}</div>
            </div>
          </React.Fragment>
        ))}

        <hr />
      </div>
    );
  }
}

This code will display either an img or an iframe depending on the media_type for the APOD.

At this point, you can modify your App component to import InfiniteSpace. Open App.js:

  • nano src/App.js

And replace the content generated by Create React App with the InfiniteSpace component:

src/App.js
import React from 'react';
import InfiniteSpace from './InfiniteSpace';

function App() {
  return (
    <div className="App">
      <InfiniteSpace />
    </div>
  );
}

export default App;

At this point, you can run your application again:

  • npm start

Fix any errors or issues with your project. And visit localhost:3000 in a web browser.

If you scroll down the height of the webpage, you will trigger the conditions for the onscroll event to fire loadApods and a new APOD should appear on the screen.

With these two pieces for infinite scrolling in place, you have established the bulk of the InfiniteSpace component. Adding an initial load and error handling will help make it more robust.

Step 3 — Adding an Initial Load and Error Handling

Presently, InfiniteSpace does not load any APODs until the conditions for the onscroll event are met. There are also three situations where you will not want to load APODs: if there are no more APODs to load, if you are currently loading an APOD, and if you encounter an error. Let’s address these issues.

First, revisit InfiniteSpace.js:

  • nano src/InfiniteSpace.js

Then, use componentDidMount() for an initial load:

src/InfiniteSpace.js
class InfiniteSpace extends Component {
  constructor(props) {
    // ...
  }

  componentDidMount() {
    this.loadApods();
  }

  dayOffset = () => {
    // ...
  }

  loadApods = () => {
    // ...
  }

  render() {
    // ...
  }
}

Add error, hasMore, and isLoading to the state to address errors and restrict unecessary loading:

src/InfiniteSpace.js
class InfiniteSpace extends Component {
  constructor(props) {
    super(props);

    this.state = {
      error: false,
      hasMore: true,
      isLoading: false,
      apods: []
    };

    // ...
  }

  // ...
}

error is initially set to false. hasMore is initially set to true. And isLoading is initially set to false.

Then, apply state to onscroll:

src/InfiniteSpace.js
class InfiniteSpace extends Component {
  constructor(props) {
    super(props);

    this.state = {
      error: false,
      hasMore: true,
      isLoading: false,
      apods: []
    };

    window.onscroll = debounce(() => {
      const {
        loadApods,
        state: {
          error,
          isLoading,
          hasMore
        }
      } = this;

      if (error || isLoading || !hasMore) return;

      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
        loadApods();
      }
    }, 100);
  }

  // ...
}

This check will bail early and prevent loadApods from being called in situations where there is an error, it is currently loading, or there are no additional APODs to load.

Then, apply state to loadApods:

src/InfiniteSpace.js
class InfiniteSpace extends Component {
  // ...

  loadApods = () => { this.setState({ isLoading: true }, () => {
    request
      .get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
      .then((results) => {
        const nextApod = {
          date: results.body.date,
          title: results.body.title,
          explanation: results.body.explanation,
          copyright: results.body.copyright,
          media_type: results.body.media_type,
          url: results.body.url
        };

        this.setState({
          hasMore: (this.state.apods.length < 5),
          isLoading: false,
          apods: [
            ...this.state.apods,
            nextApod
          ],
        });
      })
      .catch((err) => {
        this.setState({
          error: err.message,
          isLoading: false
          });
      });
    });
  }

  // ...
}

This code uses setState with a callback function passed-in as the second argument. The initial call to setState in the loadApods method sets the value of isLoading to true and then in the callback function the next APOD is loaded and setState is called again to set isLoading to false.

For the purposes of our tutorial, hasMore is a boolean check to limit the amount of APODs to 5. In different scenarios, an API may return some value as part of the payload that indicates whether there is any more content to load.

If loadApods encounters an error, error is set to err.message in the catch block.

Then, apply state to render:

src/InfiniteSpace.js
class InfiniteSpace extends Component {
  // ...

  render() {
    const {
      error,
      hasMore,
      isLoading,
      apods
    } = this.state;

    return (
      <div>
        {/* ... React.Fragment ... */}

        {error &&
          <div style={{ color: '#900' }}>
            {error}
          </div>
        }

        {isLoading &&
          <div>Loading...</div>
        }

        {!hasMore &&
          <div>Loading Complete</div>
        }
      </div>
    );
  }
]

This will now display messages for error, isLoading, and hasMore.

When all the pieces are put together, InfiniteSpace will look like this:

src/InfiniteSpace.js
import React from 'react';
import request from 'superagent';
import debounce from 'lodash.debounce';

class InfiniteSpace extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      error: false,
      hasMore: true,
      isLoading: false,
      apods: []
    };

    window.onscroll = debounce(() => {
      const {
        loadApods,
        state: {
          error,
          isLoading,
          hasMore,
        },
      } = this;

      if (error || isLoading || !hasMore) return;

      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
        loadApods();
      }
    }, 100);
  }

  componentDidMount() {
    this.loadApods();
  }

  dayOffset = () => {
    let today = new Date();
    let day = today.setDate(-1 * this.state.apods.length);
    return new Date(day).toISOString().split('T')[0];
  }

  loadApods = () => {this.setState({ isLoading: true }, () => {
    request
      .get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
      .then((results) => {
        const nextApod = {
          date: results.body.date,
          title: results.body.title,
          explanation: results.body.explanation,
          copyright: results.body.copyright,
          media_type: results.body.media_type,
          url: results.body.url
        };

        this.setState({
          hasMore: (this.state.apods.length < 5),
          isLoading: false,
          apods: [
            ...this.state.apods,
            nextApod
          ],
        });
      })
      .catch((err) => {
        this.setState({
          error: err.message,
          isLoading: false
        });
      });
    });
  }

  render() {
    const {
      error,
      hasMore,
      isLoading,
      apods
    } = this.state;

    return (
      <div style={{
        padding: 10
      }}>
        <h1>Infinite Space!</h1>
        <p>Scroll down to load more!!</p>

        {apods.map(apod => (
          <React.Fragment key={apod.date}>
            <hr />
            <div>
              <h2>{apod.title}</h2>
              {apod.media_type === 'image' &&
                <img
                  alt={`NASA APOD for {apod.date}`}
                  src={apod.url}
                  style={{
                    maxWidth: '100%',
                    height: 'auto'
                  }}
                />
              }
              {apod.media_type === 'video' &&
                <iframe
                  src={apod.url}
                  width='640'
                  height='360'
                  style={{
                    maxWidth: '100%'
                  }}
                ></iframe>
              }
              <div>{apod.explanation}</div>
              <div>{apod.copyright}</div>
            </div>
          </React.Fragment>
        ))}

        <hr />

        {error &&
          <div style={{ color: '#900' }}>
            {error}
          </div>
        }

        {isLoading &&
          <div>Loading...</div>
        }

        {!hasMore &&
          <div>Loading Complete</div>
        }
      </div>
    );
  }
}

export default InfiniteSpace;

Finally, run your application again:

  • npm start

Fix any errors or issues with your project. And visit localhost:3000 in a web browser.

Scroll down and your application will fetch and display 5 APODs. All the pieces for infinite scrolling have come together.

Conclusion

In this tutorial, you implemented infinite scrolling in a React application. Infinite scrolling is one modern solution to potentially address presenting a lot of information to the end-user without large initial loading times.

If your project has content in the footer of the page that you want the user to reach, infinite scrolling may result in a worse user experience.

There are also other libraries that provide this functionality that may be best suited for the needs of your project.

If you’d like to learn more about React, take a look at our How To Code in React.js series, or check out our React topic page for exercises and programming projects.

Creative Commons License