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