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

React加载更多初始page=0重复调用问题求助

问题:React加载更多首次page=0请求两次导致数据重复

实现React滚动加载更多功能时遇到以下问题:

  • 首次加载时pageNum=0被调用两次,导致前3条查询结果重复显示
  • 滚动加载pageNum=1、2时功能完全正常
  • 后端采用Python+MongoDB开发

用户提供的代码如下:

import './App.css';
import { debounce } from 'lodash';

const App = () => {
  const [videos, setVideos] = useState([]);
  const [page, setPage] = useState(0);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const limit = 3;
  const [initialLoad, setInitialLoad] = useState(true); // New state to track initial load

  useEffect(() => {
    if (initialLoad) {
      fetchVideos(page); // Initial load with page 0
      setInitialLoad(false); // Set initialLoad to false after initial fetch
    }
  }, [page, initialLoad]); // Include page and initialLoad in dependency array

  const fetchVideos = async (pageNum) => {
    setLoading(true);
    try {
      const response = await fetch(`http://127.0.0.1:5000/videos?page=${pageNum}&limit=${limit}`);
      const data = await response.json();
      console.log('Fetched videos:', data);
      if (data.length > 0) {
        setVideos(prevVideos => [...prevVideos, ...data]);
        setPage(pageNum + 1);
      } else {
        setHasMore(false);
      }
    } catch (error) {
      console.error('Error fetching videos:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleScroll = useCallback(() => {
    if (loading || !hasMore) return;
    const lastVideo = document.querySelector('.video-list > .video-card:last-child');
    if (!lastVideo) return;
    const lastVideoOffset = lastVideo.offsetTop + lastVideo.clientHeight;
    const pageOffset = window.pageYOffset + window.innerHeight;
    const bottomOffset = 20;

    if (pageOffset > lastVideoOffset - bottomOffset) {
      fetchVideos(page);
    }
  }, [loading, hasMore, page]);

  useEffect(() => {
    const handleScrollDebounced = debounce(() => {
      handleScroll();
    }, 300);

    window.addEventListener('scroll', handleScrollDebounced);
    return () => window.removeEventListener('scroll', handleScrollDebounced);
  }, [handleScroll]);

  return (
    <div className="App">
      <h1>Music Reels</h1>
      <div className="video-list">
        {videos.map(video => (
          <div className="video-card" key={video._id}>
            <video controls autoPlay muted>
              <source src={video.video_link} type="video/mp4" />
            </video>
            <div className="video-info">
              <h2>{video.title}</h2>
              <p>{video.description}</p>
              <p>{new Date(video.uploaded_date).toLocaleDateString()}</p>
            </div>
          </div>
        ))}
        {loading && <p>Loading...</p>}
        {!loading && !hasMore && <p>No more videos</p>}
      </div>
    </div>
  );
};

export default App;

解决方案

问题原因

  1. useEffect依赖数组触发重复执行:原初始加载的useEffect依赖了page和initialLoad,当setInitialLoad(false)修改状态后,会再次触发该useEffect,此时page仍为0,导致第二次调用fetchVideos(0)。
  2. React 18 Strict Mode双重执行:在Strict Mode下,React会刻意执行两次挂载相关的生命周期逻辑,用于检测潜在问题,这会加剧重复请求的现象。

修复方案

使用useRef标记初始加载状态(不会触发组件重渲染),并将初始加载逻辑设置为仅在组件挂载时执行一次:

完整修改后代码

import './App.css';
import { debounce } from 'lodash';
import { useState, useEffect, useCallback, useRef } from 'react';

const App = () => {
  const [videos, setVideos] = useState([]);
  const [page, setPage] = useState(0);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const limit = 3;
  const hasLoadedInitial = useRef(false); // 标记是否完成初始加载

  // 仅在组件挂载时执行一次初始加载
  useEffect(() => {
    if (hasLoadedInitial.current) return;
    hasLoadedInitial.current = true;
    fetchVideos(0);
  }, []);

  const fetchVideos = async (pageNum) => {
    setLoading(true);
    try {
      const response = await fetch(`http://127.0.0.1:5000/videos?page=${pageNum}&limit=${limit}`);
      const data = await response.json();
      console.log('Fetched videos:', data);
      if (data.length > 0) {
        setVideos(prevVideos => [...prevVideos, ...data]);
        setPage(pageNum + 1);
      } else {
        setHasMore(false);
      }
    } catch (error) {
      console.error('Error fetching videos:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleScroll = useCallback(() => {
    if (loading || !hasMore) return;
    const lastVideo = document.querySelector('.video-list > .video-card:last-child');
    if (!lastVideo) return;
    const lastVideoOffset = lastVideo.offsetTop + lastVideo.clientHeight;
    const pageOffset = window.pageYOffset + window.innerHeight;
    const bottomOffset = 20;

    if (pageOffset > lastVideoOffset - bottomOffset) {
      fetchVideos(page);
    }
  }, [loading, hasMore, page]);

  useEffect(() => {
    const handleScrollDebounced = debounce(() => {
      handleScroll();
    }, 300);

    window.addEventListener('scroll', handleScrollDebounced);
    return () => window.removeEventListener('scroll', handleScrollDebounced);
  }, [handleScroll]);

  return (
    <div className="App">
      <h1>Music Reels</h1>
      <div className="video-list">
        {videos.map(video => (
          <div className="video-card" key={video._id}>
            <video controls autoPlay muted>
              <source src={video.video_link} type="video/mp4" />
            </video>
            <div className="video-info">
              <h2>{video.title}</h2>
              <p>{video.description}</p>
              <p>{new Date(video.uploaded_date).toLocaleDateString()}</p>
            </div>
          </div>
        ))}
        {loading && <p>Loading...</p>}
        {!loading && !hasMore && <p>No more videos</p>}
      </div>
    </div>
  );
};

export default App;

修复说明

  • useRef的current属性不会触发组件重渲染,标记初始加载状态后不会重复触发useEffect。
  • 空数组依赖的useEffect仅在组件挂载时执行一次,彻底避免重复请求,同时兼容React 18 Strict Mode的双重执行机制。

内容的提问来源于stack exchange,提问作者Kumar Divyanshu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 05:07:32