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

时间:2018-12-05 13:40:43

标签: javascript reactjs react-hooks

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

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

那是为什么?如何不使用类就可以保存状态?

这是一个虚拟的例子。当您按关闭时,结果始终为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

6 个答案:

答案 0 :(得分:7)

  

我试图对useEffect挂钩执行相同的操作,但是在useEffect的返回函数中状态似乎不正确。

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

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

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

如果您坚持只在卸载时写入localStorage,则可以使用一个丑陋的hack /解决方案-参考。基本上,您将创建一个在组件的整个生命周期中都存在的变量,您可以从组件中的任何位置进行引用。但是,您必须手动将状态与该值同步,这非常麻烦。引用不会给您上述关闭问题,因为引用是具有current字段的对象,并且多次调用useRef将返回相同的对象。只要您更改.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>

答案 1 :(得分:4)

您的useEffect回调函数将显示初始计数,这是因为useEffect在初始渲染上仅运行一次,并且该回调使用初始渲染期间存在的count值存储为零。

相反,您要做的是

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

在react文档中,您会找到为什么这样定义的原因

  

React何时确切地清除效果?当组件卸载时,React进行清除。但是,正如我们先前所了解的,   效果针对每个渲染运行,而不仅仅是一次。这就是为什么React也   在运行效果之前清除前一个渲染的效果   下次。

阅读Why Effects Run on Each Update上的react文档

它确实可以在每个渲染上运行,要对其进行优化,可以使其在count更改下运行。但这是useEffect的当前建议行为,正如文档中也提到的那样,在实际实现中可能会发生变化。

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

答案 2 :(得分:2)

另一个答案是正确的。为何不将[count]传递给useEffect,并在count发生更改时保存到localStorage?这样调用localStorage并不会造成实际的性能损失。

答案 3 :(得分:1)

这将起作用-使用React的useRef-但效果不佳:

function Content(props) {
  const [count, setCount] = useState(0);
  const countRef = useRef();
  // this effect fires every time count is changed
  useEffect(() => () => {
    countRef.current = count;
  },[count]);

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

请注意,对于useEffect来说,稍微更可以忍受的(我认为!)“返回函数的函数”代码构造。

问题在于useEffect会在合成时复制道具和状态,因此永远不要重新评估它们-这无助于此用例,但实际上不是useEffects的目的。

答案 4 :(得分:0)

您可以使用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>
  );
}

答案 5 :(得分:0)

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

  • 具有useEffect且具有超过计数的依存关系,它将在该依存关系的每次更改时刷新它。
  • 在setCount上使用function updater

如果您执行以下操作:

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

我正在添加此解决方案,以防万一有人来这里寻求将旧值更新为useEffect而不进行重新加载的问题。