在 React useEffect 挂钩中引用过时的状态

Referencing outdated state in React useEffect hook

我想在卸载组件时将状态保存到 localStorage。 这曾经在 componentWillUnmount.

中工作

我尝试用 useEffect 钩子做同样的事情,但在 useEffect 的 return 函数中似乎状态不正确。

这是为什么?如何在不使用 class 的情况下保存状态?

这是一个虚拟示例。当您按下关闭时,结果始终为 0。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Why is count in console always 0 ?</div>}
    </div>
  );
}

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

  useEffect(() => {
    // TODO: Load state from localStorage on mount

    return () => {
      console.log("count:", count);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector("#app"));

CodeSandbox

您的 useEffect 回调函数显示了初始计数,这是因为您的 useEffect 在初始渲染中仅 运行 一次,并且回调与初始渲染期间存在的计数值一起存储为零。

在你的情况下你会做的是

 useEffect(() => {
    // TODO: Load state from localStorage on mount
    return () => {
      console.log("count:", count);
    };
  });

在 React 文档中,您会找到这样定义的原因

When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.

阅读 Why Effects Run on Each Update

上的 React 文档

它在每个渲染上执行 运行,要优化它,您可以在 count 更改时将其设为 运行。但这是文档中也提到的 useEffect 当前提议的行为,可能会在实际实施中发生变化。

 useEffect(() => {
    // TODO: Load state from localStorage on mount
    return () => {
      console.log("count:", count);
    };
  }, [count]);

另一个答案是正确的。为什么不将 [count] 传递给您的 useEffect,以便在 count 更改时保存到 localStorage?像这样调用 localStorage 并没有真正的性能损失。

I tried to do the same with the useEffect hook, but it seems state is not correct in the return function of useEffect.

这是因为关闭。闭包是函数对其范围内变量的引用。当组件安装时,您的 useEffect 回调仅 运行 一次,因此 return 回调引用初始计数值 0。

这里给出的答案是我推荐的。我会推荐@Jed Richard 关于将 [count] 传递给 useEffect 的回答,这具有仅在计数更改时才写入 localStorage 的效果。这比在每次更新时完全不传递任何内容的方法要好。除非您非常频繁地更改计数(每隔几毫秒),否则您不会看到性能问题,并且只要 count 发生变化,就可以写入 localStorage

useEffect(() => { ... }, [count]);

如果您坚持只在卸载时写入 localStorage,那么您可以使用丑陋的 hack/solution - refs。基本上,您将创建一个变量,该变量在组件的整个生命周期中都存在,您可以从其中的任何地方引用它。但是,您必须手动将您的状态与该值同步,这非常麻烦。 Refs 不会给你上面提到的关闭问题,因为 refs 是一个带有 current 字段的对象,多次调用 useRef 将 return 你是同一个对象。只要你改变 .current 值,你的 useEffect 总是(只能)读取最新的值。

CodeSandbox link

const {useState, useEffect, useRef} = React;

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Count in console is not always 0</div>}
    </div>
  );
}

function Content(props) {
  const value = useRef(0);
  const [count, setCount] = useState(value.current);

  useEffect(() => {
    return () => {
      console.log('count:', value.current);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button
        onClick={() => {
          value.current -= 1;
          setCount(value.current);
        }}
      >
        -1
      </button>
      <button
        onClick={() => {
          value.current += 1;
          setCount(value.current);
        }}
      >
        +1
      </button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

这会起作用 - 使用 React 的 useRef - 但它并不漂亮:

function Content(props) {
  const [count, setCount] = useState(0);
  const countRef = useRef();

  // set/update countRef just like a regular variable
  countRef.current = count;

  // this effect fires as per a true componentWillUnmount
  useEffect(() => () => {
    console.log("count:", countRef.current);
  }, []);
}

注意 useEffect 的代码构造稍微更容易接受(在我看来!)'function that returns a function'。

问题是 useEffect 在组合时复制道具和状态,因此从不重新评估它们 - 这对这个用例没有帮助,但它不是 useEffects 的真正用途。

感谢@Xitang 直接赋值给.current作为ref,这里不需要useEffect。甜蜜!

您可以使用 useEffect 来更新参考,而不是像在已接受的答案中那样手动跟踪您的状态变化。

function Content(props) {
  const [count, setCount] = useState(0);
  const currentCountRef = useRef(count);

  // update the ref if the counter changes
  useEffect(() => {
    currentCountRef.current = count;
  }, [count]);

  // use the ref on unmount
  useEffect(
    () => () => {
      console.log("count:", currentCountRef.current);
    },
    []
  );

  return (
    <div>
      <p>Day: {count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

useEffect 第一次运行时发生的情况是,它对您传递的状态值创建了一个闭包;那么如果你想得到实际的而不是第一个..你有两个选择:

  • 使 useEffect 具有超过计数的依赖性,这将在该依赖性的每次更改时刷新它。
  • 对 setCount 使用 function updater

如果你这样做:

useEffect(() => {
  return () => {
    setCount((current)=>{ console.log('count:', current); return current; });
  };
}, []);

我添加这个解决方案是为了防止有人来这里寻找一个问题,试图在不重新加载的情况下根据旧值将更新更新到 useEffect 中。