Adding Infinite Scroll For Both Desktop and Mobile in Your Gatsby Project with React Hooks
Tags: react, gatsby, infinitescroll, reacthooks
Published: 2019-05-20 21:00:27

---
title: Adding Infinite Scroll For Both Desktop and Mobile in Your Gatsby Project with React Hooks
published: true
tags: react, gatsby, infinite-scroll, react-hooks, 
canonical_url: https://www.wesleylhandy.net/blog/infinite-scroll-mobile-desktop-gatsby.html
---

[![List of items waiting to be updated on scroll](https://www.wesleylhandy.net/static/310b5d4fba2ad789399bfaeb5397a733/c35de/infinite-scroll.jpg)](/static/310b5d4fba2ad789399bfaeb5397a733/c35de/infinite-scroll.jpg)

I recently created my second production [Gatsby](https://gatsbyjs.org) application that gives a [simple presentation of a local government open data dataset](https://vb-business-licenses.netlify.com). I say production, though, much of the application is a proof-of-concept for a bigger application I have in the works (perhaps a startup in the mix? Not sure yet...). My app includes over 2400 nodes, so I needed a way to present the data in user-friendly ways. Each record in my collection included a set of categories, so I could easily create a categories page and split the data that way. However, I also want to eventually add search and also allow for a user to browse through the entire dataset. This is where I looked into `pagination` and `infinite-scroll`. You can [read about adding pagination on the Gatsby blog](https://www.gatsbyjs.org/docs/adding-pagination/)—it’s pretty straight-forward. Below is how I set up infinite-scroll that works in both development and production environments, as well as both in the browser and on touch screens. I was able to accomplish this using React `Hooks` within a functional component rather than a class-based component.

## <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Inifinite Scroll & React Hooks

As much as this post is about integrating this within Gatsby, this is properly a `react` question. Gatsby is simply a platform for sourcing data. This could easily be implemented via `fetch`-ing of data from an API during client-side component. Adjust this method to what you need in your case.

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Setting Up `gatsby-node.js`

Within `gatsby-node.js` you will define an export named `createPages` that queries `graphql` for nodes from your data source and returns that list of nodes. Before returning, you can call the `createPage` API as many times a you need to generate your site. I intend, among many other things, to create a single page that generates and infinite scroll through my list of businesses. I will call `createPage`, passing to it the path of the page, the template for the page, and data that will be provided to the client via the `Context` api:

```
exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions
  return graphql(`
    {
      #some query specific to your source data
      specificNameOfYourQuery {
        edges {
          node {
            #specific fields
          }
        }
      }
    }
  `).then(result => {
    if (result.errors) {
      return Promise.reject(result.errors)
    }
    const { data: [specificNameOfYourQuery]: edges } } } = result;
    const infiniteScrollTemplate = path.resolve(`src/templates/infinite-scroll-template.js`)
    createPage({ 
      path: "/businesses", 
      component: infiniteScrollTemplate, 
      context: { 
        edges,
      },
    }) return edges;
  })
```

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Creating the Template that Will Include Infinite Scroll

From the root of your project, go into your `src` directory, create a `templates` folder if it doesn’t already exist, then create your template page. I entitled mine `infinite-scroll-template.js`.

```
cd src
mkdir templates
cd templates
touch infinite-scroll-template.js
```

#### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>React Hooks - `useEffect` and `useState`

Once you have opened your template file within your IDE, you will need to import `React` as well as the `useEffect` and `useState` hooks. For Gatsby projects, you will also import your `Layout` component so that your page will match the rest of your site. The `createPage` API passes to your component `pageContext` as props where you can access the list of `edges` you pass to your template from `gatsby-node`.

```
import React, { useState, useEffect } from 'react'
import Layout from '../components/Layout'

function InfiniteScroll({ pageContext: { edges } }) {
    return null
}

function InfiniteScrollTemplate(props) {
  return (
    <Layout {...props}>
      <InfiniteScroll {...props}/>
    </Layout>
  )
}

export default InfiniteScrollTemplate
```

We will focus on adding the core logic for infinite scroll to the `InfiniteScroll` functional component. We do not need to declare a `React` class because of the two aforementioned hooks—:`useState` and `useEffect`

##### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Creating and Setting Internal State with `useState`

React hooks allow us to write functional components that can “hook into” other React features. `useState` allows us to add state that is preserved between renders of a functional component. Unlike `state` within a React `class`, `useState` replaces the previous state rather than merging with previous state. Calling `useState` takes only one argument, whatever you conceive of as the initial state. The `useState` hook can be called multiple times, so rather than having a single `state` object, you can have multiple `state`-like variables. This is because the call to `useState` returns an array of two properties - the value of the current state and a function to call to update the value of that state.

```
const [currentState, setState] = useState(/* some initial value or function that returns a value */)
```

For infinite scroll to work in this example, we need two state variables—a `boolean` indicating if there are more records to load and an `array` of the records already loaded. Seed the `currentList` with the first 10 records. Don’t worry if there is the possibility that the initial set is less than 10, `Array.slice` will return all records up to the length of the array if you provide an ending value greater than the last index of the array.

_Note: if we had a situation where you loaded data asynchronously from an API, we would also need someway to determine if data was in loading state_

```
const [hasMore, setMore] = useState(edges.length > 10)
const [currentList, addToList] = useState([...edges.slice(0, 10)])
const [isLoading, setLoading] = useState(false) // if loading from an API asynchronously
```

##### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Creating Event Handlers to Read and Set State

Reading and setting state will occur within an event listener on the scroll position of the page. If you use an external api and that api is still loading content, we will return immediately, and we will also exit if we know there are no more edges to load. Otherwise, we will check to see if the scroll position of the `document` plus the `innerHeight` of the window equals the `offsetHeight` of the document, and if so, we can load more edges. Basically, this checks to see if the page is scrolled all the way to the bottom.

```
const handleScroll = () => {
  if ( !hasMore || isLoading ) return;
  if ( window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight ){
    loadEdges(true)
  }
}
```

The `loadEdges` function will do the following, in order:

1. if using an asynchronously api call, will set the loading flag to true
2. determine if any more edges remaining
3. slice a new chunk of edges and append to the current list
4. if using an asynchronously api call, will return the loading flag to false

Since I’m loading from the `Context` API, I will ignore steps 1 & 2 above.

```
const loadEdges = () => {
  const currentLength = currentList.length
  const more = currentLength < edges.length
  const nextEdges = more ? edges.slice(currentLength, currentLength + 20) : []
  setMore(more)
  addToList([...currentList, ...nextEdges])
}
```

_How would `isLoading` be used?_

Here is one overly simplistic example:

```
const loadEdges = async () => {
  setLoading(true);
  try {
      const newEdges = await fetch('https://path/to/some/api')
      const more = newEdges.length > 0
      setMore(more)
      addBusinesses([...currentList, ...nextEdges])
  } catch(err) {
      console.error({fetchNewEdgesError: err})
  }
  setLoading(false)
}
```

##### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Checking The Scroll Position on Each Render with `useEffect`

The final step to initializing infinite scroll is the `useEffect` hook. The hook `useEffect` takes two arguments: a function, and an array. The first argument is the function that will be called every time _after_ the component is rendered. This function is allowed to return another function which will be remembered and gets called as a cleanup function (I’m not sure I fully understand cleanups yet, but I think [Dan Abramov does](https://overreacted.io/a-complete-guide-to-useeffect/)). The second argument is an array of dependencies that would prevent an effect from being called if the values of those dependencies are unchanged between renders. Infinite scroll will a function with a cleanup callback as well as the array full of dependencies to work.

```
useEffect(
  () => {
    /* function that gets called every time */
    return () => {
      /* cleanup function to be called */
    }
  }, [/* dependencies */])
```

`useEffect` is a great place to initialize event listeners on the `window` or `document`, as well as to remove those listeners during cleanup, such as listening for `scroll` events.

```
  window.addEventListener('scroll', handleScroll)
```

And thus, the cleanup function:

```
  window.removeEventListener('scroll', handleScroll)
```

This gives us an almost complete `useEffect` function call, we will simply add our state variables to the dependencies array so that effect is only set or cleaned up when the variables change:

```
useEffect(
  () => {
    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [hasMore, isLoading, currentList])
```

With state, event handlers, effects initialized, we are free to return the `jsx` for the scrolling list. The following maps over the `currentList` array. It also adds labels displaying the current state of the list:

```
return (
  <> {/* shorthand for React.Fragment */}
    <ul>
      {
        currentList.map(({node: { fields }}, idx) => {
          return (
            <li key={`fields-${idx}`} index={idx + 1}>
              { 
                /* you will know the specifics here from how you load your data */
                fields 
              }
            </li>
          )
        })
      }
    </ul>
    {
      !hasMore &&
        <div>All Businesses Loaded!</div>
    }
    {
      hasMore &&
        <div>Scroll Down to Load More...</div>
    }
    {
      /* if using this flag, otherwise omit */
      isLoading && 
        <div>Loading...</div>
    }
  </>
)
```

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Putting It All Together

Now we have a complete picture of the infinite scroll functional component. See below, but don’t leave yet, we still have to account for `gatsby build` and mobile events.

```
import React, { useState, useEffect } from 'react'
import Layout from '../components/Layout'

function InfiniteScroll({ pageContext: { edges } }) {
  const [hasMore, setMore] = useState(edges.length > 10)
  const [currentList, addToList] = useState([...edges.slice(0, 10)])
  
  const loadEdges = () => {
    const currentLength = currentList.length
    const more = currentLength < edges.length
    const nextEdges = more ? edges.slice(currentLength, currentLength + 20) : []
    setMore(more)
    addToList([...currentList, ...nextEdges])
  }

  const handleScroll = () => {
    if ( !hasMore ) return;
    if ( window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight ){
      loadEdges()
    }
  }

  useEffect(() => {
    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [hasMore, currentList])

  return (
    <> {/* shorthand for React.Fragment */}
      <ul>
        {
          currentList.map(({node: { fields }}, idx) => {
            return (
              <li key={`fields-${idx}`} index={idx + 1}>
                { 
                  /* you will know the specifics here from how you load your data */
                  fields 
                }
              </li>
           )
          })
        }
      </ul>
      {
        !hasMore &&
          <div>All Businesses Loaded!</div>
      }
      {
        hasMore &&
          <div>Scroll Down to Load More...</div>
      }
      {
    </>
  )
}

function InfiniteScrollTemplate(props) {
  return (
    <Layout {...props}>
      <InfiniteScroll {...props}/>
    </Layout>
  )
}

export default InfiniteScrollTemplate
```

This functional component will work just fine during development on a desktop browser. But you will lose the scroll effect during the build and on touch screens. It will fail during build because client globals like `window` and `document` are undefined during the build. It will fail on mobile because `scroll` is a mouse event. We need to add some conditions for our event listeners to handle both conditions.

### <svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>Optimizing for Production and Touch Screens

In addition to adding an event listener on `scroll`, we also need event listeners for `touchend` and `resize` (to handle situations where someone resizes their browser), plus we need to add `preventDefault` to touch events to prevent duplicate events being fired in certain situations where devices have both a mouse and a touch screen. First, create a new event handler for handling `touchend`. This new handler will simply prevent the default actions on `touchend` and call the handler for the scroll event (which simply checks to see if we should load more documents). Second, update the `useEffect` function to add the additional event handlers.

```
const handleTouchEnd = (e) => { e.preventDefault(); handleScroll();}
useEffect(() => {
  window.addEventListener('scroll', handleScroll)
  window.addEventListener('resize', handleScroll)
  window.addEventListener('touchend', handleTouchEnd)
  return () => {
    window.removeEventListener('scroll', handleScroll)
    window.removeEventListener('resize', handleScroll)
    window.removeEventListener('touchend', handleTouchEnd)
  }
}, [hasMore, currentList])
```

Finally, we need to add a few simple `boolean` checks (`window &&`) to every instance of `window` or `document` so that the build process succeeds _and_ so that infinite scroll still operates in the client. Plus, we need to change the scroll position check to be `>=` instead of the strict equality `===`.

```
const handleScroll = () => {
  if ( !hasMore || isLoading ) return;
  if ( window && ( ( window.innerHeight + document.documentElement.scrollTop ) >= document.documentElement.offsetHeight ) ){ loadEdges()
  }
}
useEffect(() => {
  window && window.addEventListener('scroll', handleScroll)
  window && window.addEventListener('resize', handleScroll)
  window && window.addEventListener('touchend', handleTouchEnd)
  return () => {
    window && window.removeEventListener('scroll', handleScroll)
    window && window.removeEventListener('resize', handleScroll)
    window && window.removeEventListener('touchend', handleTouchEnd) }
}, [hasMore, currentList])
```

There you have it! You have a component that will implement infinite scroll in the browser, on touch screens, and in production within a Gatsby or React application.

Check out the following page in your browser and on mobile to [see this code in action](https://vb-business-licenses.netlify.com/businesses).

You can also see [my specific implementation of the source code on github](https://github.com/wesleylhandy/got-business-client).