导读:本期聚焦于小伙伴创作的《React useEffect在无限滚动中重复触发导致数据重复该怎么解决》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《React useEffect在无限滚动中重复触发导致数据重复该怎么解决》有用,将其分享出去将是对创作者最好的鼓励。

在React项目中实现无限滚动功能时,useEffect的异常调用是很容易遇到的坑点,轻则导致接口重复请求浪费资源,重则让列表出现大量重复数据,严重影响用户使用体验。要解决这个问题,我们需要先理清触发原因,再针对性优化。

React useEffect在无限滚动中重复触发导致数据重复该怎么解决

常见问题诱因分析

无限滚动中useEffect重复触发、数据重复的核心原因通常有三类:

  • useEffect的依赖项配置不当,比如把会频繁变化的状态或者函数放进依赖数组,导致每次状态变化都重新执行effect
  • 滚动监听事件没有正确清理,组件卸载或者effect重新执行时没有移除之前的监听,导致多个监听同时存在
  • 接口请求没有加锁,上一次请求还没返回时又触发了新的请求,拿到数据后重复追加到列表

具体优化方案

1. 合理设置useEffect依赖项

首先不要随意把状态或者内联函数放进依赖数组,比如下面这种错误写法:

import { useEffect, useState } from 'react';

function InfiniteList() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);

  // 错误示例:把setList、setPage这类稳定的状态更新函数放进依赖,或者把page放进依赖导致频繁触发
  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      // 模拟接口请求
      const res = await new Promise(resolve => setTimeout(() => resolve([1,2,3]), 1000));
      setList(prev => [...prev, ...res]);
      setPage(prev => prev + 1);
      setLoading(false);
    };
    fetchData();
  }, [list, setList, setPage, page]); // 依赖项不合理,会反复触发
  return (
    <div>列表内容</div>
  );
}

正确的做法是把稳定的状态更新函数或者固定引用提取出来,避免不必要的重新执行,比如用useCallback包裹请求函数,并且只在必要的依赖变化时触发effect:

import { useEffect, useState, useCallback } from 'react';

function InfiniteList() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const fetchData = useCallback(async () => {
    if (loading || !hasMore) return;
    setLoading(true);
    try {
      // 模拟接口请求,实际项目中替换为真实接口
      const res = await new Promise(resolve => setTimeout(() => {
        const data = Array.from({ length: 10 }, (_, i) => page * 10 + i);
        resolve(data);
      }, 1000));
      setList(prev => [...prev, ...res]);
      // 假设最多加载5页,实际根据接口返回判断是否有更多数据
      if (page >= 5) {
        setHasMore(false);
      }
      setPage(prev => prev + 1);
    } finally {
      setLoading(false);
    }
  }, [loading, hasMore, page]);

  // 只在初始化时触发一次数据请求,后续滚动触发的请求通过其他方式调用
  useEffect(() => {
    fetchData();
  }, []);
}

2. 正确清理滚动监听副作用

无限滚动需要监听窗口或者容器的滚动事件,一定要在effect返回的函数里移除监听,避免内存泄漏和重复触发:

import { useEffect, useRef } from 'react';

function InfiniteList() {
  const containerRef = useRef(null);
  // 其他状态定义省略

  useEffect(() => {
    const container = containerRef.current;
    const handleScroll = () => {
      if (!container) return;
      const { scrollTop, clientHeight, scrollHeight } = container;
      // 距离底部小于50px时触发加载
      if (scrollHeight - scrollTop - clientHeight < 50) {
        // 调用请求函数,这里可以结合前面的请求锁逻辑
      }
    };
    container.addEventListener('scroll', handleScroll);
    // 清理副作用,移除监听
    return () => {
      container.removeEventListener('scroll', handleScroll);
    };
  }, []); // 依赖项为空,只在组件挂载时添加监听,卸载时移除
}

3. 添加请求锁避免重复请求

即使滚动监听逻辑正确,也可能出现快速滚动时多次触发请求的情况,这时候需要加一个请求锁,保证同一时间只有一个请求在执行:

import { useEffect, useState, useCallback, useRef } from 'react';

function InfiniteList() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const loadingRef = useRef(false); // 用ref存储请求状态,避免闭包问题
  const containerRef = useRef(null);

  const fetchData = useCallback(async () => {
    // 如果正在请求或者没有更多数据,直接返回
    if (loadingRef.current || !hasMore) return;
    loadingRef.current = true; // 加锁
    try {
      const res = await new Promise(resolve => setTimeout(() => {
        const data = Array.from({ length: 10 }, (_, i) => page * 10 + i);
        resolve(data);
      }, 1000));
      setList(prev => [...prev, ...res]);
      if (page >= 5) {
        setHasMore(false);
      }
      setPage(prev => prev + 1);
    } finally {
      loadingRef.current = false; // 解锁
    }
  }, [hasMore, page]);

  useEffect(() => {
    const container = containerRef.current;
    const handleScroll = () => {
      if (!container) return;
      const { scrollTop, clientHeight, scrollHeight } = container;
      if (scrollHeight - scrollTop - clientHeight < 50) {
        fetchData();
      }
    };
    // 可以给滚动事件加防抖,避免频繁触发
    let timer = null;
    const debounceScroll = () => {
      if (timer) clearTimeout(timer);
      timer = setTimeout(handleScroll, 200);
    };
    container.addEventListener('scroll', debounceScroll);
    return () => {
      container.removeEventListener('scroll', debounceScroll);
      if (timer) clearTimeout(timer);
    };
  }, [fetchData]);

  return (
    <div ref={containerRef} style={{ height: '500px', overflow: 'auto' }}>
      {list.map(item => <div key={item} style={{ height: '50px', borderBottom: '1px solid #eee' }}>{item}</div>)}
      {!hasMore && <div>没有更多数据了</div>}
    </div>
  );
}

总结

解决React useEffect在无限滚动中的重复触发和数据重复问题,核心是从依赖项配置、副作用清理、请求状态控制三个维度入手。合理设置useEffect的依赖,避免不必要的重新执行;正确清理滚动监听等副作用,防止内存泄漏;添加请求锁结合防抖处理,避免重复请求。按照这些思路优化后,无限滚动功能的稳定性会大幅提升,也能避免数据重复的问题。

ReactuseEffect无限滚动数据重复防抖修改时间:2026-06-03 01:30:54

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。