移除useEffect的fetchData依赖后index状态不更新的原因
问题描述
我实现了一个滚动加载的React组件,核心逻辑是滚动到容器底部时调用fetchData请求新数据并更新index状态。但发现最后一个useEffect必须依赖fetchData,否则index无法正常更新,想搞懂背后的原因。
相关代码
export default function ImagePage() { const [artData, setArtData] = useState([]) const [isLoading, setIsLoading] = useState(false) const [index, setIndex] = useState(2) const url = "https://api.artic.edu/api/v1/artworks?page=1&limit=5" const fetchData = useCallback(async () => { if (isLoading) return setIsLoading(true) axios.get(`https://api.artic.edu/api/v1/artworks?page=${index}&limit=5`) .then((res) => { setArtData(((prevItems) => [... new Set ([...prevItems, ...res.data.data])])) setIndex((prevIndex) => prevIndex + 1) setIsLoading(false) }) .catch((err) => {console.log(err)}) }, [index, isLoading]) useEffect(() => { setIsLoading(true) axios.get(url) .then((res) => { setArtData(res.data.data) setIsLoading(false) console.log("use effect loaded") }) .catch((err) => { console.log(err) }) }, []) useEffect(() => { const handleScroll = () => { const {scrollTop, clientHeight, scrollHeight} = document.querySelector('#image-page-container') if (scrollTop + clientHeight >= scrollHeight - clientHeight) { console.log("Fetching data") console.log(index) fetchData() } } document.querySelector('#image-page-container').addEventListener("scroll", handleScroll) return () => { document.querySelector('#image-page-container').removeEventListener('scroll', handleScroll) } }, [fetchData]) // 移除该依赖后index状态无法正常更新
原因解析
1. 闭包陷阱是核心问题
当你移除fetchData作为依赖时,这个useEffect只会在组件第一次挂载时执行一次。此时创建的handleScroll函数会捕获挂载时的fetchData版本——这个版本里的index是初始值2,而且永远不会更新。
因为fetchData用useCallback包裹,它的依赖是index和isLoading:每次index变化,useCallback都会生成一个新的fetchData函数实例,里面携带最新的index值。但如果useEffect不监听fetchData,它就不会重新执行,也就不会创建新的handleScroll去捕获最新的fetchData。
所以每次滚动触发handleScroll时,调用的都是挂载时的旧fetchData,里面的index永远是2。哪怕你调用setIndex(prev => prev+1)确实更新了state,但下次触发滚动时,还是会用旧的fetchData,拿的还是旧的index值,看起来就像index根本没更新。
2. 依赖列表的本质是同步闭包
useEffect的依赖列表不是摆设,它的作用是告诉React:当依赖项变化时,必须重新执行副作用函数,创建新的闭包,捕获最新的状态和函数。
这里把fetchData加入依赖后,每次index变化导致fetchData更新,useEffect就会重新执行:先移除旧的滚动监听,再创建新的handleScroll——这个新的handleScroll会捕获最新的fetchData,里面的index是当前最新值,这样调用fetchData时才能正确使用最新的index,并更新到下一个值。
3. 为什么移除依赖后看似“正常运行”?
移除依赖后,第一次滚动确实能触发请求,因为初始的fetchData能正常调用。但后续滚动时,fetchData还是旧的实例,index一直是2,请求的都是同一页数据,setIndex虽然执行,但下次调用还是拿旧的index,所以不仅index看起来没更新,新数据也不会加载进来。
优化小建议
- 可以用
useRef缓存滚动容器元素,避免每次滚动都查询DOM,提升性能:const containerRef = useRef(null); // 渲染时给容器加ref:<div id="image-page-container" ref={containerRef}> // 后续用containerRef.current替代document.querySelector - 如果想减少
fetchData的依赖,可以把index作为参数传入fetchData,而不是让它依赖index状态,不过当前写法已经是合理的,不需要强行修改。
内容的提问来源于stack exchange,提问作者Thanh Nguyen

