Javascript 如何使用 useEffect 钩子注册事件?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/55565444/
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
How to register event with useEffect hooks?
提问by Isaac
I am following a Udemy course on how to register events with hooks, the instructor gave the below code:
我正在学习关于如何使用钩子注册事件的 Udemy 课程,讲师给出了以下代码:
const [userText, setUserText] = useState('');
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
});
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
Now it works great but I'm not convinced that this is the right way. The reason is, if I understand correctly, on each and every re-render, events will keep registering and deregistering every time and I simply don't think it is the right way to go about it.
现在效果很好,但我不相信这是正确的方法。原因是,如果我理解正确,在每次重新渲染时,事件每次都会继续注册和取消注册,我只是认为这不是正确的方法。
So I made a slight modification to the useEffecthooks to below
所以我useEffect对下面的钩子做了一点修改
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
By having an empty array as the second argument, letting the component to only run the effect once, imitating componentDidMount. And when I try out the result, it's weird that on every key I type, instead of appending, it's overwritten instead.
通过将一个空数组作为第二个参数,让组件只运行一次效果,模仿componentDidMount. 当我尝试结果时,奇怪的是在我键入的每个键上,它不是附加,而是被覆盖。
I was expecting setUserText(${userText}${key});to have new typed key append to current state and set as a new state but instead, it's forgetting the old state and rewriting with the new state.
我期待setUserText( ${userText}${key}); 将新类型的键附加到当前状态并设置为新状态,但相反,它忘记了旧状态并用新状态重写。
Was it really the correct way that we should register and deregister event on every re-render?
我们应该在每次重新渲染时注册和取消注册事件真的是正确的方法吗?
回答by Shubham Khatri
The best way to go about such scenarios is to see what you are doing in the event handler. If you are simply setting state using previous state, its best to use the callback pattern and register the event listeners only on initial mount. If you do not use the callback pattern(https://reactjs.org/docs/hooks-reference.html#usecallback) the listeners reference along with its lexical scope is being used by the event listener but a new function is created with updated closure on new render and hence in the handler you will not be able to the updated state
处理此类场景的最佳方法是查看您在事件处理程序中执行的操作。如果您只是使用以前的状态设置状态,最好使用回调模式并仅在初始安装时注册事件侦听器。如果您不使用callback pattern(https://reactjs.org/docs/hooks-reference.html#usecallback),则事件侦听器正在使用侦听器引用及其词法范围,但会创建一个新函数,并在其上更新闭包新渲染,因此在处理程序中您将无法更新状态
const [userText, setUserText] = useState('');
const handleUserKeyPress = useCallback(event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prevUserText => `${prevUserText}${key}`);
}
}, []);
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, [handleUserKeyPress]);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
回答by Spark.Bao
new answer:
新答案:
useEffect(() => {
function handlekeydownEvent(event) {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prevUserText => `${prevUserText}${key}`);
}
}
document.addEventListener('keyup', handlekeydownEvent)
return () => {
document.removeEventListener('keyup', handlekeydownEvent)
}
}, [])
when using setUserText, pass the function as the argument instead of the object, the prevUserTextwill be always the newest state.
使用时setUserText,将函数作为参数而不是对象传递,prevUserText将始终是最新状态。
old answer:
旧答案:
try this, it works same as your original code:
试试这个,它的工作原理与您的原始代码相同:
useEffect(() => {
function handlekeydownEvent(event) {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
}
document.addEventListener('keyup', handlekeydownEvent)
return () => {
document.removeEventListener('keyup', handlekeydownEvent)
}
}, [userText])
because in your useEffect()method, it depends on the userTextvariable but you don't put it inside the second argument, else the userTextwill always be bound to the initial value ''with argument [].
因为在您的useEffect()方法中,它取决于userText变量,但您不要将它放在第二个参数中,否则userText将始终''使用参数绑定到初始值[]。
you don't need to do like this, just want to let you know why your second solution doesn't work.
您不需要这样做,只是想让您知道为什么您的第二个解决方案不起作用。
回答by codejockie
For your use case, useEffectneeds a dependency array to track changes and based on the dependency it can determine whether to re-render or not. It is always advised to pass a dependency array to useEffect. Kindly see the code below:
对于您的用例,useEffect需要一个依赖项数组来跟踪更改,并根据依赖项确定是否重新渲染。始终建议将依赖项数组传递给useEffect. 请看下面的代码:
I have introduced useCallbackhook.
我已经介绍了useCallback钩子。
const { useCallback, useState, useEffect } = React;
const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prevUserText => `${prevUserText}${key}`);
}
}, []);
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [handleUserKeyPress]);
return (
<div>
<blockquote>{userText}</blockquote>
</div>
);
回答by ford04
Issue
问题
[...] on each and every re-render, events will keep registering and deregistering every time and I simply don't think it is the right way to go about it.
[...] 在每次重新渲染时,事件每次都会继续注册和取消注册,我只是认为这不是正确的方法。
You are right. It doesn't make sense to restart event handling inside useEffecton everyrender.
你是对的。useEffect在每次渲染时重新启动内部事件处理是没有意义的。
[...] empty array as the second argument, letting the component to only run the effect once [...] it's weird that on every key I type, instead of appending, it's overwritten instead.
[...] 空数组作为第二个参数,让组件只运行一次效果 [...] 奇怪的是,在我输入的每个键上,不是追加而是覆盖。
This is an issue with stale closure values.
这是陈旧的闭包值的问题。
Reason: Used functions inside useEffectshould be part of the dependencies. You set nothing as dependency ([]), but still call handleUserKeyPress, which itself reads userTextstate.
原因:内部使用的函数useEffect应该是依赖项的一部分。您没有设置任何依赖项 ( []),但仍然调用handleUserKeyPress,它本身读取userText状态。
Solution
解决方案
There are some alternatives depending on your use case.
根据您的用例,有一些替代方案。
1. State updater function
1.状态更新器功能
setUserText(prev => `${prev}${key}`);
? least invasive approach
? only access to own previous state, not other states
? 侵入性最小的方法
? 只能访问自己以前的状态,不能访问其他状态
const App = () => {
const [userText, setUserText] = useState("");
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prev => `${prev}${key}`); // use updater function here
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []); // still no dependencies
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
2. useReducer- "cheat mode"
2. useReducer- “作弊模式”
We can switch to useReducerand have access to current state/props - with similar API to useState.
我们可以切换到useReducer并访问当前状态/道具 - 使用与useState.
const [userText, handleUserKeyPress] = useReducer((state, event) => {
const { key, keyCode } = event;
// isUpperCase is always the most recent state (no stale closure value)
return `${state}${isUpperCase ? key.toUpperCase() : key}`;
}, "");
const App = () => {
const [isUpperCase, setUpperCase] = useState(false);
const [userText, handleUserKeyPress] = useReducer((state, event) => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
// isUpperCase is always the most recent state (no stale closure)
return `${state}${isUpperCase ? key.toUpperCase() : key}`;
}
}, "");
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
<button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
{isUpperCase ? "Disable" : "Enable"} Upper Case
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
useStateuseState更新函数
const [userText, setUserText] = useReducer((state, action) =>
typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);
const App = () => {
const [isUpperCase, setUpperCase] = useState(false);
const [userText, setUserText] = useReducer(
(state, action) =>
typeof action === "function" ? action(state, isUpperCase) : action,
""
);
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(
(prevState, isUpper) =>
`${prevState}${isUpper ? key.toUpperCase() : key}`
);
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
<button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
{isUpperCase ? "Disable" : "Enable"} Upper Case
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
? no need to manage dependencies
? access multiple states and props
? same API as useState
? extendable to more complex cases/reducers
? slightly less performance due to inline reducer (kinda neglectable)
? slightly increased complexity of reducer
? 不需要管理依赖
?访问多个状态和道具
?与useState
?相同的 API 可扩展到更复杂的案例/减速器
?由于内联减速器(有点可忽略),性能略有下降
?稍微增加了减速器的复杂性
3. handleUserKeyPressinside useEffect/ useCallback
3.handleUserKeyPress内部useEffect/useCallback
If you can, move handleUserKeyPressinside useEffect. Then, exhaustive deps ESLint rule will tell you, what exact canonical dependencies are missing.
如果可以的话,将handleUserKeyPress里面useEffect。然后,详尽的 deps ESLint 规则会告诉您,缺少哪些确切的规范依赖项。
const App =() => {
const [userText, setUserText] = useState("");
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [userText]);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Alternative: useCallbackis an equivalent approach with a bit more indirection of dependencies.
替代:useCallback是一种具有更多间接依赖关系的等效方法。
const App = () => {
const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(
event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
},
[userText]
);
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [handleUserKeyPress]);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
? all-purpose pragmatic solution
? minimal invasive
? manual dependencies' management
? useCallbackmakes function definition more verbose/cluttered
? 万能的务实解决方案
? 微创
? 手动依赖管理
? useCallback使函数定义更加冗长/混乱
4. Store callback in mutable ref / useRef
4. 将回调存储在可变 ref / useRef
const cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; }); // update after each render
useEffect(() => {
const cb = e => cbRef.current(e); // then use most recent cb value
window.addEventListener("keydown", cb);
return () => { window.removeEventListener("keydown", cb) };
}, []);
const App = () => {
const [userText, setUserText] = useState("");
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
const cbRef = useRef(handleUserKeyPress);
useEffect(() => {
cbRef.current = handleUserKeyPress;
});
useEffect(() => {
const cb = e => cbRef.current(e);
window.addEventListener("keydown", cb);
return () => {
window.removeEventListener("keydown", cb);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
? for callbacks/event handlers not used for re-render data flow
? only recommended as last option by React docs
? more imperative approach
? 对于不用于重新渲染数据流的回调/事件处理程序
?React docs 只推荐作为最后一个选项
?更必要的方法
回答by maazadeeb
You'll need a way to keep track of the previous state. useStatehelps you keep track of the current state only. From the docs, there is a way to access the old state, by using another hook.
您需要一种方法来跟踪以前的状态。useState仅帮助您跟踪当前状态。从docs,有一种方法可以通过使用另一个钩子来访问旧状态。
const prevRef = useRef();
useEffect(() => {
prevRef.current = userText;
});
I've updated your example to use this. And it works out.
我已经更新了您的示例以使用它。它奏效了。
const { useState, useEffect, useRef } = React;
const App = () => {
const [userText, setUserText] = useState("");
const prevRef = useRef();
useEffect(() => {
prevRef.current = userText;
});
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${prevRef.current}${key}`);
}
};
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
回答by varun agarwal
In the second approach, the useEffectis bound only once and hence the userTextnever gets updated. One approach would be to maintain a local variable which gets updated along with the userTextobject on every keypress.
在第二种方法中,useEffect只绑定一次,因此userText永远不会更新。一种方法是维护一个局部变量,该变量userText在每次按键时与对象一起更新。
const [userText, setUserText] = useState('');
let local_text = userText
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
local_text = `${userText}${key}`;
setUserText(local_text);
}
};
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
Personally I don't like the solution, feels anti-reactand I think the first method is good enough and is designed to be used that way.
我个人不喜欢该解决方案,感觉anti-react并且我认为第一种方法已经足够好并且旨在以这种方式使用。
回答by di3
you dont have access to the changed useText state. you can comapre it to the prevState. store the state in a variable e.g.: state like so:
您无权访问更改后的 useText 状态。您可以将其与 prevState 进行比较。将状态存储在一个变量中,例如:像这样的状态:
const App = () => {
const [userText, setUserText] = useState('');
useEffect(() => {
let state = ''
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
state += `${key}`
setUserText(state);
}
};
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
};


