[英]Wrong React hooks behaviour with event listener
我正在玩React Hooks ,但遇到了问题。 当我尝试使用事件侦听器处理的按钮来控制台记录它时,它显示错误的 state。
代码沙盒: https://codesandbox.io/s/lrxw1wr97m
为什么会显示错误的state?
在第一张卡片中, 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
顺便说一句,如果我使用类重写 CardsProvider,问题就消失了。 CodeSandbox使用class: https://codesandbox.io/s/w2nn3mq9vl
这是使用useState
挂钩的功能组件的常见问题。 同样的问题适用于任何使用useState
状态的回调函数,例如setTimeout
或setInterval
计时器函数。
事件处理程序在CardsProvider
和Card
组件中的处理方式不同。
CardsProvider
功能组件中使用的handleCardClick
和handleButtonClick
是在其范围内定义的。 每次运行时都会有新的函数,它们指的是在定义它们时获得的cards
状态。 每次呈现CardsProvider
组件时都会重新注册事件处理程序。
在Card
功能组件中使用的handleCardClick
作为道具被接收,并在组件挂载时使用useEffect
注册一次。 它在整个组件生命周期中都是相同的函数,并且指的是在第一次定义handleCardClick
函数时新鲜的陈旧状态。 handleButtonClick
作为 prop 接收并在每次Card
渲染时重新注册,每次都是一个新函数,并引用新状态。
解决此问题的常用方法是使用useRef
而不是useState
。 ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:
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
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
对事件监听器进行额外的记忆,以防止在作为道具传递时不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
useState
挂钩实现,但在最终 React 16.8 实现中不适用。 useState
目前仅支持不可变状态。*解决此问题的一种更简洁的方法是创建一个我称为useStateRef
的钩子
function useStateRef(initialValue) {
const [value, setValue] = useState(initialValue);
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
您现在可以使用ref
作为对状态值的引用。
对我来说简短的回答是 useState 对此有一个简单的解决方案:
function Example() {
const [state, setState] = useState(initialState);
function update(updates) {
// this might be stale
setState({...state, ...updates});
// but you can pass setState a function instead
setState(currentState => ({...currentState, ...updates}));
}
//...
}
对我的简短回答
这不会在 myvar 更改时触发重新渲染。
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
这将触发渲染 -> 将myvar放入 []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
检查控制台,你会得到答案:
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
只需将props.handleCardClick
添加到依赖项数组中,它就会正常工作。
这样,您的回调将始终更新状态值;)
// registers an event listener to component parent
React.useEffect(() => {
const parentNode = elementRef.current.parentNode
parentNode.addEventListener('mouseleave', handleAutoClose)
return () => {
parentNode.removeEventListener('mouseleave', handleAutoClose)
}
}, [handleAutoClose])
为了建立Moses Gitau 的出色答案,如果您正在使用 Typescript 进行开发,为了解决类型错误,请使用通用钩子函数:
function useStateRef<T>(initialValue: T | (() => T)):
[T, React.Dispatch<React.SetStateAction<T>>, React.MutableRefObject<T>] {
const [value, setValue] = React.useState(initialValue);
const ref = React.useRef(value);
React.useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
从@Moses Gitau 的回答开始,我使用的是一个略有不同的版本,它不能访问值的“延迟”版本(这对我来说是个问题)并且更加简约:
import { useState, useRef } from 'react';
function useStateRef(initialValue) {
const [, setValueState] = useState(initialValue);
const ref = useRef(initialValue);
const setValue = (val) => {
ref.current = val;
setValueState(val); // to trigger the refresh
};
const getValue = (val) => {
return ref.current;
};
return [getValue , setValue];
}
export default useStateRef;
这就是我正在使用的
使用示例:
const [getValue , setValue] = useStateRef(0);
const listener = (event) => {
setValue(getValue() + 1);
};
useEffect(() => {
window.addEventListener('keyup', listener);
return () => {
window.removeEventListener('keyup', listener);
};
}, []);
编辑:它现在给出 getValue 而不是引用本身。 我发现在那种情况下最好将事情封装起来。
更改index.js
文件中的以下行后, button2
运行良好:
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
- }, []);
+ });
你不应该使用[]
作为第二个参数useEffect
除非你希望它运行一次。
更多细节: https ://reactjs.org/docs/hooks-effect.html
我遇到了类似的问题,我的事件函数从上下文中获取了过时的值,这篇文章帮助了我。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.