React useState 不更新值

React useState does not update value

我有点困惑为什么这个组件没有按预期工作:

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>;
}

React 文档说:

The problem is that inside the setInterval callback, the value of count does not change, because we’ve created a closure with the value of count set to 0 as it was when the effect callback ran. Every second, this callback then calls setCount(0 + 1), so the count never goes above 1.

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

为什么它看起来不起作用?

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

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>;
}

钩子的最佳实践

因为很容易混淆所有的回调和时间,并且为了避免任何意外side-effects,最好使用功能更新程序状态 setter 参数。

// ❌ 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>;
}

还有很多事情要做,并且需要更多的语言解释来最好地解释它是如何工作的以及为什么这样工作,尽管我一直专注于你的例子以保持简单。

  • More on closures.