在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的依赖,避免不必要的重新执行;正确清理滚动监听等副作用,防止内存泄漏;添加请求锁结合防抖处理,避免重复请求。按照这些思路优化后,无限滚动功能的稳定性会大幅提升,也能避免数据重复的问题。