Using the “Infinite Scrolling” Method to Fetch API Data in ReactJS
For today’s fun adventure we’ll be using ReactJS to fetch data from an API and output that data to the user. We’ll also test for how far the user has scrolled down the page and automatically load more data when they’ve reached the bottom.
The API data we’re going to use is from the fine folks at PunkAPI. They provide pictures, descriptions and recipes for craft beers. Soon we will, too!
What you’ll need:
— npm package manager
— VSCode
— create-react-app
— react/react-dom
The first thing we’re going to do is run a terminal, and from the command line enter “npx create-react-app ourcoolbeerapp” which will build the file structure and a template we can use instead of typing all the boiler stuff for a new app.
Note how the name of our app, “ourcoolbeerapp” is in all lower-case letters. That’s a requirement when using create-react-app to setup our template.
While that’s running (it takes a while) we can sashay over to github and create ourselves a repo to link to our app. We’ll also be connecting our github repo to herokuapp to host our nifty creation when we’re done. That will allow all our updates to be automatically deployed.
Speaking of which: here’s a live demo of what we’ll be making. This tutorial is only going to take us to the fetching all the data, but the rest is basically just styling and tinkering around with the same tools.
Now that our git repo is ready and our app has been setup, we can begin to make changes. Basically we’re going to hollow out the guts of the boilerplate app that create-react-app just installed on our machine and replace them with our own code.
Create-react-App should have given you the “happy hacking” message by now, so let’s get started.
Using VSCode, open up the directory we just created and it should look something like this:
As you can see already, there’s some extemporaneous stuff in there. Of course we’re not going to need the react logos. You can delete them now.
The only files we’re going to be editing are App.js and App.css from the “src” folder. You can edit the public/index.html file to change the title of the page if you want, but other than that there’ s not much else that requires changing.
Now let’s take a look at App.js and it should look like this:
Obviously we don’t need line 2, because that just imports the logo we already just deleted, so go ahead and trash that.
We’re going to be making our own components, so let’s change line 1 to read:
import React, {Component} from ‘react’;
You may already know that using reactjs we can make either components or elements. Elements are basically just an in-memory virtualization of items that are going to be rendered to the DOM. Components are functional objects that can possess their own states and methods.
Of course we’re going to need some functionality and state, so let’s just gut out that whole App() function altogether and replace it with something like this:
We turned the App() element into a Class so we could use its “state” to track the data from our API, so let’s define our state now. Add all this right after the App() declaration and before the render() call:
constructor(props) {super(props);this.state = {
beers: [],
page: 1
}}
What we’re doing is declaring the constructor for our class, just like in regular Javascript and establishing the props object we can pass to other components to use. We’ll be using that here in a minute.
We also set our “state” object to contain two elements; the beers array to hold the objects that the API will return and the “page” variable to remember what page of API data we’re currently loading.
Notice we set the default page to 1 instead of 0. That’s how the PunkAPI handles the data it delivers. Now we’re going to fetch that data!
Here’s the code to fetch the data:
fetchData = (pageNum) => {let beerUrl = 'https://api.punkapi.com/v2/beers?page='+pageNum;fetch(beerUrl)
.then(res=>res.json())
.then(data => {
this.setState({
beers: [...this.state.beers,...data]
})
})
}
This is all inside our App() class declaration. We pass it the pageNum variable, which has already been initialized to 1. Then we build the url for the API endpoint by taking the base API url, “https://api.punkapi.com/v2/beers” and appending “?page=pageNum” to the end of it.
Next we just fetch the url, parse the results into a JSON object and then we react to the data we’ve received. Let’s talk about that:
We set a “beers” array up in our “state” object. We use the built-in .setState() method to track our beers. Obviously, since we’re going to be making multiple API calls, we’re going to want to append the new data to the array instead of just replacing the array with the new data.
To accomplish that, we’re going to use the JSX “spread” operator. The spread operator treats an entire object or array as individual components, so in this case:
beers: [...this.state.beers,...data]
we’re just telling the state object to add the entire contents of the data object to the end of the beers array.
In case you’re wondering why we don’t use “push” to append items to the beer array, remember that we can’t mutate the state object directly, but only by the setState method.
That means we have to perform whatever functional manipulations we have to until we come up with just one item and setState to that one item, but we can’t setState to a function or operation.
Crazy, right? But not that big a deal, just something to keep in mind.
Now that we have our fetch method in place, we just need to call it. Let’s call it when the component has been applied to the page.
componentDidMount = () => {this.fetchData(this.state.page);}
“ComponentDidMount” is the last step in the “mounting” stage of the component lifecycle. That means that it’s been already been “imagined” and approved and has now been rendered onto the page for the user to gain access to.
That’s when we make our call to the first set of data. We’ll call the rest of the pages later, but first let’s start outputting the data we’ve already received just to get some action going.
We’re going to want to build a new component. Since we will be outputting a list of individual beers, we can make each beer into its own separate component. It’ll make sense when you see it, so let’s just go above our App() class declaration and add this Beer class:
class Beer extends Component {render() {return (
<div className="beerDiv">
<div style={{fontSize:'10pt'}}>{this.props.beer.name}</div>
<img src={this.props.beer.image_url} alt="" style={{width: '30px', float: 'left',paddingRight: '10px',paddingBottom: '10px'}}/>
<p style={{fontSize:'10pt'}}>{this.props.beer.tagline}</p>
</div>)
}
}
There’s a few things to remember about Components. They always start their name with a capital letter, so declaring it as Beer was no accident.
Also, if you just have a return statement that returns a bunch of individual elements you’re going to have a bad time. All the elements that are returned from a class component must be wrapped in a container. For this one we’ve made a div and assigned it the className “beerDiv”.
You may have noticed we don’t use the “class” property in our HTML. That’s because React has its own naming convention because it takes all the normal css styling instructions and converts them to objects.
Ex: in normal CSS you might use a css property called “border-radius”. In JSX you can access the same property, but you define it as an object instead of a string. That’s what the next line is doing:
<div style={{fontSize:'10pt'}}>
We’ve told JSX we want a div to be output, and we want to access the “font-size” property of the CSS. So we remove the hyphen, convert the name to camelCase notation and treat it like an object whose key is the property and value is the string ‘10pt’ (which will give us little tiny text).
The reason we made Beer a Component and not just an element is because we want to have access to its “props” object. You may notice we didn’t include a constructor or super declaration. That’s because if you’re not manipulating “state” then those aren’t needed.
But we do still have access to props, because the props will be included when we use this component inside its parent component, which in this case will be our App() class, which we’ll get to in a minute.
Notice this:
{this.props.beer.name}
That’s the entire content of the div we just defined. It’s kind of like a template literal in javascript. This curly brace notation is how we tell JSX to insert values that we have either inherited from our component’s parent or functionally created here in this class.
Now every time JSX runs across a Beer component it’s going to encapsulate the whole thing in a container, then put a div inside that container which contains only the name of the current item being rendered.
Let’s do the picture next!
<img src={this.props.beer.image_url} alt="" style={{width: '30px', float: 'left',paddingRight: '10px',paddingBottom: '10px'}}/>
More JSX. This time we’re using the good ‘ol HTML tag for an image <img and setting its src to our props object containing the image-url.
Notice the alt=”” property? That’s because if you don’t include an “alt” property for images, React whines about it. That’s really all it is, and an empty string will satisfy it.
Then we set some style; the image will be 30 pixels wide, float to the left, have a little space between it and the text to the right of it, and it will float 10 pixels above the bottom of its container div. Simple stuff you’re probably already bored of.
Next to that we add a paragraph tag with the “tagline” of the current beer that we’re displaying:
<p style={{fontSize:'10pt'}}>{this.props.beer.tagline}</p>
Then close up the div and we’re done. We’ve just defined one component to display one beer. Next we’re going to go back to our API results and map them all to Beer components and blast the whole thing out to the screen foro the user.
But first you may be wondering where we’re getting all those words for the “props”, and it’s a valid concern. It comes from here:
[
{
"id": 192,
"name": "Punk IPA 2007 - 2010",
"tagline": "Post Modern Classic. Spiky. Tropical. Hoppy.",
"first_brewed": "04/2007",
"description": "Our flagship beer that kick started the craft beer revolution. This is James and Martin's original take on an American IPA, subverted with punchy New Zealand hops. Layered with new world hops to create an all-out riot of grapefruit, pineapple and lychee before a spiky, mouth-puckering bitter finish.",
"image_url": "https://images.punkapi.com/v2/192.png",
"abv": 6.0,
"ibu": 60.0,
"target_fg": 1010.0,
"target_og": 1056.0,
"ebc": 17.0,
"srm": 8.5,
"ph": 4.4,
"attenuation_level": 82.14,
"volume": {
"value": 20,
"unit": "liters"
},
"boil_volume": {
"value": 25,
"unit": "liters"
},
"method": {
"mash_temp": [
{
"temp": {
"value": 65,
"unit": "celsius"
},
"duration": 75
}
],
"fermentation": {
"temp": {
"value": 19.0,
"unit": "celsius"
}
},
"twist": null
},
"ingredients": {
"malt": [
{
"name": "Extra Pale",
"amount": {
"value": 5.3,
"unit": "kilograms"
}
}
],
"hops": [
{
"name": "Ahtanum",
"amount": {
"value": 17.5,
"unit": "grams"
},
"add": "start",
"attribute": "bitter"
},
{
"name": "Chinook",
"amount": {
"value": 15,
"unit": "grams"
},
"add": "start",
"attribute": "bitter"
},
...
],
"yeast": "Wyeast 1056 - American Ale™"
},
"food_pairing": [
"Spicy carne asada with a pico de gallo sauce",
"Shredded chicken tacos with a mango chilli lime salsa",
"Cheesecake with a passion fruit swirl sauce"
],
"brewers_tips": "While it may surprise you, this version of Punk IPA isn't dry hopped but still packs a punch! To make the best of the aroma hops make sure they are fully submerged and add them just before knock out for an intense hop hit.",
"contributed_by": "Sam Mason <samjbmason>"
}
]
That’s one JSON object that’s returned from our fetch call. If you’ve already looked into the PunkAPI documentation, then you know it defaults to returning 25 results if you don’t manually tell it to return a different amount.
Let’s go back to our App() class and call our snazzy new Beer component for each of the results and it’ll make more sense why we refer to them the way we do.
You remember we already called our fetch API endpoint and used the setState method to store those results into our “beer” array. Now all we have to do is add one line to our return to call the Beer component with each element of our result set:
{this.state.beers.map((beerdata,idx) => (<Beer key={idx} beer={beerdata} />))}
Yes, that’s one line of code.
What we’re doing is taking the “state” object and “map”ping the beers array. The section that says (beerdata,idx) => is telling it to call each object in the array “beerdata” and assigning an “index” value (idx) to each one.
The reason we’re using the index value is because every time we output a component, JSX wants it to have a unique “key” to remind it where it is in the DOM and to make it recognizable to the Javascript engine as its own element. Makes it easier to access.
So basically, since the API returns 25 records, we’re just outputting this 25 times:
<Beer key={idx} beer={beerdata} />
Just a simple JSX call to render the component we defined earlier. But we call it 25 times, and each time we call it we send a new key and a new beer from the API results.
See we call it “beerdata” as the value and “beer” as the prop. That’s why the beer component references the data we send it like:
{this.props.beer.name}
It’s calling the “props” object that it’s been sent, and in that object it’s referencing the particular prop we’re interested in (there can be more than one) — which we called “beer”, and the “beerdata” value we provided it with when we called it is just one result from the API.
Scroll up this tutorial to the sample date returned from the API and you’ll see that the second item in the object is:
"name": "Punk IPA 2007 - 2010",
and that’s where it all comes from. If you wanted to access “alcohol by volume” in the Beer component you would call it like {this.props.beer.abv} and if you wanted the picture of the beer itself it would of course be: {this.props.beer.image_url} like we already used in our Beer component.
So just start your terminal in VSCode and type in “npm start” to get our server running. It may hem-and-haw a second or two, but then you should see something like this:
Your results may look a little different, but that’s only because of one thing. Remember when we made our Beer component, the first line of our render return was
<div className="beerDiv">
All that’s happening is that I’ve defined the “beerDiv” style already and you haven’t. So let’s do that now.
Go to your App.css page and get rid of everything in there. It’s all from the boilerplate stuff that create-react-app put in there, so just get rid of it.
I replaced mine with this:
.beerDiv {width: 200px;
height: 200px;
display: inline-block;
margin: 20px;
border: 1px solid black;}
and the output in the picture above is what you get. You’ll end up adding tons of styles in this file as well as some inline.
A quick note on styles: How do you know whether to use inline-styling or stylesheet styling?
That’s a good question and is nearly always able to provoke knock-down-drag-out disagreements in the coding community.
My guideline is to use it where it makes more sense to you when you come back to the code. Meaning that if you put this code away for 10 months then look at it again you’ll see that there’s a div that has some defined style to it, but it contains an image that you want to be 30px wide with some padding next to it and under it.
It’s that simple. You don’t want to clutter up your stylesheet using a whole definition for one style property for every. single. element. in your app, but you definitely do for the main controls and containers.
But now that we have all that, we still only have 25 beer results from the API! SO let’s add our “infinite scroller” to get them ALL!
Back in our App() class we want to add a method:
infiniteScroll = () => {
// End of the document reached?
if (window.innerHeight + document.documentElement.scrollTop
=== document.documentElement.offsetHeight){
let newPage = this.state.page;
newPage++; this.setState({
page: newPage
}); this.fetchData(newPage);
}
}
What this does is check to see if the user has scrolled to the bottom of the page. Caveat: there are about 11ty different ways to calculate the position in the document. Just find one you like. I like this one.
If they are at the bottom, then we set a variable called newPage and populate it with the “page” element of the “state” object. Then we increment the page and then use the this.setState() method to assign (not mutate) the value to the page element.
Finally we call the function we made earlier to retrieve the data, fetchData and pass it our new page number as a starting point for what data to retrieve.
But when do we call this function? When the user scrolls the window. And to do that we just add a listener during our componentDidMount() call from earlier. So now it looks like this:
componentDidMount = () => {
window.addEventListener('scroll', this.infiniteScroll);
this.fetchData(this.state.page);
}
That just tells it to check every time the user scrolls the page to call the infinite scroller function to get more data if they’re at the bottom.
And that’s it! Save your work, npm start your server and your page should look like what we saw earlier, only this time when you scroll down to the bottom it will automatically append new data to the end of the results and just keep going.
Fantastic.
LPT: when you see “Zipcode” beer you’re already at the end of the data.
Now you can go back and add more components, apply functionality to it, style it up some and deploy your amazing app on the herokuapp server (or your own) for the whole world to enjoy!
And here’s the repo for this example
And don’t forget to tip your server!