Infinite scroll in React with mongodb

February 14, 2021
by Moiz Shaikh
  • Blog Featured Image

A lot of applications demand the ease of delivering content to the user. It you look at websites like Facebook, Instagram and so on, you’ll notice, that these applications load data as the user scrolls through it.  Infinite scroll is also common in ecommerce websites as an alternative to pagination.

Though both have their own pro and cons, In this article we will implement Infinite scroll in React and mongodb. To begin with, I have root directory which hosts all the server file. It also contains a client directory which contains the react app created using create-react-app.

Setting Up Node App

File Structure

File Structure

We will first need to install a couple of packages that will help up set up our backend server. In side the root directory simply run npm install cors dotenv express mongoose --save

The data.js file contains a list of games as dummy data which we will use to populate our Games collection. We will be using mongodb atlas to store our collection, the username and password of the same is stored in the .env file.

Using mongoose, we will create a Games Schema to create a Collection and populate it.

games.js
const mongoose = require('mongoose');

const Games = new mongoose.Schema({
    game: {type: String, required: true}
})

module.exports = mongoose.model('Games', Games);



We now import the Games model into our Index.js and initiate connection to our mongodb database.

index.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
const app = express();
const Games = require('./Models/games');
const data = require('./data');

mongoose.connect(
  `mongodb+srv://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@ihb-db.ql3nj.mongodb.net/Infinite-scroll-data?retryWrites=true&w=majority`,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  }
);
let Connection = mongoose.connection;


Connection.once('open', async() => {
  console.log("Connected")
  const gamesExist = await Games.find({});
  if(gamesExist.length)
  return;

  Games.insertMany(data).then(res => {
    console.log("Sucessfully inserted data")
  }).catch(err => console.log(err))
})


Connection.on('error', err => {
  console.error('connection error:',err)
})

app.use(express.json())
app.use(cors());

app.listen(5000, () => {
    console.log("server is running")
})

In the above code snippet, We’ve imported all the required packages along with dummy data file and the Games Model. We then use mongoose.connect to create a connection and pass the connection string that you can find on mongodb atlas. Next, we pass the ‘open’ event inside Connection.once() which executes the callback as soon as our connection is successful. In the callback function, we check whether the Games collection has already been populated. If yes then simply return, else populate our games collection with the dummy data.

Next, we need a route that will handle our requests for fetching appropriate set of data from the games collection based on the scroll request.
Inside Index.js,

index.js
app.get('/game', async(req, res) => {
  const count = +req.query.count;
  const page = +req.query.page;
  try{
    const response = await Games.find().skip(count * (page - 1)).limit(count)
    res.status(200).json({games: response})
  }catch(err){
    console.log(err)
  }
})

From the request query we get page and count, and use a simple skip-limit logic to get our next set of data. We are essentially skipping the entries that have already been fetched and then getting the next set of entries using limit.

Now that we have our server setup, let us move to the client app.

Setting up React App

Similar to the server, we’ll install a couple of packages for our react app. Navigate into the Client directory and run npm install axios styled-components --save

index.js
import React, {useState, useEffect, useCallback, useRef} from 'react';
import styled from "styled-components";
import axios from 'axios';

const App = () => {
  const [games, setGames] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [loadMore, setLoadMore] = useState(true);
  const [page, setPage] = useState(1);
  const observer = useRef();

  const items = games.map((el, index) => {
    if(games.length === index + 1){
      return <Item key = {el._id} ref = {lastItem}>{el.game}</Item>
    }else{
      return <Item key = {el._id}>{el.game}</Item>
    }
  })

 return (
    <Container>
        {items}
        {isLoading ? <Loading>Loading...</Loading>: null}
    </Container>
  );

export default App;

const Container = styled.div`
  margin: 0px auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 25%;
`

const Item = styled.div`
  height: 120px;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  border-bottom: 1px solid rgba(0,0,0,0.3);
`

const Loading = styled.div`
  width: 100%;
  font-size: 20px;
  background-color: red;
  text-align: center;

In App.js, we initialize couple of states. The games array holds the fetched data. isLoading and hasMore are boolean state. hasMore is set to false when there’s no more data to be fetched. The page is incremented every time a request is to made to load more entries.

There are a couple of ways to achieve infinite scroll, We’ll be making use of Intersection Observer Web API. Alternatively you can also use window’s client height to keep track of when the data needs to be fetched.

In the above code snippet, we map the games array, and assign a function reference on the last div element.

index.js
const lastItem = useCallback((element) => {
    if(observer.current) {
      observer.current.disconnect();
    }
    observer.current = new IntersectionObserver((entries) => {
      if(entries[0].isIntersecting && loadMore){
        setPage(prevPage => prevPage + 1);
      }
    }, {threshold: 1});

    if(element){
      observer.current.observe(element);
    }
  }, [loadMore])

Since functions are re-created on every render, setting a state inside lastItem without wrapping it with useCallback will cause our app to render infinitely. Inside this function, we get our last div element as a parameter. We conditionally call the disconnect method, if an html element(in our case the div) is already been observered. The reason behind this, is that every time we load data, that last element of the array is the one that needs to be observed.

We then instantiate IntersectionObserver and call the observe method(observer.current.observe(element)) with the element as the parameter. The IntersectionObserver callback gets executed when the element is visible in the viewport. Inside the callback we increment the page by 1.

The last thing we need to do, is make a request to our server, every time the page number is incremented.

index.js
 useEffect(() => {
    setIsLoading(true);
    let fetchGames = async() => {
      try {
        const response = await axios.get(`http://localhost:5000/game?page=${page}&count=10`)
        if(response.data.games.length === 0)
        setLoadMore(false)
        setGames(prevGames => [...prevGames , ...response.data.games])
      }catch(err){
        console.log(err)
      }
      setIsLoading(false);
    }
    fetchGames()
  }, [page])

Here in the useEffect, we have page as a dependency. We make a get request to the game endpoint and pass the page number and limit as query params. If the response returns an empty array, we set the the boolean state setLoadMore to false, indicating there’s no more data to be fetched. However, if we do get a response array, we spread the new data along with the old entries.

That’s all the code you need to implement Infinite scroll in react. You can now run your client and server app and see Infinite scroll in action. You can find the full github repository Here!

About

Jr. Software developer, Skilled in frontend technologies like React and Angular | Backend technologies like node and Express | SQL and NoSql Database.

Get notified on our new articles

Subscribe to get the latest on Node.js, Angular, Blockchain and more. We never spam!
First Name:
Email:

Leave a Comment