Building Infinite scroll in React with hooks and Intersection Observer
Tags: javascript, react, hooks, intersectionobserver
Published: 2019-10-12 19:10:54

---
title: Building Infinite scroll in React with hooks and Intersection Observer
published: true
description: A walkthrough on how IntersectionObserver() API works and how it can be integrated with React hooks
tags: JavaScript, React, hooks, IntersectionObserver
---

Knowing what is visible and not visible on a webpage can be very useful information. You can lazy-load images when they come into view, stop videos when they go out of view, even get proper analytics regarding how many content users read on your blog. However, this is usually a difficult thing to implement. Historically, there was no dedicated API for this and one had to find some other means (e.g. [`Element.getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)) for workarounds which can negatively affect the performance of our applications. 


### Introducing: Intersection Observer API 

A better performant way to achieve our goal. The Intersection Observer API is a browser API that can be used to track the position of HTML elements in context to the actual viewport of the browser. The official documentation says: *"The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport." — [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)*

I wanted to explore how I could implement infinite scroll with react using Intersection Observer. I thought I should summarize what I have learned to hopefully help you avoid the same mistakes I ran into. 

It is important that you are comfortable using [React's ref API](https://www.robinwieruch.de/react-ref-attribute-dom-node/) because it is applied to enable the connection between DOM nodes and the intersection observer in React. Otherwise React is a declarative view layer library where it is not planned to access DOM nodes.


### How Does The Intersection Observer API Work?
 
In order to get a complete understanding of the Intersection Observer API, I would recommend that you check out the [documentation found at MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). 

Intersection Observers work in two parts: an observer instance attached to either a specific node or to the overall viewport and a request to this observer to monitor specific children within its descendants. When the observer is created, it is also provided with a callback that receives one or more intersection entries.

Simply put, you need to create an Observer that will ‘*observe*’ a DOM node and execute a callback when one or more of its threshold options are met. A threshold can be any ratio from 0 to 1 where 1 means the element is 100% in the viewport and 0 is 100% out of the viewport. By default, the threshold is set to 0.

```javascript
// Example from MDN

let options = {
  root: document.querySelector('#scrollArea') || null, // page as root
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

/* 
   options let you control the circumstances under which
   the observer's callback is invoked
*/
```

Once you have created your observer, you have to give it a target element to watch: 

```javascript
let target = document.querySelector('#listItem');
observer.observe(target);
```
Whenever the target meets a threshold specified for the `IntersectionObserver`, the callback is invoked. The callback receives a list of `IntersectionObserverEntry` objects and the observer:

```javascript
let callback = (entries, observer) => { 
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });


 console.log(entries, observer)
};
```

### The Threshold

The threshold refers to how much of an intersection has been observed in relation to the root of the `IntersectionObserver` 

Let's consider this image below:
![The threshold for page A is 25%, B is 50% while C is 75%](https://thepracticaldev.s3.amazonaws.com/i/xthilpeig56tawxwl381.png "The threshold for page A is 25%, B is 50% while C is 75%")

The first thing to do is to declare the page/scroll area as our `root`. We can then consider the image container as our target. Scrolling the target into the root gives us different thresholds. The threshold can either be a single item, like 0.2, or an array of thresholds, like [0.1, 0.2, 0.3, ...]. It is important to note that the root property must be an ancestor to the element being observed and is the browser viewport by default.

```javascript
let options = {
  root: document.querySelector('#scrollArea'), 
  rootMargin: '0px',
  threshold: [0.98, 0.99, 1]
}

let observer = new IntersectionObserver(callback, options);
```

We have the observer, but it’s not yet observing anything. To start it observing, you need to pass a dom node to the observe method. It can observe any number of nodes, but you can only pass in one at a time. When you no longer want it to observe a node, you call the unobserve() method and pass it the node that you would like it to stop watching or you can call the disconnect() method to stop it from observing any node, like this:

```javascript
let target = document.querySelector('#listItem');
observer.observe(target);

observer.unobserve(target);
//observing only target

observer.disconnect(); 
//not observing any node
```

### React

We are going to be implementing Intersection observer by creating an infinite scroll for a list of images. We will be making use of the super easy . It's a great choice because it is paginated. 

NB: You should know how to fetch data using hooks, if you are not familiar, you can check out [this article](https://www.robinwieruch.de/react-hooks-fetch-data
). Good stuff there!

```javascript
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

export default function App() {
  const [loading, setLoading] = useState(false);
  const [images, setImages] = useState([]);
  const [page, setPage] = useState(1);


  const fetchData = useCallback(async pageNumber => {
    const url = `https://picsum.photos/v2/list?page=${page}&limit=15`;
    setLoading(true);

    try {
      const res = await axios.get(url);
      const { status, data } = res;

      setLoading(false);
      return { status, data };
    } catch (e) {
      setLoading(false);
      return e;
    }
  }, []);

  const handleInitial = useCallback(async page => {
      const newImages = await fetchData(page);
      const { status, data } = newImages;
      if (status === 200) setImages(images => [...images, ...data]);
    },
    [fetchData]
  );

  useEffect(() => {
    handleInitial(page);
  }, [handleInitial]);

  return (
      <div className="appStyle">
     
      {images && (
        <ul className="imageGrid">
          {images.map((image, index) => (
            <li key={index} className="imageContainer">
              <img src={image.download_url} alt={image.author} className="imageStyle" />
            </li>
          ))}
        </ul>
      )}

      {loading && <li>Loading ...</li>}

      <div className="buttonContainer">
        <button className="buttonStyle">Load More</button>
      </div>
    </div>
  )
}
```

This is the core of the app. We want to be able to load up the page and have it make API call to the [Lorem Picsum API](https://picsum.photos) and then display some images.
{% codesandbox images-list-jf1i5 %}

This is a good first step as we have been able to handle data fetching. The next thing to do is thinking of how we can write code to make more requests and update the image lists we have stored in our state. To do this, we have to create a function that will take in the current page and then increase it by `1`. This should then trigger the `useEffect()` to make a call for us and update the UI.

```javascript
// const [page, setPage] = useState(1);
const loadMore = () => {
    setPage(page => page + 1);
    handleInitial(page);
};
```

Great, we have written our updater function. We can attach this to a button on the screen and have it make the calls for us!

```javascript
<div className="buttonContainer">
   <button className="buttonStyle" onClick={loadMore}>Load More</button>
</div>
```

Open up your network tab to be sure that this is working. If you checked properly, you would see that when we click on `Load More`, it actually works. The only problem is, it is reading the updated value of page as `1`. This is interesting, you might be wondering why that is so. The simple answer is, we are still in a function scope when the update is made and we do not have access to the updated state until the function finishes executing. This is unlike `setState()` where you had a callback available.

Ok, so how do we solve this. We will be making use of react `useRef()` hook. `useRef()` returns an object that has a current attribute pointing to the item you are referencing.

```javascript
import React, { useRef } from "react";

const Game = () => {
  const gameRef = useRef(1);
};

const increaseGame = () => {
  gameRef.current; // this is how to access the current item
  gameRef.current++;

  console.log(gameRef); // 2, update made while in the function scope.
} 
```

This approach will help us properly handle the data fetching in our application.

```javascript
// Instead of const [page, setPage] = useState(1);
const page = useRef(1);

const loadMore = () => {
  page.current++;
  handleInitial(page);
};

useEffect(() => {
   handleInitial(page);
}, [handleInitial]);
```

Now, if you hit the `Load More` button, it should behave as expected. Yay! 🎉. We can consider the first part of this article done. Now to the main business, how can we take what we have learned about `Intersection Observer` and apply it to this app?

The first thing to consider is the approach. Using the illustration explaining the threshold above, we will like to load images once the Load More button comes into view. We can have the threshold set at `1` or `0.75`. We have to set up `Intersection Observer` in React.

```javascript
// create a variable called observer and initialize the IntersectionObserver()
const observer = useRef(new IntersectionObserver());

/*

A couple of things you can pass to IntersectionObserver() ... 
the first is a callback function, that will be called every time
the elements you are observing is shown on the screen, 
the next are some options for the observer

*/

const observer = useRef(new IntersectionObserver(entries => {}, options)
```

By doing this we have initialized the `IntersectionObserver()`. However, initializing is not enough. React needs to know to observe or unobserve. To do this, we will be making use of the `useEffect()` hook. Lets also set the threshold to `1`.

```javascript
// Threshold set to 1
const observer = useRef(new IntersectionObserver(entries => {}, { threshold: 1 })

useEffect(() => {
  const currentObserver = observer.current;
	// This creates a copy of the observer 
  currentObserver.observe(); 
}, []);
```

We need to pass an element for the observer to observe. In our case, we want to observe the Load More button. The best approach to this creates a ref and pass it to the observer function. 

```javascript
// we need to set an element for the observer to observer
const [element, setElement] = useState(null);

<div ref={setElement} className="buttonContainer">
  <button className="buttonStyle">Load More</button>
</div>

/*
 
on page load, this will trigger and set the element in state to itself, 
the idea is you want to run code on change to this element, so you 
will need this to make us of `useEffect()`

*/
```

So we can now update our observer function to include the element we want to observe

```javascript
useEffect(() => {
  const currentElement = element; // create a copy of the element from state
  const currentObserver = observer.current;

  if (currentElement) {
    // check if element exists to avoid errors
    currentObserver.observe(currentElement);
  }
}, [element]);
```

The last thing is to set up a cleanup function in our `useEffect()` that will `unobserve()` as the components unmount. 

```javascript
useEffect(() => {
  const currentElement = element; 
  const currentObserver = observer.current; 

  if (currentElement) {
    currentObserver.observe(currentElement); 
  }

  return () => {
    if (currentElement) {
      // check if element exists and stop watching
      currentObserver.unobserve(currentElement);
    }
  };
}, [element]);
```

If we take a look at the webpage, it still doesn't seem like anything has changed. Well, that is because we need to do something with the initialized `IntersectionObserver()`.

```javascript
const observer = useRef(
  new IntersectionObserver(
    entries => {},
    { threshold: 1 }
  )
);

/*

entries is an array of items you can watch using the `IntersectionObserver()`,
since we only have one item we are watching, we can use bracket notation to
get the first element in the entries array

*/

const observer = useRef(
  new IntersectionObserver(
    entries => {
      const firstEntry = entries[0];
      console.log(firstEntry); // check out the info from the console.log()
    },
    { threshold: 1 }
  )
);
```

From the `console.log()`, we can see the object available to each item we are watching. You should pay attention to the isIntersecting, if you scroll the Load More button into view, it changes to true and updates to false when not in-view. 

```javascript
const observer = useRef(
  new IntersectionObserver(
    entries => {
      const firstEntry = entries[0];
      console.log(firstEntry);

      if (firstEntry.isIntersecting) {
        loadMore(); // loadMore if item is in-view
      }
    },
    { threshold: 1 }
  )
);
```

This works for us, you should check the webpage and as you scroll approaching the `Load More` button, it triggers the `loadMore()`. This has a bug in it though, if you scroll up and down, `isIntersecting` will be set to `false` then `true`. You don't want to load more images when you anytime you scroll up and then down again. 

To make this work properly, we will be making use of the `boundingClientRect` object available to the item we are watching.

```javascript
const observer = useRef(
    new IntersectionObserver(
      entries => {
        const firstEntry = entries[0];
        const y = firstEntry.boundingClientRect.y;
        console.log(y); 
      },
      { threshold: 1 }
    )
  );
```

We are interested in the position of the `Load More` button on the page. We want a way to check if the position has changed and if the current position is greater than the previous position.

```javascript
const initialY = useRef(0); // default position holder

const observer = useRef(
  new IntersectionObserver(
    entries => {
      const firstEntry = entries[0];
      const y = firstEntry.boundingClientRect.y;

			console.log(prevY.current, y); // check
      
      if (initialY.current > y) {
				console.log("changed") // loadMore()
      }

      initialY.current = y; // updated the current position
    },
    { threshold: 1 }
  )
);
```

With this update, when you scroll, it should load more images and its fine if you scroll up and down within content already available.

### Full Code

```javascript
import React, { useState, useEffect, useCallback, useRef } from 'react';
import axios from 'axios';

export default function App() {
  const [element, setElement] = useState(null);
  const [loading, setLoading] = useState(false);
  const [images, setImages] = useState([]);

  const page = useRef(1);
  const prevY = useRef(0);
  const observer = useRef(
    new IntersectionObserver(
      entries => {
        const firstEntry = entries[0];
        const y = firstEntry.boundingClientRect.y;

        if (prevY.current > y) {
          setTimeout(() => loadMore(), 1000); // 1 sec delay
        }

        prevY.current = y;
      },
      { threshold: 1 }
    )
  );

  const fetchData = useCallback(async pageNumber => {
    const url = `https://picsum.photos/v2/list?page=${pageNumber}&limit=15`;
    setLoading(true);

    try {
      const res = await axios.get(url);
      const { status, data } = res;

      setLoading(false);
      return { status, data };
    } catch (e) {
      setLoading(false);
      return e;
    }
  }, []);

  const handleInitial = useCallback(
    async page => {
      const newImages = await fetchData(page);
      const { status, data } = newImages;
      if (status === 200) setImages(images => [...images, ...data]);
    },
    [fetchData]
  );

  const loadMore = () => {
    page.current++;
    handleInitial(page.current);
  };

  useEffect(() => {
    handleInitial(page.current);
  }, [handleInitial]);

  useEffect(() => {
    const currentElement = element;
    const currentObserver = observer.current;

    if (currentElement) {
      currentObserver.observe(currentElement);
    }

    return () => {
      if (currentElement) {
        currentObserver.unobserve(currentElement);
      }
    };
  }, [element]);

  return (
    <div className="appStyle">
      {images && (
        <ul className="imageGrid">
          {images.map((image, index) => (
            <li key={index} className="imageContainer">
              <img src={image.download_url} alt={image.author} className="imageStyle" />
            </li>
          ))}
        </ul>
      )}

      {loading && <li>Loading ...</li>}

      <div ref={setElement} className="buttonContainer">
        <button className="buttonStyle">Load More</button>
      </div>
    </div>
  );
}
```

{% codesandbox complete-5hbof %}


It’s important to note that to some extent, IO is safe to use and supported across most browsers. However, you can always use a [Polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill) if you are not comfortable. You can refer to [this](https://caniuse.com/#feat=intersectionobserver) to learn more about support:


![Browser support](https://thepracticaldev.s3.amazonaws.com/i/xvhfmrca0vyv4nyviecx.png "Browser support")


#### Adios 👋🏾