componetnts re renders once data is fetched from backend and need to scroll form top to gte new items in infinirte loading reactjs

so my problem is i had a component implementing infinite loading but the data is fetching and work good but when new data is fetched from below it re render and in need to scroll from top
for ex : if i get 100 products from first req and display it and if i scroll to last element and it renders and goes to first component but products fetcted and displayed at bottom and added to it
"use client";
import './products.css';
import axios from "axios";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCartPlus, faL, faXmark } from "@fortawesome/free-solid-svg-icons";
import { useEffect, useRef, useState, useMemo } from "react";
import Loader from "../components/Reusable/Loader";
import { uselocalCart } from '../../../contexts/LocalCart';
export default function ProductPage() {
const cache = useRef({
category: null,
products: new Map(),
})
const [productDetails, setProductDetails] = useState([])
const [pages, setpages] = useState(1);
const [isLoading, setLoading] = useState(false);
const sentinel = useRef(null);
const [totalDocs, setTotalDocs] = useState(0);
const [filterString, setFilterString] = useState('all');
const { addTocart } = uselocalCart();
async function getProducts() {
if (isLoading === true || productDetails.length > totalDocs) {
return
}
const cacheKey = `products-page-${pages}-filter-${filterString}`;
if (cache.current.products.has(cacheKey)) {
const cachedData = cache.current.products.get(cacheKey);
setProductDetails(cachedData)
console.log("cache is true")
return;
}
let productData = []
try {
setLoading(true)
const res = await axios.get(`${process.env.NEXT_PUBLIC_URL}/api/getProducts`, {
params: { pages, limit: 10, filter: filterString || 'all' }
});
if (!res) {
throw new Error("Error occured");
}
console.log("req calls")
if (res.data?.isFind === true) {
productData = res.data.products;
setTotalDocs(parseInt(res.data.totalDocs));
cache.current.products.set(cacheKey, productData);
}
}
catch (e) {
console.log("Error occured", e);
}
finally {
setLoading(false)
}
return setProductDetails((prev) => [...prev, ...productData])
}
useEffect(() => {
getProducts()
}, [])
useEffect(() => {
if (!isLoading && (productDetails.length < totalDocs)) {
getProducts();
}
}, [pages]);
useEffect(() => {
if (isLoading || productDetails.length >= totalDocs) return;
const options = {
rootMargin: "0px",
threshold: 0.1,
};
const obs = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
obs.disconnect();
setpages((prev) => prev + 1);
}
}, options);
if (sentinel.current) {
obs.observe(sentinel.current);
}
return () => obs.disconnect();
}, [productDetails.length, totalDocs, isLoading]);
function setFilterCategory(id) {
const newFilter = id || 'all';
setFilterString(newFilter)
}
function clearFilter() {
setFilterString('all');
}
useEffect(() => {
return () => cache.current.products.clear();
}, [])
const memoizedFilterProducts = useMemo(() => {
if (isLoading) {
return []
}
if (filterString === 'all') {
return productDetails
}
return productDetails.filter(item => item.category === filterString)
}, [productDetails, filterString, isLoading]);
return (
<div id="product-container">
<div id="products-grid">
{
!isLoading && memoizedFilterProducts.length === 0 && <p>No products found</p>
}
{
memoizedFilterProducts.length > 0 && memoizedFilterProducts.map((item, ind) => {
return <div key={item._id} className="product-grid-wrapper" onClick={() => Showproduct(item)}
>
<div id="product-grid-img-div" >
<img src={item.files} alt="product-1" />
</div>
<div id="product-grid-contents-div" >
<div>
<p> {item.product_name} </p>
<div id='product-detail-div' >
<p> {item.uom} </p>
{
(() => {
let price = CalculateTotalPrice(item);
if (Number(item.price) !== Number(price)) {
return (
<p className={item.discount ? 'strike-rs' : ''}>
Rs {item.price}
</p>
);
}
})()
}
<p className='final-price' > Rs {CalculateTotalPrice(item)} </p>
</div>
</div>
<button className='bg-color' onClick={(e) => {
e.stopPropagation();
let pirceTouse = CalculateTotalPrice(item)
addTocart({ ...item, price: pirceTouse }, item._id)
}
} >
<FontAwesomeIcon icon={faCartPlus} height={25} width={25} color="white" />
</button>
</div>
</div>
})
}
<div id="last-div" ref={sentinel} style={{ visibility: 'hidden', height: '1px', width: '0' }} ></div>
</div>
{
isLoading && <Loader size={100} color="black" />
}
</div>
)
}
Answer
Issue
From what I see it seems the issue lies in how you're handling the loading state. When you start a new fetch, you set your product list (the filtered one, which actually renders) to an empty array ([]
) in the useMemo hook.
(this part)
const memoizedFilterProducts = useMemo(() => {
if (isLoading) {
return []
}
...
What happens is that:
- This triggers a rerender with 0 products, which most likely moves you to the top of the page, as no content below will exist for a while.
- Fetch completes, removes the loading state and you get the data to the array, but now you're staying on the top of the page since the first step, exactly like you would when you entered it for the first time.
Note that this probably happens very quickly, as you're likely fetching from a localhost, so what you see is a just a moving to the of the page.
Solution
What you can do is keep the current item list content when loading starts. That essentialy meens removing the first if
statement in the useMemo hook mentioned above.
I understand that you want to somehow visualize the loading is active, so you could maybe have a modal overlay with an loading animation, or something different, I think you can come up with something suitable.
Enjoyed this article?
Check out more content on our blog or follow us on social media.
Browse more articles