反应useState不会更新值

时间:2020-09-03 17:15:11

标签: javascript reactjs react-hooks use-effect use-state

我对于为什么该组件无法按预期工作感到困惑:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // This effect depends on the `count` state
    }, 1000);
    return () => clearInterval(id);
  }, []); // ? Bug: `count` is not specified as a dependency

  return <h1>{count}</h1>;
}

但可以按以下方式进行重写:

function Counter() {
  const [count, setCount] = useState(0);
  let c = count;
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c++);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

反应文档说:

问题在于在setInterval回调中,count的值不会更改,因为我们创建了一个闭包,其值与运行效果回调时的count设置为0相同。每秒,此回调都会调用setCount(0 + 1),因此计数永远不会超过1。

但是解释没有意义。那么,为什么第一个代码不能正确更新计数,而第二个代码却可以正确计数呢? (也声明为let [count, setCount] = useState(0)然后使用setCount(count++)也可以)。

1 个答案:

答案 0 :(得分:2)

为什么看起来不起作用?

有一些提示可以帮助您了解正在发生的事情。

countconst ,因此它的范围永远不会改变。之所以令人困惑,是因为它在调用setCount时似乎正在更改,但是它永远不会更改,只是再次调用了该组件并创建了一个新的count变量。

在回调中使用count时,闭包捕获变量,并且count保持可用,即使组件函数执行完毕。再次,它与useEffect混淆,因为看起来回调是在每个渲染周期中创建的,捕获了最新的count值,但这不是正在发生的事情。

为清楚起见,让我们在每次创建变量时都添加一个后缀,以查看发生了什么情况。

在安装时

function Counter() {
  const [count_0, setCount_0] = useState(0);

  useEffect(
    // This is defined and will be called after the component is mounted.
    () => {
      const id_0 = setInterval(() => {
        setCount_0(count_0 + 1);
      }, 1000);
      return () => clearInterval(id_0);
    }, 
  []);

  return <h1>{count_0}</h1>;
}

一秒钟后

function Counter() {
  const [count_1, setCount_1] = useState(0);

  useEffect(
    // completely ignored by useEffect since it's a mount 
    // effect, not an update.
    () => {
      const id_0 = setInterval(() => {
        // setInterval still has the old callback in 
        // memory, so it's like it was still using
        // count_0 even though we've created new variables and callbacks.
        setCount_0(count_0 + 1);
      }, 1000);
      return () => clearInterval(id_0);
    }, 
  []);

  return <h1>{count_0}</h1>;
}

为什么它可以与let c一起使用?

let使得可以重新分配给c,这意味着当它被我们的useEffectsetInterval闭包捕获时,它仍然可以像使用它一样使用存在,但仍然是第一个定义的。

在安装时

function Counter() {
  const [count_0, setCount_0] = useState(0);

  let c_0 = count_0;

  // c_0 is captured once here
  useEffect(
    // Defined each render, only the first callback 
    // defined is kept and called once.
    () => {
      const id_0 = setInterval(
        // Defined once, called each second.
        () => setCount_0(c_0++), 
        1000
      );
      return () => clearInterval(id_0);
    }, 
    []
  );

  return <h1>{count_0}</h1>;
}

一秒钟后

function Counter() {
  const [count_1, setCount_1] = useState(0);

  let c_1 = count_1;
  // even if c_1 was used in the new callback passed 
  // to useEffect, the whole callback is ignored.
  useEffect(
    // Defined again, but ignored completely by useEffect.
    // In memory, this is the callback that useEffect has:
    () => {
      const id_0 = setInterval(
        // In memory, c_0 is still used and reassign a new value.
        () => setCount_0(c_0++),
        1000
      );
      return () => clearInterval(id_0);
    }, 
    []
  );

  return <h1>{count_1}</h1>;
}

使用钩子的最佳实践

由于很容易与所有回调和计时混淆,并避免任何意外的副作用,所以最好使用功能更新程序状态设置器参数。

// ❌ Avoid using the captured count.
setCount(count + 1)

// ✅ Use the latest state with the updater function.
setCount(currCount => currCount + 1)

在代码中:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // I chose a different name to make it clear that we're 
    // not using the `count` variable.
    const id = setInterval(() => setCount(currCount => currCount + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

还有很多事情要做,并且对语言的更多解释可以最好地准确地解释它是如何工作的以及为什么这样工作,尽管我一直专注于您的示例,以使其简单。