ReactJS无限加载组件获取数据后滚动至顶部问题求助
问题:React无限加载组件加载新数据后自动滚动到顶部
实现的React无限加载组件,数据获取功能正常,但下拉加载新数据后,组件重渲染并自动滚动至顶部。例如首次加载100个产品,滚动到底部触发加载后,新产品已添加到底部,但页面跳回顶部。
组件代码如下:
"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 occurred"); } 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 occurred", 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> ) }
问题原因
- 缓存命中时错误替换数据:在
getProducts函数中,命中缓存时直接用setProductDetails(cachedData)替换整个数组,而非追加到现有数据中,导致之前的产品DOM被移除,新DOM渲染后滚动位置重置到顶部。 - 加载时隐藏所有产品:
memoizedFilterProducts在isLoading为true时返回空数组,加载过程中产品全部消失,DOM高度骤变引发滚动异常。
解决方案
1. 修复缓存数据更新逻辑
将缓存命中时的状态更新改为追加模式,保留现有数据:
if (cache.current.products.has(cacheKey)) { const cachedData = cache.current.products.get(cacheKey); // 改为追加而非替换 setProductDetails(prev => [...prev, ...cachedData]) console.log("cache is true") return; }
2. 修正加载时的产品显示逻辑
修改memoizedFilterProducts,加载时保留现有产品数据:
const memoizedFilterProducts = useMemo(() => { if (filterString === 'all') { return productDetails } return productDetails.filter(item => item.category === filterString) }, [productDetails, filterString]);
3. 过滤条件切换时重置状态
切换filterString时,重置相关状态以重新加载对应分类的数据:
useEffect(() => { setProductDetails([]); setpages(1); setTotalDocs(0); cache.current.products.clear(); }, [filterString]);
4. 补充缺失的函数实现
代码中使用的Showproduct和CalculateTotalPrice函数未定义,需补充实现避免运行时错误:
// 示例实现,可根据实际需求调整 function Showproduct(item) { console.log('展示产品详情:', item); // 这里可添加跳转或弹窗逻辑 } function CalculateTotalPrice(item) { if (item.discount && item.discount > 0) { return (item.price * (100 - item.discount)) / 100; } return item.price; }
内容的提问来源于stack exchange,提问作者Vignesh
相关产品推荐
相关产品推荐

