对于使用useState
钩子的功能组件,这是常见的问题。相同的考虑适用于useState
使用状态的任何回调函数,例如setTimeout
或setInterval
定时器函数。
事件处理程序在CardsProvider
和Card
组件中被区别对待。
handleCardClick
并且handleButtonClick
在CardsProvider
功能组件中使用的组件在其范围内定义。每次运行时都有新功能,它们引用cards
在定义它们时获得的状态。每次CardsProvider
呈现组件时都会重新注册事件处理程序。
handleCardClick
用于Card
功能组件的组件会作为道具接收并在组件支架上一次注册useEffect
。它在整个组件寿命期间都具有相同的功能,并且是指在首次handleCardClick
定义功能时新鲜的陈旧状态。handleButtonClick
作为道具接收并在每个Card
渲染器上重新注册,每次都是新功能,并引用新鲜状态。
可变状态
解决此问题的常用方法是使用useRef
而不是useState
。引用基本上是一种配方,提供了一个可变对象,可以通过引用传递该对象:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
万一组件应该在状态更新时重新渲染,如预期的那样useState
,则refs不适用。
可以分别保持状态更新和可变状态,但是forceUpdate
在类和函数组件中都被视为反模式(列出仅供参考):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
状态更新器功能
一种解决方案是使用状态更新程序功能,该功能从封闭的范围接收新鲜状态而不是陈旧状态:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
如果需要一个状态来实现同步副作用,例如console.log
,一种解决方法是返回相同状态以防止更新。
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
}, []);
这不适用于异步副作用,尤其是async
函数。
手动事件侦听器重新注册
另一种解决方案是每次都重新注册事件侦听器,因此回调总是从封闭范围获得新状态:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
}, [state]);
内置事件处理
除非在上注册了事件侦听器document
,window
或者其他事件目标不在当前组件的范围之内,否则在可能的情况下必须使用React自己的DOM事件处理,这样就不需要useEffect
:
<button onClick={eventListener} />
在最后一种情况下,事件侦听器可以作为道具传递时,还可以通过useMemo
或useCallback
来记住,以防止不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
答案的先前版本建议使用可变状态,该可变状态适用useState
于React16.7.0-alpha版本中的初始钩子实现,但不适用于最终的React16.8实现。useState
当前仅支持不可变状态。