Javascript 错误的 React 使用事件侦听器挂钩行为
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/53845595/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Wrong React hooks behaviour with event listener
提问by Mark Lano
I'm playing around with React hooks and faced a problem. It shows the wrong state when I'm trying to console log it using button handled by event listener.
我在玩 React 钩子并遇到了一个问题。当我尝试使用事件侦听器处理的按钮进行控制台记录时,它显示错误的状态。
CodeSandbox:https://codesandbox.io/s/lrxw1wr97m
CodeSandbox:https ://codesandbox.io/s/lrxw1wr97m
- Click on 'Add card' button 2 times
- In first card, click on Button1 and see in console that there are 2 cards in state (correct behaviour)
- In first card, click on Button2 (handled by event listener) and see in console that there are only 1 card in state (wrong behaviour)
- 单击“添加卡”按钮 2 次
- 在第一张卡片中,单击 Button1 并在控制台中看到有 2 张卡片处于状态(正确的行为)
- 在第一张卡片中,单击 Button2(由事件侦听器处理)并在控制台中看到只有 1 张卡片处于状态(错误行为)
Why does it show the wrong state? In first card, Button2 should display 2 cards in console. Any ideas?
为什么它显示错误的状态?在第一张卡片中,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")
);
I use 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 сlass, the problem is gone. CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl
顺便说一句,如果我使用 сlass 重写 CardsProvider,问题就消失了。CodeSandbox 使用类:https://codesandbox.io/s/w2nn3mq9vl
回答by Estus Flask
This is common problem for functional components that use useStatehook. The same concerns are applicable to any callback functions where useStatestate is used, e.g. setTimeoutor setIntervaltimer functions.
这是使用useStatehook 的功能组件的常见问题。同样的问题适用于任何useState使用状态的回调函数,例如setTimeout或setInterval定时器函数。
Event handlers are treated differently in CardsProviderand Cardcomponents.
事件处理程序在CardsProvider和Card组件中的处理方式不同。
handleCardClickand handleButtonClickused in CardsProviderfunctional component are defined in its scope. There are new functions each time it runs, they refer to cardsstate that was obtained at the moment when they were defined. Event handlers are re-registered each time CardsProvidercomponent is rendered.
handleCardClick和handleButtonClick用于CardsProvider功能组件的定义在其范围内。每次运行时都会有新的函数,它们指的cards是在定义它们时获得的状态。每次CardsProvider呈现组件时都会重新注册事件处理程序。
handleCardClickused in Cardfunctional component is received as a prop and registered once on component mount with useEffect. It's the same function during entire component lifespan and refers to stale state that was fresh at the time when handleCardClickfunction was defined the first time. handleButtonClickis received as a prop and re-registered on each Cardrender, it's a new function each time and refers to fresh state.
handleCardClick在Card功能组件中使用作为 prop 接收并在组件挂载时注册一次useEffect。它在整个组件生命周期中都是相同的函数,并且指的handleCardClick是第一次定义函数时新鲜的陈旧状态。handleButtonClick作为道具接收并在每次Card渲染时重新注册,它每次都是一个新函数,并且指的是新鲜状态。
Mutable state
可变状态
A common approach that addresses this problem is to use useRefinstead of useState. A ref is a basically a recipe that provides a mutable object that can be passed by reference:
解决此问题的常用方法是使用useRef代替useState。ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
In case a component should be re-rendered on state update like it's expected from useState, refs aren't applicable.
如果组件应该像预期的那样在状态更新时重新呈现useState,则引用不适用。
It's possible to keep state updates and mutable state separately but forceUpdateis considered an antipattern 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();
}
State updater function
状态更新器功能
One solution is to use state updater function that receives fresh state instead of stale state from enclosing scope:
一种解决方案是使用状态更新器函数,该函数从封闭范围接收新鲜状态而不是陈旧状态:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
In case a state is needed for synchronous side effect 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
}, []);
This doesn't work well with asynchronous side effects, notably asyncfunctions.
这不适用于异步副作用,尤其是async函数。
Manual event listener re-registration
手动事件监听器重新注册
Another solution is to re-register event listener every time, so a callback always gets fresh state from enclosing scope:
另一种解决方案是每次都重新注册事件侦听器,因此回调始终从封闭范围获得新状态:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
}, [state]);
Built-in event handling
内置事件处理
Unless event listener is registered on document, windowor other event targets are outside of the scope of 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 event listener can be additionally memoized with useMemoor useCallbackto prevent unnecessary re-renders when it's passed as a prop:
在最后一种情况下,当事件侦听器作为道具传递时,可以使用useMemo或useCallback防止不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
Previous edition of the answer suggested to use mutable state that is applicable to initial useStatehook implementation in React 16.7.0-alpha version but isn't workable in final React 16.8 implementation. useStatecurrently supports only immutable state.
上一版的答案建议使用可变状态,该状态适用useState于 React 16.7.0-alpha 版本中的初始钩子实现,但在最终的 React 16.8 实现中不可用。useState目前仅支持不可变状态。
回答by Moses Gitau
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 refas a reference to the state value.
您现在可以将ref用作状态值的引用。
回答by ChetPrickles
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}));
}
//...
}
回答by Nelles
Short answer for me
对我的简短回答
this WILL NOTnot trigger rerender
这将不会不触发重新呈现
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This WILLtrigger render -> putting myvar in []
这将触发渲染 -> 将myvar 放入 []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
回答by Paduado
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)
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
Just add props.handleCardClickto the array of dependencies and it will work correctly.
只需添加props.handleCardClick到依赖项数组,它就会正常工作。

