Building the Full Infinite Scroll Functionality in React
This is gonna be a really fun guide, I know that we've taken a number of lectures to get up here and you've written quite a bit of code and in this guide, all of your work is going to come to fruition.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

We are going to take all of those pieces that we've built up to this point and we are going to connect them, and we're actually gonna see our infinite scrolling feature working on the screen, so this is gonna be a good one. One thing I will say is I like to think of these larger features like a journey, like a road trip or something like that.

And it's because before we started, we had to plan just like if you plan a long trip, you have to put into place the directions on where you wanna go, you have to make hotel reservations, you have to do all of these kinds of things and as you might have noticed, we did the same kind of planning as we're building out this feature.

We first decided the application's behavior, we decided what we wanted it to do, we talked about how a user would interact, and then from there, we were able to see all of the pieces that we needed. Everything from listening to scroll events to finding the dimensions of the page, and the document, and then combining that with the API call and that's what we're gonna do in this guide, so this is gonna be a really fun one.

Let's open up Visual Studio Code here and let's get started. The very first thing that I wanna do is let's just test out what would happen if we were to call getBlogItems inside of our Infinite Scroll feature. So I'm gonna say this.getBlogItems, I'll remove this console.log statement and I'm gonna add it down here inside of the response code and I'll say this is getting and then I want the response.data.

blog.js

axios
  .get("https://jordan.devcamp.space/portfolio/portfolio_blogs",
    withCredentials: true
  })
  .then(response => {
    console.log("getting", response.data);
    this.setState({
      blogItems: response.data.portfolio_blogs,
      totalCount: response.data.meta.total_records,

So what this is going to do is it's just gonna print out the API response every time getBlogItems is called which is now gonna be every time, one that the page loads, it gets called right when it's mounted, but then it also is going to be called whenever the user reaches the bottom of the screen.

So let's see what happens, I'm gonna open up the JavaScript Console here. You can see we already got our first version of this because it's called right when the page loads and as you can see, if I open this up, you can see I have the portfolio_blogs starting on ID 21 going down to ID 12, so that tells us our initial state and our initial records.

If I scroll all the way down to the bottom, you'll see we got another set of data. We got more portfolio_blogs and we got 10, so already something sounds a little bit off and if you open this up, you can see we got the exact same records. And all we effectively did is we took in that API call, we took the response, and we just overwrote what was already there.

Obviously, that's not what we're looking to do. What we need to do is have a way of telling the API in the server that we do not want the first set of records, we want the next set. And so, what we can do is we can pass an optional parameter to the URL right here. So I want to remove the hardcoded string and I'm going to use a string literal, so I'm gonna use backticks here.

And if you remember how we can add optional parameters to a URL at the very end of the string, you add a question mark, and then whatever the name of the parameter is. In this case, with the API as I designed it, I created something called a page parameter.

So when this is hit, when this URL is hit, the way that the system works is it looks to see if a page parameter has been set. If it has, then it will send a different set of records as opposed to if you just called the blogs without any of those.

So I'm gonna say page equals and now you're gonna see why we needed the currentPage and why we needed it in state and also to increment it. So add a dollar sign with beginning and ending curly brackets and say this.state.currentPage, hit save and what we're going to do now is because we have this currentPage in our system's local state, every time getBlogItems is called, it's going to increment it.

So the very first time, it's going to increment it and it's gonna say okay, I want the page one 'cause remember, we start with a current page of zero which doesn't exist. And the very first time this is called, it's gonna be, currentPage it's gonna be set to one. The next time, it's gonna be two and three and so on and so forth. So that is the parameter that we're going to send.

Now that's all we have to do in regards to the API, let's go and see what it's doing for us now. So we got our response back, we got our portfolio_blogs of 10. Now if you scroll all the way down, oh, they all disappeared, how did that happen? Well, let's walk through it and see.

Right now you have the second response, an array of 10 items and if you remember back to when we set the total number of records, you remember we did get 18 and that was located in the meta object. So when you add those up, we are getting all the records. But we don't have an error on the screen and all of our records are now gone, so what gives?

Well, we have a couple key bugs that we need to fix. One of them is related to this third response. Notice that even though we got all 18 records, we once again got another response, which means that we called the API one too many times.

So what's happening here and this might be a little confusing if you've never seen this before, we called setState and we got the response back and we said okay, I want those portfolio_blogs but all we're doing is we're taking that array that comes in and we're just overwriting what was already there.

So even if this third response didn't come through, so if we fixed the bug that triggered that, all that would happen is we would get down to the bottom of the page, we would call Infinite Scroll and then all of our records would be replaced by the eight records that came in right here.

So, the 10 that came in at the top would be overwritten, it would be kind of like if you were on Facebook or Instagram and you started scrolling down to the bottom of the page and instead of it simply tacking on additional stories or records or pictures, it replaced your entire timeline with the new ones that you asked for.

And that's not the behavior we're looking for, we want to take the 10 that we got and then we want to add on. You might say, we want to concat the other eight and that's the hint for what we need to do in order to implement this. And you've already used the concat method in the portfolio manager, but we will do it once again here.

So what we're gonna do is say okay, for blogItems, I want to and let me just give us a little bit of room, I wanna call this.state.blogItems, so I'm gonna grab whatever is there inside of the current list of blogItems and then I want to concat what comes in from the array which is gonna be this response.data.portfolio_blogs and then close off the parens, add a comma, and now this will fix one of the issues.

blog.js

.then(response => {
  console.log("gettting", response.data);
  this.setState({
    blogItems: this.state.blogItems.concat(response.data.portfolio_blogs),
    totalCount: response.data.meta.total_records,
    isLoading: false
  });
})

If you just go back to the browser right now and try this out, it's not going to work because we're still going to be hitting that other issue. So I'll show you right now, we can test this out, if I hit Refresh, and scroll all the way down, it starts to do it and oh and you know what? Actually we're not hitting the third issue at the moment because, and this is a good point.

So what we're doing now is because we came down to the bottom, we still are getting this empty third array. So that still is being triggered but all that's happening because we're using concat now is it's just adding an empty array on to our current array.

So nothing's happening, these records aren't being overwritten but it's not something that you specifically need to worry about, we're still gonna fix it, but that's the reason why all of those records are here. So let's just take a second to appreciate this, this is pretty cool.

I'm gonna hit Refresh, and scroll all the way down, and look at that, all of our records are showing up and it's very seamless, and that's really the goal of an infinite scroll feature is to work like that where these are, doesn't really know what's going on, they don't even have to do anything, really, it just works for them.

large

So now that we have that, let's go and let's fix that other issue. And it can be one that's a little bit tricky so let's walk through what it is. What happens is when we are in our activateInfiniteScroll method and this onscroll listener is occurring, what we need to do is we need to add some guards, so we need some validations in place.

We need to say things like okay, if the list of blogItems in state is equal to our totalCount, I do not want you to call getBlogItems again because that means that all of the records in the database are already on the screen, we don't need anymore. So that is what we're going to do and there's one other little trick that we're gonna implement that also is required. But for right now, let's just do the total record check.

So here I'll say if this.state.blogItems.length is triple equals to this.state.totalCount and if you have never done this before, in JavaScript, whenever you're in a function like we're in right now, if you just type return, the way that JavaScript works is it will return and break out of that entire function.

So what we're wanting to do is say if the totalCount is already there, if the blogItems is equal to that count, I want you to skip everything else below this and so that's the behavior that this will give us.

blog.js

activateInfiniteScroll() {
  window.onscroll = () => {
    if (this.state.blogItems.length === this.state.totalCount) {
      return;
    }

So we can come back here and test this out. We have our 10 items and if I come down to the bottom, this is still working very nicely. Now let's try one more thing, I'm gonna come up to the top, there's a little edge case that we need to watch out for.

So we have our 10 items here and I'm gonna quickly scroll here and this is something that I've seen happen in testing when I was originally building this and also when I've built an infinite scrolling feature in the past, is that sometimes if you try to load too many items too fast and so you may or may not see this on your side and it kinda depends with how you're using it, and I'll try it one more time, but either way, I'm gonna show you how to fix it even if we can't replicate it perfectly here, just because I've seen this happen in my own applications.

But I'm gonna scroll down to the bottom and sometimes it'll give you that portfolio_blogs or whatever response you get is sending you those empty records and so what's essentially happening is it's this right here is not being triggered because the user has tried to access the records too quickly.

large

So right here, our blogItems.length may not actually have gotten to the point where it equals the totalCount. So if the user comes down to the bottom of the page and they hit down on their mouse or on their keyboard too quickly before the state has been updated, it will try to call the API again. It won't really change anything as far as the user can see but I still count it as a bug because my goal is that this getBlogItems function should never be called if the records have been maxed out.

So what you can do to fix it is pretty easy. You can just say this.state.isLoading and then add the two pipes. Now these are not I's, these are the pipes key right above the Return key, hit Shift and then you'll have access to those pipes.

activateInfiniteScroll() {
  window.onscroll = () => {
    if (this.state.isLoading || this.state.blogItems.length === this.state.totalCount) {
      return;
    }

So what is happening here is I wanna say I do not want you to go and get more blogItems if we're in a loading state. And the reason for that is because that is what happens when I see a user triggering that too quickly is that they're trying to scroll down while the system is loading.

This is also something I've seen happen with slow internet connections. So I want to say if we're in a loading state, I want you to skip anything down here because remember, the loading status gets updated only when the response comes back, so that's what we're checking for there.

Once again, it's a little bit of an edge case but that will help prevent some kind of odd behavior and some bugs later on. So let's just make sure everything is still working, I'm gonna hit Refresh, and our initial loader is working and if you scroll down to the bottom, you'll see Infinite Scroll is working very nicely.

So great job if you went through this, I know that was a lot of code that you wrote and there are quite a few guides dedicated to just building out this feature but I think it was worth it, I think that by going through this, one, you've now learned how to build a non-trivial React feature.

This is something that you can now use in any application that you need pagination on in the future but two, we've walked through a professional approach at building a feature. So we took a step-by-step method for being able to figure out from the very beginning all the way through the end and everything in between on how to build a non-trivial feature in React and how we could leverage core JavaScript in order to do that.

So great job if you went through all of that. In the next guide, we're gonna have a few cleanup items that I wanna show you, a few edge case items that I wanna take care of and then we are gonna go after that and start building out the modal and give you the ability to create rich text and blogs inside of your app.

Resources