useState 中的变量未在 useEffect 回调中更新

Posted

技术标签:

【中文标题】useState 中的变量未在 useEffect 回调中更新【英文标题】:variable in useState not updating in useEffect callback 【发布时间】:2020-03-19 21:31:30 【问题描述】:

我在使用 useState 和 useEffect 挂钩时遇到问题

import  useState, useEffect  from "react";

const counter = ( count, speed ) => 
    const [inc, setInc] = useState(0);

    useEffect(() => 

        const counterInterval = setInterval(() => 
            if(inc < count)
                setInc(inc + 1);
            else
                clearInterval(counterInterval);
            
        , speed);

    , [count]);

    return inc;


export default counter;

上面的代码是一个计数器组件,它在props中获取count,然后用0初始化inc并递增直到它等于count

问题是每次我得到 0 时,我都没有在 useEffect 和 setInterval 的回调中获得更新的 inc 值,因此它将 inc 呈现为 1,而 setInterval 永远不会清楚。我认为 inc 必须关闭使用 useEffect 和 setInterval 的回调,所以我必须在那里获取更新 inc,所以也许这是一个错误?

我无法在依赖项中传递 inc(在其他类似问题中建议),因为在我的情况下,我在 useEffect 中设置了 setInterval,因此在依赖项数组中传递 inc 会导致无限循环

我有一个使用有状态组件的可行解决方案,但我想使用功能组件来实现这一目标

【问题讨论】:

对否决票有什么解释吗? 尝试使用回调来使用最新值:setInc(inc =&gt; inc + 1);。如果有帮助,请告诉我。 那么请告诉我们你是如何使用这个钩子的 请注意,你的钩子不能这么简单,因为 useEffect 需要 speedinc 作为依赖项,你应该返回一个函数来清除间隔 @Alvaro,我尝试使用 setInc(inc => inc + 1);现在我在 setInc 的回调中更新了 inc,但仍然没有在 setInterval 的回调中更新 inc,所以它永远不会进入导致无限循环的 else 条件 【参考方案1】:

有几个问题:

    您没有从 useEffect 返回函数来清除间隔 您的 inc 值不同步,因为您没有使用之前的 inc 值。

一个选项:

const counter = ( count, speed ) => 
    const [inc, setInc] = useState(0);

    useEffect(() => 
        const counterInterval = setInterval(() => 
            setInc(inc => 
                if(inc < count)
                    return inc + 1;
                else
                    // Make sure to clear the interval in the else case, or 
                    // it will keep running (even though you don't see it)
                    clearInterval(counterInterval);
                    return inc;
                
            );
        , speed);

        // Clear the interval every time `useEffect` runs
        return () => clearInterval(counterInterval);

    , [count, speed]);

    return inc;

另一种选择是在 deps 数组中包含 inc,这使事情变得更简单,因为您不需要在 setInc 中使用之前的 inc

const counter = ( count, speed ) => 
    const [inc, setInc] = useState(0);

    useEffect(() => 
        const counterInterval = setInterval(() => 
            if(inc < count)
                return setInc(inc + 1);
            else
                // Make sure to clear your interval in the else case,
                // or it will keep running (even though you don't see it)
                clearInterval(counterInterval);
            
        , speed);

        // Clear the interval every time `useEffect` runs
        return () => clearInterval(counterInterval);

    , [count, speed, inc]);

    return inc;

还有一种更简单的第三种方法: 在 deps 数组中包含 inc,如果是 inc &gt;= count,则在调用 setInterval 之前提前返回:

    const [inc, setInc] = useState(0);

    useEffect(() => 
        if (inc >= count) return;

        const counterInterval = setInterval(() => 
          setInc(inc + 1);
        , speed);

        return () => clearInterval(counterInterval);
    , [count, speed, inc]);

    return inc;

【讨论】:

选项一:else case 是返回 inc 然后清除 setInterval 这样该行将永远不会被执行并会导致无限循环 您的所有选项都运行良好,感谢您的回答,如果应该返回 setInc(inc + 1),则第二个选项存在一个问题【参考方案2】:

这里的问题是来自clearInterval 的回调是在每次useEffect 运行时定义的,也就是count 更新的时间。 inc 定义时的值是回调中将读取的值。

此编辑采用不同的方法。我们包含一个 ref 来跟踪 inc 小于 count,如果小于 inc,我们可以继续递增。如果不是,那么我们清除计数器(就像您在问题中所说的那样)。每次inc 更新时,我们都会评估它是否仍然小于计数并将其保存在ref 中。然后在之前的useEffect 中使用此值。

正如@DennisVash 在他的回答中正确指出的那样,我包含了对speed 的依赖。

const useCounter = ( count, speed ) => 
    const [inc, setInc] = useState(0);
    const inc_lt_count = useRef(inc < count);

    useEffect(() => 
        const counterInterval = setInterval(() => 
            if (inc_lt_count.current) 
                setInc(inc => inc + 1);
             else 
                clearInterval(counterInterval);
            
        , speed);

        return () => clearInterval(counterInterval);
    , [count, speed]);

    useEffect(() => 
        if (inc < count) 
            inc_lt_count.current = true;
         else 
            inc_lt_count.current = false;
        
    , [inc, count]);

    return inc;
;

【讨论】:

@DennisVash 感谢您告诉我,您能否详细说明为什么它不起作用? 你检查了吗?原因很多,主要是增加reference不会导致re-render 实际上,在父组件(我使用 Counter)中,count 的值来自服务器,这就是我将它添加到依赖数组中的原因,我可以通过在之后安装 Counter 组件来删除它的依赖API 调用。所以 useEffect 将作为 componentDidMount 工作。现在使用 useRef 不会重新渲染组件。所以我需要状态来重新渲染组件 inc.current 值正在更新,但反应没有重新渲染它,因为它不在状态 @AbhaySehgal 这就是我所说的“副作用”。所以你需要inc 在一个状态中。好的,让我编辑。【参考方案3】:

需要处理的主要问题是Closures和依赖于props的条件下的清除间隔。

您应该在功能 setState 中添加条件检查:

setInc(inc => (inc < count ? inc + 1 : inc));

此外,清除间隔应该发生在卸载时。

如果要在条件(inc &lt; count)上添加clearInterval,则需要保存对区间id和增加数的引用:

import React,  useState, useEffect, useRef  from 'react';
import ReactDOM from 'react-dom';

const useCounter = ( count, speed ) => 
  const [inc, setInc] = useState(0);

  const incRef = useRef(inc);
  const idRef = useRef();

  useEffect(() => 
    idRef.current = setInterval(() => 
      setInc(inc => (inc < count ? inc + 1 : inc));
      incRef.current++;
    , speed);

    return () => clearInterval(idRef.current);
  , [count, speed]);

  useEffect(() => 
    if (incRef.current > count) 
      clearInterval(idRef.current);
    
  , [count]);

  useEffect(() => 
    console.log(incRef.current);
  );

  return inc;
;

const App = () => 
  const inc = useCounter( count: 10, speed: 1000 );
  return <h1>Counter : inc</h1>;
;

ReactDOM.render(<App />, document.getElementById('root'));

【讨论】:

这可能适用于这个特定的用例,但如果组件重新渲染太频繁会导致错误:overreacted.io/making-setinterval-declarative-with-react-hooks 你能重现任何错误吗? 在 setInc 函数中尝试 console.log(),它会无限调用 它会一直运行到卸载,我猜你想在条件下清除间隔,让我修复它 @AbhaySehgal 我编辑了答案,现在它清除了计数条件

以上是关于useState 中的变量未在 useEffect 回调中更新的主要内容,如果未能解决你的问题,请参考以下文章

在 useEffect API 调用之前调用 useState 变量

act 和 useState 的单元测试错误,在 useEffect 中进行 API 调用

useEffect 中的 useState 不更新状态

useState和useEffect

如何检查 useEffect 中的数据加载

useState 和 useEffect 有啥区别?