[英]Wrong React hooks behaviour with event listener
I'm playing around with React Hooks and am facing a problem.我正在玩React Hooks ,但遇到了问题。 It shows the wrong state when I'm trying to console log it using a button handled by event listener.
当我尝试使用事件侦听器处理的按钮来控制台记录它时,它显示错误的 state。
CodeSandbox: https://codesandbox.io/s/lrxw1wr97m代码沙盒: https://codesandbox.io/s/lrxw1wr97m
Why does it show the wrong state?为什么会显示错误的state?
In first card, Button2
should display 2
cards in the console.在第一张卡片中,
Button2
应该在控制台中显示2
张卡片。 Any ideas?有任何想法吗?
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>
I am using React 16.7.0-alpha.0 and Chrome 70.0.3538.110我正在使用 React 16.7.0-alpha.0 和 Chrome 70.0.3538.110
BTW, if I rewrite the CardsProvider using a сlass, the problem is gone.顺便说一句,如果我使用类重写 CardsProvider,问题就消失了。 CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl
CodeSandbox使用class: https://codesandbox.io/s/w2nn3mq9vl
This is a common problem for functional components that use the useState
hook.这是使用
useState
挂钩的功能组件的常见问题。 The same concerns are applicable to any callback functions where useState
state is used, eg setTimeout
or setInterval
timer functions .同样的问题适用于任何使用
useState
状态的回调函数,例如setTimeout
或setInterval
计时器函数。
Event handlers are treated differently in CardsProvider
and Card
components.事件处理程序在
CardsProvider
和Card
组件中的处理方式不同。
handleCardClick
and handleButtonClick
used in the CardsProvider
functional component are defined in its scope. CardsProvider
功能组件中使用的handleCardClick
和handleButtonClick
是在其范围内定义的。 There are new functions each time it runs, they refer to cards
state that was obtained at the moment when they were defined.每次运行时都会有新的函数,它们指的是在定义它们时获得的
cards
状态。 Event handlers are re-registered each time the CardsProvider
component is rendered.每次呈现
CardsProvider
组件时都会重新注册事件处理程序。
handleCardClick
used in the Card
functional component is received as a prop and registered once on component mount with useEffect
.在
Card
功能组件中使用的handleCardClick
作为道具被接收,并在组件挂载时使用useEffect
注册一次。 It's the same function during the entire component lifespan and refers to stale state that was fresh at the time when the handleCardClick
function was defined the first time.它在整个组件生命周期中都是相同的函数,并且指的是在第一次定义
handleCardClick
函数时新鲜的陈旧状态。 handleButtonClick
is received as a prop and re-registered on each Card
render, it's a new function each time and refers to fresh state. handleButtonClick
作为 prop 接收并在每次Card
渲染时重新注册,每次都是一个新函数,并引用新状态。
A common approach that addresses this problem is to use useRef
instead of useState
.解决此问题的常用方法是使用
useRef
而不是useState
。 A ref is basically a recipe that provides a mutable object that can be passed by reference: ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
In this case a component should be re-rendered on a state update like it's expected from useState
, refs aren't applicable.在这种情况下,组件应该在状态更新时重新渲染,就像
useState
所期望的那样,refs 不适用。
It's possible to keep state updates and mutable state separately but forceUpdate
is considered an anti-pattern in both class and function components (listed for reference only):可以分别保持状态更新和可变状态,但
forceUpdate
在类和函数组件中都被视为反模式(列出仅供参考):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
One solution is to use a state updater function that receives fresh state instead of stale state from the enclosing scope:一种解决方案是使用状态更新器函数,该函数从封闭范围接收新鲜状态而不是陈旧状态:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
In this case a state is needed for synchronous side effects like console.log
, a workaround is to return the same state to prevent an update.在这种情况下,像
console.log
这样的同步副作用需要一个状态,解决方法是返回相同的状态以防止更新。
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
This doesn't work well with asynchronous side effects, notably async
functions.这不适用于异步副作用,尤其是
async
函数。
Another solution is to re-register the event listener every time, so a callback always gets fresh state from the enclosing scope:另一种解决方案是每次都重新注册事件侦听器,因此回调始终从封闭范围获取新状态:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
Unless the event listener is registered on document
, window
or other event targets that are outside of the scope of the current component, React's own DOM event handling has to be used where possible, this eliminates the need for useEffect
:除非事件侦听器注册在
document
、 window
或其他超出当前组件范围的事件目标上,否则必须尽可能使用 React 自己的 DOM 事件处理,这消除了对useEffect
的需要:
<button onClick={eventListener} />
In the last case the event listener can be additionally memoized with useMemo
or useCallback
to prevent unnecessary re-renders when it's passed as a prop:在最后一种情况下,可以使用
useMemo
或useCallback
对事件监听器进行额外的记忆,以防止在作为道具传递时不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
useState
hook implementation in React 16.7.0-alpha version but isn't workable in final React 16.8 implementation.useState
挂钩实现,但在最终 React 16.8 实现中不适用。 useState
currently supports only immutable state.* useState
目前仅支持不可变状态。*A much cleaner way to work around this is to create a hook I call useStateRef
解决此问题的一种更简洁的方法是创建一个我称为
useStateRef
的钩子
function useStateRef(initialValue) {
const [value, setValue] = useState(initialValue);
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
You can now use the ref
as a reference to the state value.您现在可以使用
ref
作为对状态值的引用。
Short answer for me was that useState has a simple solution for this:对我来说简短的回答是 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}));
}
//...
}
Short answer for me对我的简短回答
this WILL NOT not trigger re-render ever time myvar changes.
这不会在 myvar 更改时触发重新渲染。
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This WILL trigger render -> putting myvar in []
这将触发渲染 -> 将myvar放入 []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
Check the console and you'll get the answer:检查控制台,你会得到答案:
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
Just add props.handleCardClick
to the array of dependencies and it will work correctly.只需将
props.handleCardClick
添加到依赖项数组中,它就会正常工作。
This way your callback will have updated state values always ;)这样,您的回调将始终更新状态值;)
// registers an event listener to component parent
React.useEffect(() => {
const parentNode = elementRef.current.parentNode
parentNode.addEventListener('mouseleave', handleAutoClose)
return () => {
parentNode.removeEventListener('mouseleave', handleAutoClose)
}
}, [handleAutoClose])
To build off of Moses Gitau's great answer , if you are developing in Typescript, to resolve type errors make the hook function generic:为了建立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];
}
Starting from the answer of @Moses Gitau, I'm using a sligthly different one that doesn't give access to a "delayed" version of the value (which is an issue for me) and is a bit more minimalist:从@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;
This is what I'm using这就是我正在使用的
Example of usage:使用示例:
const [getValue , setValue] = useStateRef(0);
const listener = (event) => {
setValue(getValue() + 1);
};
useEffect(() => {
window.addEventListener('keyup', listener);
return () => {
window.removeEventListener('keyup', listener);
};
}, []);
Edit: It now gives getValue and not the reference itself.编辑:它现在给出 getValue 而不是引用本身。 I find it better to keep things more encapsulated in that case.
我发现在那种情况下最好将事情封装起来。
after changing the following line in the index.js
file the button2
works well:更改
index.js
文件中的以下行后, button2
运行良好:
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
- }, []);
+ });
you should not use []
as 2nd argument useEffect
unless you want it to run once.你不应该使用
[]
作为第二个参数useEffect
除非你希望它运行一次。
more details: https://reactjs.org/docs/hooks-effect.html更多细节: https ://reactjs.org/docs/hooks-effect.html
我遇到了类似的问题,我的事件函数从上下文中获取了过时的值,这篇文章帮助了我。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.