Tutorial

Infinite Scroll in React Using Intersection Observer

Draft updated on Invalid Date
Default avatar

By Chris Nwamba

Infinite Scroll in React Using Intersection Observer

This tutorial is out of date and no longer maintained.

Introduction

The most dreaded features when building frontend apps are features that need you to take control of the scroll events, behavior, and properties. Not only are they hard to implement because of numbers crunching, but more often they are prone to affect performance badly. A lot of us head to libraries for help but they still can’t help you with performance. This is because they use the main thread to manage the scroll events which are called frequently.

A more efficient and simpler alternative is the in-built Intersection Observer which is fairly new but can be polyfilled.

Intersection and Observation

The Intersection Observer does not require an event. Rather, it waits for a rectangle you want to observe to get into view before running any code. You can call this rectangle the target and the view to be entered, the root.

From the image above, root can be the entire page or a portion (e.g., div) in the page. By default, observation starts once the target enters the root.

Here is a code example that shows how to achieve the intersection in the above diagram:

const options = {
  root: document.querySelector('#divRoot'), /* or `null` for page as root */
}

const observer = new IntersectionObserver(callback, options);

After setting up the observer, you can start observing the target:

const target = document.querySelector('#divTarget');
observer.observe(target);

For each time an intersection happens, the callback function in the IntersectionObserver constructor is called:

const callback = (entities, options) => {
  console.log(entities, options)
}

The Threshold

The threshold refers to how much of an intersection has been observed. This illustration should help you understand better:

The threshold for page A is 25%. B is 50% while C is 75%. These are not just figures, you can tell your browser to start observing at any threshold. By default, the observations start at 0.0 but you can ignore the first half of the target and start observing at the second half (0.5).

To set the threshold, all that needs to be done is to add the threshold to the options object:

const options = {
  root: document.querySelector('#divRoot'), /* or `null` for page as root */
  threshold: 1.0 // Only observe when the entire box is in view
}

Now let’s see how to use this in a real example.

React State

In your React’s App component, add the following constructor:

constructor(props) {
  super(props);
  this.state = {
    users: [],
    page: 0,
    loading: false,
    prevY: 0
  };
}

The state object contains the following:

  • users: Stores a list of users on GitHub
  • page: The start page for the list of users from GitHub
  • loading: When true shows a div with a loading text. This will be helpful when fetching data
  • prevY: This is where the last intersection y position will be stored for reference

Fetching Users from GitHub

Add axios as dependency via npm then add a componentDidMount in the App class:

componentDidMount() {
  this.getUsers(this.state.page);
}

The method is calling getUsers which we will create next:

getUsers(page) {
  this.setState({ loading: true });
  axios
    .get(`https://api.github.com/users?since=${page}&per_page=100`)
    .then(res => {
      this.setState({ users: [...this.state.users, ...res.data] });
      this.setState({ loading: false });
    });
}

This method takes a page parameter and uses the parameter to query the GitHub API for users. It then updates the state with the users. Notice how the method is flipping the loading state to show a loading text.

Go ahead to render the list in the browser:

render() {
  const loadingCSS = {
    height: '100px',
    margin: '30px'
  };
  const loadingTextCSS = { display: this.state.loading ? 'block' : 'none' };
  return (
    <div className="container">
      <div style={{ minHeight: '800px' }}>
        <ul>
          {this.state.users.map(user => <li key={user.id}>{user.login}</li>)}
        </ul>
      </div>
      <div
        ref={loadingRef => (this.loadingRef = loadingRef)}
        style={loadingCSS}
      >
        <span style={loadingTextCSS}>Loading...</span>
      </div>
    </div>
  );
}

Notice loadingRef div which shows a loading text. We’ll use it later as a target for our Intersection Observer.

View example on CodeSandbox

Implementing Infinite Scroll with IO

We want the observer to start after a component is mounted, therefore we should set it up in componentDidMount:

componentDidMount() {
  this.getUsers(this.state.page);

  // Options
  var options = {
    root: null, // Page as root
    rootMargin: '0px',
    threshold: 1.0
  };
  // Create an observer
  this.observer = new IntersectionObserver(
    this.handleObserver.bind(this), //callback
    options
  );
  //Observ the `loadingRef`
  this.observer.observe(this.loadingRef);
}

Just as we saw earlier, this.observer is the instance of IntersectionObserver. We are also using the instance to observe the loadingRef we created in the render method above. This makes loadingRef the target.

The callback is named handleObserver. Let’s create it like so:

handleObserver(entities, observer) {
  const y = entities[0].boundingClientRect.y;
  if (this.state.prevY > y) {
    const lastUser = this.state.users[this.state.users.length - 1];
    const curPage = lastUser.id;
    this.getUsers(curPage);
    this.setState({ page: curPage });
  }
  this.setState({ prevY: y });
}

The method completes each of the following:

  1. The if statement makes sure the method body is only called when scrolling down and not when scrolling up.
  2. Paging in GitHub uses the IDs instead of the page number. In that case, we need the last ID of the current list to make decisions for the next. The lastUser and curePage variables help to maintain this.
  3. When the curPage has been updated, we can then call getUsers with its value and update the state with setState.

Have another look at the example:

View example on CodeSandbox

Conclusion

It’s important to mention that as much as this is exciting to use, it’s not ready and it’s yet to be supported well enough across browsers. Keep this in mind and use a polyfill.

You can refer to this to learn more about support.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Chris Nwamba

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel