Introduction
Infinite scrolling is when a user reaches the bottom of a page and new content is fetched and loaded so the user can continue to scroll in a relatively seamless experience. This is an alternative to other pagination solutions which use numbered pages or buttons that load more content.
You may have encountered infinite scrolling in applications like Instagram. You are presented with a feed of images and as you scroll down, more images keep showing up. Over and over and over until they run out of content to give you.
In this tutorial, you will touch upon the two key concepts that allow infinite scrolling to work – detecting when the user has reached the bottom of the page and loading the next batch of content to display. You will use these concepts to construct a display of astronomy photos and videos.
Prerequisites
To complete this tutorial, you’ll need:
A local development environment for Node.js. Follow How to Install Node.js and Create a Local Development Environment.
This tutorial will utilize NASA’s Astronomy Picture of the Day (APOD) API. For demonstration purposes, it will utilize the DEMO_KEY
for requests, but you may wish to sign up for an API key if you are generating many requests.
This tutorial was verified with Node v14.12.0, npm
v6.14.8, react
v16.13.1, superagent
v6.1.0, and lodash.debounce
v2.7.1.
Step 1 — Setting Up the Project
Start with using create-react-app
to generate a React App and then install dependecies:
npx create-react-app react-infinite-scroll-example
Change into the new project directory:
cd react-infinite-scroll-example
For loading data from the APOD API, you will be using superagent
.
For debouncing the events, you will be using lodash
.
To add superagent
and lodash.debounce
to your project via npm
run:
npm install superagent@6.1.0 lodash.debounce@4.0.8
Now, you can run the React application:
npm start
Fix any errors or issues with your project. And visit localhost:3000
in a web browser.
Once you have a working React application, you can start building your infinite scroll functionality.
Step 2 — Implementing onscroll
and loadApods
Infinite scrolling will require two key parts. One part will be a check for the window scroll position and the height of the window to determine if a user has reached the bottom of the page. Another part will be handling the request for additional information to display.
Let’s start by creating an InfiniteSpace.js
file:
nano src/InfiniteSpace.js
Construct your InfiniteSpace
component:
src/InfiniteSpace.js
import React from 'react';
import request from 'superagent';
import debounce from 'lodash.debounce';
class InfiniteSpace extends React.Component {
constructor(props) {
super(props);
this.state = {
apods: [],
};
}
render() {
return (
<div>
<h1>Infinite Space!</h1>
<p>Scroll down to load more!!</p>
</div>
)
}
}
export default InfiniteSpace;
The crux of the infinite scroll component is going to be an onscroll
event that will check to see if the user has scrolled to the bottom of the page. Upon reaching the bottom of the page, the event will attempt to load additional content.
When binding events, especially to scroll events, it is good practice to debounce the events. Debouncing is when you only run a function once a specified amount of time has passed since it was last called.
Debouncing improves performance for your user by limiting how often an event is fired and also helps take some strain off of any services you may be calling from the event handler.
src/InfiniteSpace.js
class InfiniteSpace extends Component {
constructor(props) {
super(props);
this.state = {
apods: [],
};
window.onscroll = debounce(() => {
const {
loadApods
} = this;
if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
loadApods();
}
}, 100);
}
// ...
}
This code establishes a debounce iteration of 100 milliseconds.
The loadApods
function will use superagent
’s request
to GET
the Astronomy Picture of the Day:
src/InfiniteSpace.js
class InfiniteSpace extends Component {
constructor(props) {
// ...
}
dayOffset = () => {
let today = new Date();
let day = today.setDate(-1 * this.state.apods.length);
return new Date(day).toISOString().split('T')[0];
}
loadApods = () => {
request
.get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
.then((results) => {
const nextApod = {
date: results.body.date,
title: results.body.title,
explanation: results.body.explanation,
copyright: results.body.copyright,
media_type: results.body.media_type,
url: results.body.url
};
this.setState({
apods: [
...this.state.apods,
nextApod
]
});
});
}
render() {
// ...
}
}
A dayOffset
function will be used to calculate the previous Astronomy Picture of the Day.
This code will map the response from APOD to store values for date
, title
, explanation
, copyright
, media_type
, and url
.
The data that has been loaded will be appended to an array in the component’s state and will be iterated through in the component’s render
method.
To verify that your two pieces work together, let’s render the response:
class InfiniteSpace extends Component {
// ...
render() {
return(
<div>
<h1>Infinite Space!</h1>
<p>Scroll down to load more!!</p>
{apods.map(apod => (
<React.Fragment key={apod.date}>
<hr />
<div>
<h2>{apod.title}</h2>
{apod.media_type === 'image' &&
<img
alt={`NASA APOD for {apod.date}`}
src={apod.url}
style={{
maxWidth: '100%',
height: 'auto'
}}
/>
}
{apod.media_type === 'video' &&
<iframe
src={apod.url}
width='640'
height='360'
style={{
maxWidth: '100%'
}}
></iframe>
}
<div>{apod.explanation}</div>
<div>{apod.copyright}</div>
</div>
</React.Fragment>
))}
<hr />
</div>
);
}
}
This code will display either an img
or an iframe
depending on the media_type
for the APOD.
At this point, you can modify your App
component to import InfiniteSpace
. Open App.js
:
nano src/App.js
And replace the content generated by Create React App with the InfiniteSpace
component:
src/App.js
import React from 'react';
import InfiniteSpace from './InfiniteSpace';
function App() {
return (
<div className="App">
<InfiniteSpace />
</div>
);
}
export default App;
At this point, you can run your application again:
npm start
Fix any errors or issues with your project. And visit localhost:3000
in a web browser.
If you scroll down the height of the webpage, you will trigger the conditions for the onscroll
event to fire loadApods
and a new APOD should appear on the screen.
With these two pieces for infinite scrolling in place, you have established the bulk of the InfiniteSpace
component. Adding an initial load and error handling will help make it more robust.
Step 3 — Adding an Initial Load and Error Handling
Presently, InfiniteSpace
does not load any APODs until the conditions for the onscroll
event are met. There are also three situations where you will not want to load APODs: if there are no more APODs to load, if you are currently loading an APOD, and if you encounter an error. Let’s address these issues.
First, revisit InfiniteSpace.js
:
nano src/InfiniteSpace.js
Then, use componentDidMount()
for an initial load:
src/InfiniteSpace.js
class InfiniteSpace extends Component {
constructor(props) {
// ...
}
componentDidMount() {
this.loadApods();
}
dayOffset = () => {
// ...
}
loadApods = () => {
// ...
}
render() {
// ...
}
}
Add error
, hasMore
, and isLoading
to the state to address errors and restrict unecessary loading:
src/InfiniteSpace.js
class InfiniteSpace extends Component {
constructor(props) {
super(props);
this.state = {
error: false,
hasMore: true,
isLoading: false,
apods: []
};
// ...
}
// ...
}
error
is initially set to false
. hasMore
is initially set to true
. And isLoading
is initially set to false
.
Then, apply state to onscroll
:
src/InfiniteSpace.js
class InfiniteSpace extends Component {
constructor(props) {
super(props);
this.state = {
error: false,
hasMore: true,
isLoading: false,
apods: []
};
window.onscroll = debounce(() => {
const {
loadApods,
state: {
error,
isLoading,
hasMore
}
} = this;
if (error || isLoading || !hasMore) return;
if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
loadApods();
}
}, 100);
}
// ...
}
This check will bail early and prevent loadApods
from being called in situations where there is an error, it is currently loading, or there are no additional APODs to load.
Then, apply state to loadApods
:
src/InfiniteSpace.js
class InfiniteSpace extends Component {
// ...
loadApods = () => { this.setState({ isLoading: true }, () => {
request
.get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
.then((results) => {
const nextApod = {
date: results.body.date,
title: results.body.title,
explanation: results.body.explanation,
copyright: results.body.copyright,
media_type: results.body.media_type,
url: results.body.url
};
this.setState({
hasMore: (this.state.apods.length < 5),
isLoading: false,
apods: [
...this.state.apods,
nextApod
],
});
})
.catch((err) => {
this.setState({
error: err.message,
isLoading: false
});
});
});
}
// ...
}
This code uses setState with a callback function passed-in as the second argument. The initial call to setState
in the loadApods
method sets the value of isLoading
to true
and then in the callback function the next APOD is loaded and setState
is called again to set isLoading
to false
.
For the purposes of our tutorial, hasMore
is a boolean check to limit the amount of APODs to 5
. In different scenarios, an API may return some value as part of the payload that indicates whether there is any more content to load.
If loadApods
encounters an error, error
is set to err.message
in the catch
block.
Then, apply state to render
:
src/InfiniteSpace.js
class InfiniteSpace extends Component {
// ...
render() {
const {
error,
hasMore,
isLoading,
apods
} = this.state;
return (
<div>
{/* ... React.Fragment ... */}
{error &&
<div style={{ color: '#900' }}>
{error}
</div>
}
{isLoading &&
<div>Loading...</div>
}
{!hasMore &&
<div>Loading Complete</div>
}
</div>
);
}
]
This will now display messages for error
, isLoading
, and hasMore
.
When all the pieces are put together, InfiniteSpace
will look like this:
src/InfiniteSpace.js
import React from 'react';
import request from 'superagent';
import debounce from 'lodash.debounce';
class InfiniteSpace extends React.Component {
constructor(props) {
super(props);
this.state = {
error: false,
hasMore: true,
isLoading: false,
apods: []
};
window.onscroll = debounce(() => {
const {
loadApods,
state: {
error,
isLoading,
hasMore,
},
} = this;
if (error || isLoading || !hasMore) return;
if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
loadApods();
}
}, 100);
}
componentDidMount() {
this.loadApods();
}
dayOffset = () => {
let today = new Date();
let day = today.setDate(-1 * this.state.apods.length);
return new Date(day).toISOString().split('T')[0];
}
loadApods = () => {this.setState({ isLoading: true }, () => {
request
.get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
.then((results) => {
const nextApod = {
date: results.body.date,
title: results.body.title,
explanation: results.body.explanation,
copyright: results.body.copyright,
media_type: results.body.media_type,
url: results.body.url
};
this.setState({
hasMore: (this.state.apods.length < 5),
isLoading: false,
apods: [
...this.state.apods,
nextApod
],
});
})
.catch((err) => {
this.setState({
error: err.message,
isLoading: false
});
});
});
}
render() {
const {
error,
hasMore,
isLoading,
apods
} = this.state;
return (
<div style={{
padding: 10
}}>
<h1>Infinite Space!</h1>
<p>Scroll down to load more!!</p>
{apods.map(apod => (
<React.Fragment key={apod.date}>
<hr />
<div>
<h2>{apod.title}</h2>
{apod.media_type === 'image' &&
<img
alt={`NASA APOD for {apod.date}`}
src={apod.url}
style={{
maxWidth: '100%',
height: 'auto'
}}
/>
}
{apod.media_type === 'video' &&
<iframe
src={apod.url}
width='640'
height='360'
style={{
maxWidth: '100%'
}}
></iframe>
}
<div>{apod.explanation}</div>
<div>{apod.copyright}</div>
</div>
</React.Fragment>
))}
<hr />
{error &&
<div style={{ color: '#900' }}>
{error}
</div>
}
{isLoading &&
<div>Loading...</div>
}
{!hasMore &&
<div>Loading Complete</div>
}
</div>
);
}
}
export default InfiniteSpace;
Finally, run your application again:
npm start
Fix any errors or issues with your project. And visit localhost:3000
in a web browser.
Scroll down and your application will fetch and display 5 APODs. All the pieces for infinite scrolling have come together.
Conclusion
In this tutorial, you implemented infinite scrolling in a React application. Infinite scrolling is one modern solution to potentially address presenting a lot of information to the end-user without large initial loading times.
If your project has content in the footer of the page that you want the user to reach, infinite scrolling may result in a worse user experience.
There are also other libraries that provide this functionality that may be best suited for the needs of your project.
If you’d like to learn more about React, take a look at our How To Code in React.js series, or check out our React topic page for exercises and programming projects.