How to Add Infinite Scrolling to Your React App
Tags: react, webdev, javascript, tutorial
Published: 2020-01-12 00:11:23

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

Infinite scrolling is where a page loads new data continuously as you scroll down the page, only stopping when all possible data is loaded. Adding infinite scrolling to your app is easy with React by using one of the many libraries

To add infinite scrolling to an app, we can use a library like the [react-infinite-scroller](https://github.com/CassetteRocks/react-infinite-scroller). It provides infinite scrolling that can easily be incorporated into any React app.

In this story, we will build an app that gets images and videos from the Pixabay API, located at [https://pixabay.com/](https://pixabay.com/). To use the API, we sign up for an API key and then we can call the API 5000 times per hour.

To start building our app, we run `npx create-react-app image-app`. This will create the project folder and the files we need to start building our app.

Next, we have to add some libraries. We need an HTTP client to make requests and Bootstrap to style our app, Formik and Yup for form validation, the `querystring` package to encode query string from objects, react-infinite-scroller for infinite scrolling, and React Router to route URLs to our pages. We run `npm i axios bootstrap formik querystring react-bootstrap react-router-dom yup react-infinite-scroller` to install the libraries.

Now with all the libraries installed, we can start writing code. To begin, we work on our pages. In `App.js` we replace the existing code with:

```jsx
import React from 'react';  
import { Router, Route } from "react-router-dom";  
import HomePage from './HomePage';  
import { createBrowserHistory as createHistory } from 'history'  
import Navbar from 'react-bootstrap/Navbar';  
import Nav from 'react-bootstrap/Nav';  
import './App.css';  
import ImageSearchPage from './ImageSearchPage';  
import VideoSearchPage from './VideoSearchPage';  
const history = createHistory();

function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <Navbar bg="primary" expand="lg" variant="dark" >  
          <Navbar.Brand href="/">Image Gallery App</Navbar.Brand>  
          <Navbar.Toggle aria-controls="basic-navbar-nav" />  
          <Navbar.Collapse id="basic-navbar-nav">  
            <Nav className="mr-auto">  
              <Nav.Link href="/">Home</Nav.Link>  
              <Nav.Link href="/imagesearch">Image Search</Nav.Link>  
              <Nav.Link href="/videosearch">Video Search</Nav.Link>  
            </Nav>  
          </Navbar.Collapse>  
        </Navbar>  
        <Route path="/" exact component={HomePage} />  
        <Route path="/imagesearch" exact component={ImageSearchPage} />  
        <Route path="/videosearch" exact component={VideoSearchPage} />  
      </Router>  
    </div>  
  );  
}

export default App;
```

This adds the navigation bar and routes to our pages that we will use later.

We then create `HomePage.js` to build our home page, which we will put in the `src` folder. In `HomePage.js` we add the following:

```jsx
import React from 'react';  
import { useState, useEffect } from 'react';  
import './HomePage.css';  
import InfiniteScroll from 'react-infinite-scroller';  
import Figure from 'react-bootstrap/Figure'  
import { getPhotos } from './requests';  
let page = 0;

function HomePage() {  
    const [items, setItems] = useState([]);  
    const [initialized, setInitialized] = useState(false);  
    const [totalHits, setTotalHits] = useState(0); 
    const getNewPhotos = async () => {  
        page++;  
        const response = await getPhotos(page);  
        setItems(items.concat(response.data.hits));  
        setTotalHits(response.data.totalHits);  
        setInitialized(true);  
    } 

    useEffect(() => {  
        if (!initialized) {  
            getNewPhotos();  
        }  
    }); 

    return (  
        <div className="HomePage">  
            <InfiniteScroll  
                pageStart={page}  
                loadMore={getNewPhotos}  
                hasMore={totalHits > items.length}  
                threshold={100}  
            >  
                {items.map((i, index) =>  
                    <Figure key={index}>  
                        <Figure.Image  
                            width={window.innerWidth / 3.5}  
                            src={i.previewURL}  
                        />  
                    </Figure>  
                )}  
            </InfiniteScroll>  
        </div>  
    );  
}

export default HomePage;
```

This is where we used the `react-infinite-scroller` library to add infinite scrolling. It is very simple. All we do is wrap the items we want to have infinite scrolling in the `InfiniteScroll` component and then we add the handler for loading the next items. In this case, we have the `getNewPhotos` function to do that. Note that we have to increment the page number before loading the new items and adding to the `items` array. The `hasMore` prop is a boolean, so to keep loading items until we have nothing left, we put `totalHits > items.length` as the expression for `hasMore`. `getNewPhotos` set the `totalHits` and `items` .

In `Homepage.css` in the same folder, and we add:

```css
.figure {  
  margin: 5px !important;  
}

.HomePage {  
  text-align: center;  
}
```

This adds some margins to our images.

All files will be in the `src` folder unless mentioned otherwise. We add the `ImageSearchPage.js` file for the image search page. In there we add:

```jsx
import React from 'react';  
import { Formik } from 'formik';  
import Form from 'react-bootstrap/Form';  
import Col from 'react-bootstrap/Col';  
import Button from 'react-bootstrap/Button';  
import * as yup from 'yup';  
import './ImageSearchPage.css';  
import { searchPhotos } from './requests';  
import { useState, useEffect } from 'react';  
import InfiniteScroll from 'react-infinite-scroller';  
import Figure from 'react-bootstrap/Figure'let page = 1;
const schema = yup.object({  
  query: yup.string().required('Query is required'),  
});

function ImageSearchPage() {  
  const [items, setItems] = useState([]);  
  const [totalHits, setTotalHits] = useState(0);  
  const [keyword, setKeyword] = useState(''); 
  const handleSubmit = async (evt) => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    const data = {  
      q: encodeURIComponent(evt.query),  
      image_type: 'photo',  
      page  
    }  
    const response = await searchPhotos(data);  
    setTotalHits(response.data.totalHits);  
    setItems(items.concat(response.data.hits));  
    setKeyword(evt.query);  
  } 

  const getMorePhotos = async () => {  
    page++;  
    const data = {  
      q: encodeURIComponent(keyword),  
      image_type: 'photo',  
      page  
    }  
    const response = await searchPhotos(data);  
    setTotalHits(response.data.totalHits);  
    setItems(items.concat(response.data.hits));  
  } 

  return (  
    <div className="ImageSearchPage">  
      <Formik  
        validationSchema={schema}  
        onSubmit={handleSubmit}  
      >  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors,  
        }) => (  
            <Form noValidate onSubmit={handleSubmit}>  
              <Form.Row>  
                <Form.Group as={Col} md="12" controlId="firstName">  
                  <Form.Label>  
                    <h4>Image Search</h4>  
                  </Form.Label>  
                  <Form.Control  
                    type="text"  
                    name="query"  
                    placeholder="Keyword"  
                    value={values.query || ''}  
                    onChange={handleChange}  
                    isInvalid={touched.description && errors.query}  
                  />  
                  <Form.Control.Feedback type="invalid">  
                    {errors.query}  
                  </Form.Control.Feedback>  
                </Form.Group>  
              </Form.Row>  
              <Button type="submit">Search</Button>  
            </Form>  
          )}  
      </Formik>  
      <InfiniteScroll  
        pageStart={page}  
        loadMore={getMorePhotos}  
        hasMore={totalHits > items.length}  
        threshold={100}  
      >  
        {items.map((i, index) =>  
          <Figure key={index}>  
            <Figure.Image  
              width={window.innerWidth / 3.5}  
              src={i.previewURL}  
            />  
          </Figure>  
        )}  
      </InfiniteScroll>  
    </div>  
  );  
}

export default ImageSearchPage;
```

In here, we add the form for searching by keyword, and then displaying the images. We also have infinite scrolling on this page. This is where the Formik and Yup libraries are used to check if the query has been entered. Yup provides the validation schema, which passed into the `Formik` component and validates the field. The value changes for the form field are handled by Formik automatically, so we don’t have to write anything to do it.

Then we create`ImageSearchPage.css` and add:

```css
.ImageSearchPage{  
    padding: 20px;  
}
```

This adds padding to the page.

We then create `requests.js` and add:

```javascript
const APIURL = 'https://pixabay.com/api';  
const axios = require('axios');  
const querystring = require('querystring');  
const APIKEY = 'Pixabay API key';

export const getPhotos = (page = 1) => axios.get(`${APIURL}/?key=${APIKEY}&page=${page}`);

export const searchPhotos = (data) => {  
    data['key'] = APIKEY;  
    return axios.get(`${APIURL}/?${querystring.encode(data)}`);  
}  
export const searchVideos = (data) => {  
    data['key'] = APIKEY;  
    return axios.get(`${APIURL}/videos/?${querystring.encode(data)}`);  
}
```

This helps us make requests to the Pixabay API to find photos and videos. Replace the API key with your own key that you get from Pixabay when you register for one.

Next we create the video search page. We create a file called `VideoSearchPage.js` and add:

```jsx
import React from 'react';  
import './VideoSearchPage.css';  
import { searchVideos } from './requests';  
import { Formik } from 'formik';  
import Form from 'react-bootstrap/Form';  
import Col from 'react-bootstrap/Col';  
import Button from 'react-bootstrap/Button';  
import * as yup from 'yup';  
import './ImageSearchPage.css';  
import { useState } from 'react';  
import Figure from 'react-bootstrap/Figure'
const schema = yup.object({  
  query: yup.string().required('Query is required'),  
});

function VideoSearchPage() {  
  const [items, setItems] = useState([]); 
  const handleSubmit = async (evt) => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    const data = {  
      q: encodeURIComponent(evt.query),  
      page: 1  
    }  
    const response = await searchVideos(data);  
    setItems(items.concat(response.data.hits));  
  } 

  return (  
    <div className="ImageSearchPage">  
      <Formik  
        validationSchema={schema}  
        onSubmit={handleSubmit}  
      >  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors,  
        }) => (  
            <Form noValidate onSubmit={handleSubmit}>  
              <Form.Row>  
                <Form.Group as={Col} md="12" controlId="firstName">  
                  <Form.Label>  
                    <h4>Video Search</h4>  
                  </Form.Label>  
                  <Form.Control  
                    type="text"  
                    name="query"  
                    placeholder="Keyword"  
                    value={values.query || ''}  
                    onChange={handleChange}  
                    isInvalid={touched.description && errors.query}  
                  />  
                  <Form.Control.Feedback type="invalid">  
                    {errors.query}  
                  </Form.Control.Feedback>  
                </Form.Group>  
              </Form.Row>  
              <Button type="submit">Search</Button>  
            </Form>  
          )}  
      </Formik>  
      {items.map((i, index) =>  
        <Figure key={index}>  
          <video  
            width={window.innerWidth / 3.5}  
          >  
            <source src={i.videos.tiny.url} type="video/mp4" />  
          </video>  
        </Figure>  
      )}  
    </div>  
  );  
}

export default VideoSearchPage;
```

This is similar to the image search page except that we don’t have infinite scrolling and the `img` tag is replaced with the `video` tag.

Finally, in `public/index.html`, we have:

```html
<!DOCTYPE html>  
<html lang="en"><head>  
  <meta charset="utf-8" />  
  <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />  
  <meta name="viewport" content="width=device-width, initial-scale=1" />  
  <meta name="theme-color" content="#000000" />  
  <meta name="description" content="Web site created using create-react-app" />  
  <link rel="apple-touch-icon" href="logo192.png" />  
  <!--  
      manifest.json provides metadata used when your web app is installed on a  
      user's mobile device or desktop. See [https://developers.google.com/web/fundamentals/web-app-manifest/](https://developers.google.com/web/fundamentals/web-app-manifest/)  
    -->  
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />  
  <!--  
      Notice the use of %PUBLIC_URL% in the tags above.  
      It will be replaced with the URL of the `public` folder during the build.  
      Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will  
      work correctly both with client-side routing and a non-root public URL.  
      Learn how to configure a non-root public URL by running `npm run build`.  
    -->  
  <title>Image Gallery App</title>  
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"  
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />  
</head>
<body>  
  <noscript>You need to enable JavaScript to run this app.</noscript>  
  <div id="root"></div>  
  <!--  
      This HTML file is a template.  
      If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file.  
      The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`.  
      To create a production bundle, use `npm run build` or `yarn build`.  
    -->  
</body>
</html>
```

We added:

```html
<link rel="stylesheet" href="[https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css](https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css)"  
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
```

This adds Bootstrap styles in our code.