Infinite Scroll in React Using Intersection Observer

By Chris Nwamba

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 (eg. 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');

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 starts 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) {
  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() {

The method is calling getUsers which we will create next:

getUsers(page) {
  this.setState({ loading: true });
    .then(res => {
      this.setState({ users: [...this.state.users,] });
      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' }}>
          { => <li key={}>{user.login}</li>)}
        ref={loadingRef => (this.loadingRef = loadingRef)}
        <span style={loadingTextCSS}>Loading...</span>

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

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() {

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

Just as we saw earlier, 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 =;
    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 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:

Final Words

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: