DEV Community

John Au-Yeung
John Au-Yeung

Posted on

How to Add Infinite Scrolling to Your React App

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. 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/. 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:

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:

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:

.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:

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 createImageSearchPage.css and add:

.ImageSearchPage{  
    padding: 20px;  
}

This adds padding to the page.

We then create requests.js and add:

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:

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:

<!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:

<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.

Discussion (4)

Collapse
kctripathy profile image
Kishor Tripathy

Hi John,
Thanks for this nice article.
I am novice to React.
Was trying to replicate the given code but getting an error in image & video search at line no 57.
Can you please help me to find out why it throws the error?

TypeError: Cannot read property 'query' of undefined
(anonymous function)
D:/Learn/React/react-examples/image-app/src/VideoSearchPage.js:57
54 | type="text"

55 | name="query"

56 | placeholder="Keyword"

57 | value={values.query || ''}

| ^ 58 | onChange={handleChange}

59 | isInvalid={touched.description && errors.query}

60 | />

Collapse
aumayeung profile image
John Au-Yeung Author

Thanks very much for reading.

Did you have Formik and Yup installed?

Also, this part:

const schema = yup.object({  
  query: yup.string().required('Query is required'),  
});

is pretty important. You've to remember to pass that in.

Collapse
pjiocnic profile image
PJ

I'm getting the same error as Kishor but in ImageSearchPage.js

TypeError: Cannot read property 'query' of undefined

68 | type="text"
69 | name="query"
70 | placeholder="Keyword"

71 | value={values.query || ""}
| ^ 72 | onChange={handleChange}
73 | isInvalid={touched.description && errors.query}
74 | />

I do have
const schema = yup.object({
query: yup.string().required("Query is required"),
});

but still experiencing the problem. I don't think the problem is due to yup

Collapse
aumayeung profile image
John Au-Yeung Author

Did you destructure values property from the parameter?