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