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



我想在组件卸载时将状态保存到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"));

代码沙盒

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

原因是闭包。闭包是函数对其作用域中变量的引用。当组件装载时,useEffect回调只运行一次,因此返回的回调引用了初始计数值0。

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

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

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

CodeSandbox链接

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的函数代码构造的"函数"稍微容易一些。

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

感谢@Xitang为ref直接分配给.current,这里不需要useEffect。含糖的

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

在你的情况下,你会做的是

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]);

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

您可以使用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使用函数updater

如果您执行以下操作:

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

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

我也一直遇到这个问题,refs似乎是唯一的解决方法,但这个hook/helper函数使访问最新的局部变量(通常是状态变量)变得简单明了。

useEffectUnscoped钩子实现:

var useEffectUnscoped = (callback, diffArray = [], deps) => {
var ref = useRef();
var depsRef = useRef();
depsRef.current = deps;
if (!ref.current || (diffArray.length && !diffArray.every((item, index) => item === ref.current[index]))) {
callback(depsRef);
ref.current = {diffArray};
}
};

如何使用useEffectUnscoped:

useEffectUnscoped((depsRef) => {
depsRef.current.myCallback();
}, [myDiffValue1, myDiffValue2], {
myCallback: () => console.log(myStateVar)
});

我们最经常将其用于keydown事件,因此我们为该特定用例编写了一个更干净的钩子,许多人可能会觉得它很有用。

useKeyDown钩子实现:

var useKeyDown = (handleKeyDown, options) => {
useEffectUnscoped((depsRef) => {
var handleKeyDown = (event) => depsRef.current.handleKeyDown(event);
document.addEventListener('keydown', handleKeyDown, options);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [], {handleKeyDown});
};

如何使用useKeyDown:

useKeyDown((event) => {
if (myStateVar) {
console.log(event.key);
}
}, {capture: true}); //optional secondary argument

相关内容

  • 没有找到相关文章

最新更新