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.
Top comments (4)
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"
Thanks very much for reading.
Did you have Formik and Yup installed?
Also, this part:
is pretty important. You've to remember to pass that in.
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"
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
Did you destructure values property from the parameter?