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

移除useEffect的fetchData依赖后index状态不更新的原因

React useEffect与useCallback依赖问题解析

问题描述

我实现了一个滚动加载的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 04:10:23