You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
    )
}
问题原因
  1. 缓存命中时错误替换数据:在getProducts函数中,命中缓存时直接用setProductDetails(cachedData)替换整个数组,而非追加到现有数据中,导致之前的产品DOM被移除,新DOM渲染后滚动位置重置到顶部。
  2. 加载时隐藏所有产品: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 08:42:03