我在玩React钩子(Hook),遇到了一个问题。
当我尝试使用事件侦听器处理的按钮来控制台记录日志时,它显示错误的状态。
代码沙箱: https://codesandbox.io/s/lrxw1wr97m
为什么显示错误状态?
在第一张卡中,
Button2
应该在控制台中显示2
卡。有任何想法吗?const { useState, useContext, useRef, useEffect } = React;
const CardsContext = React.createContext();
const CardsProvider = props => {
const [cards, setCards] = useState([]);
const addCard = () => {
const id = cards.length;
setCards([...cards, { id: id, json: {} }]);
};
const handleCardClick = id => console.log(cards);
const handleButtonClick = id => console.log(cards);
return (
<CardsContext.Provider
value={{ cards, addCard, handleCardClick, handleButtonClick }}
>
{props.children}
</CardsContext.Provider>
);
};
function App() {
const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
CardsContext
);
return (
<div className="App">
<button onClick={addCard}>Add card</button>
{cards.map((card, index) => (
<Card
key={card.id}
id={card.id}
handleCardClick={() => handleCardClick(card.id)}
handleButtonClick={() => handleButtonClick(card.id)}
/>
))}
</div>
);
}
function Card(props) {
const ref = useRef();
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
}, []);
return (
<div className="card">
Card {props.id}
<div>
<button onClick={props.handleButtonClick}>Button1</button>
<button ref={node => (ref.current = node)}>Button2</button>
</div>
</div>
);
}
ReactDOM.render(
<CardsProvider>
<App />
</CardsProvider>,
document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id='root'></div>
我使用React 16.7.0-alpha.0和Chrome 70.0.3538.110
顺便说一句,如果我使用Ñlass重写CardsProvider,问题就解决了。
CodeSandbox使用类:https://codesandbox.io/s/w2nn3mq9vl
最佳答案
对于使用useState
挂钩的功能组件,这是常见问题。同样的问题也适用于使用useState
状态的任何回调函数,例如 setTimeout
or setInterval
timer functions。
在CardsProvider
和Card
组件中,事件处理程序的处理方式有所不同。handleCardClick
功能组件中使用的handleButtonClick
和CardsProvider
在其范围内定义。每次运行时都有新函数,它们引用的是在定义它们时获得的cards
状态。每次渲染CardsProvider
组件时,事件处理程序都会重新注册。handleCardClick
功能组件中使用的Card
作为prop接收,并在组件安装时使用useEffect
注册一次。它在整个组件生命周期期内都具有相同的功能,并且是指在首次定义handleCardClick
函数时新鲜的过时状态。 handleButtonClick
作为prop接收,并在每个Card
渲染器上重新注册,每次都是新功能,并引用新鲜状态。
可变状态
解决此问题的常用方法是使用useRef
而不是useState
。引用基本上是一种配方,提供了一个可变对象,可以通过引用传递该对象:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
万一组件需要在状态更新时如
useState
所期望的那样重新呈现,则引用不适用。可以分别保持状态更新和可变状态,但是
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
return () => {
// unregister eventListener once
};
}, []);
这不适用于异步副作用,尤其是
async
函数。手动事件侦听器重新注册
另一种解决方案是每次都重新注册事件侦听器,因此回调总是从封闭范围获得新状态:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
内置事件处理
除非在
document
上注册了事件侦听器,否则window
或其他事件目标不在当前组件的范围之内,否则必须尽可能使用React自己的DOM事件处理,这消除了对useEffect
的需要:<button onClick={eventListener} />
在最后一种情况下,可以使用
useMemo
或useCallback
额外记住事件监听器,以防止在作为 Prop 传递时不必要的重新渲染:const eventListener = useCallback(() => {
console.log(state);
}, [state]);
答案的先前版本建议使用可变状态,该可变状态适用于React 16.7.0-alpha版本中的初始
useState
钩子(Hook)实现,但在最终的React 16.8实现中不起作用。 useState
当前仅支持不可变状态。