React setInterval 与 useEffect 结合会产生我不理解的行为。 setStatesetter函数里面需要一个回调函数

React setInterval in combination with useEffect creates behavior I don't understand. It requires a callback function inside setState setter function

我正在学习一门课程,但我不明白这两个代码示例中不同行为的逻辑原因。我从工作示例开始,然后突出显示破坏代码的地方。在类似的问题中有针对此问题的解决方案,但我没有找到正在发生的事情的描述或术语。

下面的 setInterval 工作正常,定时器功能按应有的速度每秒倒计时。有一个 secs useState 挂钩来跟踪秒数,还有一个 inProgress useState。

  React.useEffect(() => {
        if (!inProgress) {
            clearInterval(interval.current)
        } else {
            interval.current = setInterval(() => {
                setSecs((secs) => {
                    const timeLeft = secs - 1;
                    return timeLeft
                })
            }, 1000)
        }
        return () => clearInterval(interval.current);
  }, [inProgress])

下面是代码的变化和中断代码摘录。

      interval.current = setInterval(() => {
            setSecs(secs - 1)
            console.log('interval fires')
            console.log(secs)
        }, 1000)

此代码不起作用,它倒计时一秒钟,然后什么也没有发生。当使用上面看到的 console.logs 时,'interval fires' 和 'secs' 一样每秒打印一次,但是 secs 状态没有倒计时,它保持相同的数字。

    React.useEffect(() => {
    if (!inProgress) {
        clearInterval(interval.current)
    } else {
        interval.current = setInterval(() => {
            setSecs(secs - 1)
        }, 1000)
    }
    return () => clearInterval(interval.current);
}, [inProgress])

这似乎是记忆的重要行为,但我不知道如何对其进行分类。我正在寻找描述此行为的术语或对它的良好视觉解释。

上次 setSec 状态 运行 1000 毫秒前,即使我将 setInterval 设置为 10 秒,问题仍然存在。所以这不是基于时间的渲染、挂载、更新问题。我也不认为这可以用闭包来描述。我目前无法从逻辑上理解这种行为。

这是因为当你渲染你的组件时,每个渲染都有自己的propsstates.所以状态和道具在渲染中永远不会改变。
在 useEffect with emtpy dependency array 中,你知道它将 运行 在组件的第一次渲染上。这意味着当你在这种情况下 setInterval 时,你使用依赖于第一个渲染的道具和状态来设置它,即使 setInterval 被触发它也不会改变。
换句话说,您的 setInteval 函数将是 closure,状态将是自由变量。您在闭包中引用了这个 stale 变量。

React.useEffect(() => {
    if (!inProgress) {
        clearInterval(interval.current)
    } else {
        interval.current = setInterval(() => {
            setSecs(secs - 1) // state 'secs' are dependent on this render, free variable in this closure 
        }, 1000)
    }
    return () => clearInterval(interval.current);
}, [inProgress])

所以如果你想让它参考最近的状态,你可以在 setState 中使用 functional update 形式。 offical docs

setSecs(secs => secs - 1) // this form of setState guarantee that your state value is the recent one

但是如果你需要做的不仅仅是用状态值设置状态,你应该考虑使用useRef。与状态不同,ref 将始终为您提供 相同的对象引用 ,它独立于您的组件。 offical docs
您可以在 Dan Abramov's post 中了解更多详细信息,这对于理解 useEffect 和状态背后的逻辑非常有帮助。