0
(0)

Alright guys, a new React component has been created, gather around and lets discuss it.

Now let’s check out the To do list for our Infinitely scrollable list

Let me give you first the code, then you can decide if you want to see the explanation too

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';

const ProductItem = styled.div`
  display: flex;
  gap: 15px ;
  margin-bottom: 25px;
  padding:15px;
  background: #c9d838;
  color: #000;
  &:nth-child(odd) {
    background: #00a7a2;
    color: #fff;
  }
  img {
    display: block;
    border: 5px solid #fff;
  }
  .product-price {
    margin: 0 10px 0 auto;
    font-size: 25px;
    font-weight: bold;
  }
`;
const ProductTitle = styled.h2`
  font-size:25px;
  margin: 0;
`
type fetchData = {
  image: string,
  title: string,
  description: string,
  price: number
}

const ProductsWithInfiniteScroll = () => {
  const itemsPerPage = 5;
  const [data, setData] = useState<fetchData[] | undefined>();
  const [products, setProducts] = useState<fetchData[] | undefined>();
  const [currentPage, setCurrentPage] = useState(1);
  const [isEndOfList, setIsEndOfList] = useState(false);

  const setProductList = () => {
    const dataTill = currentPage * itemsPerPage;
    setProducts(data?.slice(0, dataTill));
    setCurrentPage(currentPage + 1); 
  };
  const updateProductList = () => {
    if(window.pageYOffset + window.innerHeight >= document.body.scrollHeight) {
      setProductList();
    }
  }

  useEffect(() => {
      fetch("http://localhost:4004/products")
        .then(response => response.json())
        .then(setData);
  }, []);
      
  useEffect(() => {
    if(!data) return;
    setProductList();
  }, [data]);

  useEffect(() => {
    if(!products || isEndOfList) return;
    
    window.addEventListener('scroll', () => {
      updateProductList();
    });
    
    return () => window.removeEventListener("scroll", updateProductList);
  }, [products]);

  useEffect(() => {
    if(data && currentPage === Math.ceil(data.length / itemsPerPage)) {
      setIsEndOfList(true);
    }
  }, [currentPage]);

  return (
    <>
      {products?.map((item,i) => (
        <ProductItem className="product" key={i}>
          <div className="product-image">
            <img src={item.image} alt="Sample Image" height="150" />
          </div>
          <div className="product-detail">
            <ProductTitle>{item.title}</ProductTitle>
            <p>{item.description}</p>
          </div>
          <div className="product-price">
            ${item.price.toFixed(2)}
          </div>
        </ProductItem>
      ))}
    </>  
  );  
}

export default ProductsWithInfiniteScroll;

So the component actually is built around a Product list, that can be a search results list or just simply a product enumeration. Of course you can ignore that part and just focus on the Infinite Scroll section.

Our JSON file looks like this and yes the products are beard shaving styles.(I used mockoon for the JSON mock data).

Let me explain

First we need to know how many items we show in “1 page”, or in “1 chunk”, or in “1 lazyload”, call it as you wish. So itemsPerPage is responsible to hold that information, in this case 5 ,but you can also add this as a prop to the component.

Secondly we need a state for the case when the JSON is fetched from the server and with setData we update that state, but we don’t want to display all data at once in the layout only 5 at a time. For that state that will hold the actual render data we will use the products state and initially we will slice the first 5 items from the data state and push it to the products state.

Also we need to keep track of the current page we are on, meaning how many blocks of 5 items did we already displayed. Finally we keep in the isEndOfTheList state the boolean value, if we arrived at the last items from the JSON to be displayed, this way we don’t call the infinite scroll function again and again.

On load we fetch the JSON data(only 1 time), this way there is less asyncron traffic on the internet. We better fetch all data once and then just work with the array, it seems more reasonable to me.

Then we wait till data fetch is completed and only then we populate the products state with the first 5 items or respectively with n x 5 items from the data state. Also we set in advance the next page’s number: setCurrentPage(currentPage + 1);

By multiplying the itemsPerPage(5 items) with currentPage number(starting with 1), we get the last item that should be fetched from all items, the start item is always the first(0 index). So first we fetch 0-5 items, then 0-10, then 0-15 and so on.

How about the scroll event?

If products state is not empty and we are not yet on the end of our list, we set up a scroll event listener but we do also a cleanup each time, because products will be updated multiple times, so we don’t want multiple scroll event listeners. Each time we recreate that scroll event listener and destroy it.

I call the same setProductList function that we used for the initial first 5 items, if the conditions are met and we are at the bottom of the page.

Why do we recreate the scroll event listener each time, we could add it when data state is updated 1 time or on onload( [] ) ?

Well we have a problem using a state in a useEffect, because if that state is updated the event listener will not catch it, it will always see the value of that state when the event listener was created. So we recreate the event listener each time that state is updated, this way it works.

How do we know when to stop loading new items?

We can check when the currentPage is updated if that currentPage is equal with the total number of the items divided with how many items we display on a page. In my case it was 24 items that I divided with 5 and rounded up to 5(pages).

Feel free to ask me anything about this solution !

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Categorized in: