如何使用 useEffect 挂钩注册事件?

Posted

技术标签:

【中文标题】如何使用 useEffect 挂钩注册事件?【英文标题】:How to register event with useEffect hooks? 【发布时间】:2019-08-29 03:09:32 【问题描述】:

我正在学习关于如何使用钩子注册事件的 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>
  );

现在效果很好,但我不相信这是正确的方法。原因是,如果我理解正确的话,在每次重新渲染时,事件都会一直注册和注销,我根本不认为这是正确的做法。

所以我对下面的useEffect 钩子做了一点修改

useEffect(() => 
  window.addEventListener('keydown', handleUserKeyPress);

  return () => 
    window.removeEventListener('keydown', handleUserKeyPress);
  ;
, []);

通过将空数组作为第二个参数,让组件只运行一次效果,模仿componentDidMount。当我尝试结果时,奇怪的是在我键入的每个键上,而不是附加,而是被覆盖。

我期待 setUserText($userText$key); 将新键入的键附加到当前状态并设置为新状态,但相反,它忘记了旧状态并用新状态重写。

我们应该在每次重新渲染时注册和取消注册事件真的是正确的方式吗?

【问题讨论】:

【参考方案1】:

处理此类情况的最佳方法是查看您在事件处理程序中所做的工作。

如果您只是使用以前的state 设置state,最好使用回调模式并仅在initial 挂载时注册事件侦听器。

如果您不使用callback pattern,则事件侦听器将使用侦听器引用及其词法范围,但会创建一个新函数,并在每次渲染时更新闭包;因此在处理程序中您将无法访问更新的状态

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>
  );

【讨论】:

但这会在每次按键时创建一个新的绑定函数。如果您的重点是性能,则本地状态变量会更好 @di3 如果您使用useCallback 定义具有空依赖数组的函数,您也不会遇到这个问题 我看到重要的是使用prevUserText 来引用userText 的先前状态。如果我需要访问多个状态怎么办?如何访问之前的所有状态? 如果我们使用useReducer而不是useState,我们如何获得先前的状态? @tractatusviii 我认为这篇文章会对你有所帮助:***.com/questions/55840294/…【参考方案2】:

接受的答案有效,但如果您正在注册BackHandler 事件,请确保在您的handleBackPress 函数中使用return true,例如:

const handleBackPress= useCallback(() => 
   // do some action and return true or if you do not
   // want the user to go back, return false instead
   return true;
 , []);

 useEffect(() => 
    BackHandler.addEventListener('hardwareBackPress', handleBackPress);
    return () =>
       BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
  , [handleBackPress]);

【讨论】:

【参考方案3】:

问题

[...] 每次重新渲染时,事件都会不断注册和注销,我根本不认为这是正确的做法。

你是对的。在 every 渲染上重新启动 useEffect 内的事件处理是没有意义的。

[...] 空数组作为第二个参数,让组件只运行一次效果 [...] 奇怪的是,在我键入的每个键上,它不是追加,而是被覆盖。

这是stale closure values 的问题。

原因:useEffect 中使用的函数应该是part of the dependencies。您没有设置任何依赖项 ([]),但仍调用 handleUserKeyPress,其本身读取为 userText 状态。

解决方案

根据您的用例,有一些替代方案。

1。状态更新函数

setUserText(prev => `$prev$key`);

✔ 侵入性最小的方法 ✖ 只能访问自己之前的状态,不能访问其他状态

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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var  useReducer, useEffect, useState, useRef  = React</script>

2。 useReducer - "cheat mode"

我们可以切换到 useReducer 并访问当前状态/道具 - 使用与 useState 类似的 API。

变体 2a:reducer 函数内部的逻辑

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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var  useReducer, useEffect, useState, useRef  = React</script>

变体 2b:reducer 函数之外的逻辑 - 类似于 useState updater 函数

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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var  useReducer, useEffect, useState, useRef  = React</script>

✔ 无需管理依赖项 ✔ 访问多个状态和道具 ✔ 与useState相同的API ✔ 可扩展到更复杂的案例/减速器 ✖ 由于内联减速器,性能略有下降 (kinda neglectable) ✖ 略微增加了 reducer 的复杂度

3。 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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var  useReducer, useEffect, useState, useRef, useCallback  = React</script>

✔ 用于不用于重新渲染数据流的回调/事件处理程序 ✔ 无需管理依赖项 ✖ 仅被 React 文档推荐为最后一个选项 ✖ 更命令式的方法

查看这些链接以获取更多信息:123

不当解决方案

useCallback

虽然它可以以多种方式应用,但useCallback 并不适合这个特殊的问题案例

原因:由于添加了依赖项 - 此处为 userText -,事件侦听器将在 每次 按键时重新启动,在最好的情况下不会执行,或者更糟会导致不一致。

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]); // we rely directly on handler, indirectly on userText

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>userText</blockquote>
    </div>
  );


ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var  useReducer, useEffect, useState, useRef, useCallback  = React</script>

为了完整起见,以下是useCallback 的一些要点:

✔ 万能实用解决方案 ✔ 微创 ✖ 手动依赖管理 ✖ useCallback 使函数定义更加冗长/杂乱

useEffect中声明处理函数

声明事件handler function directly inside useEffectuseCallback有或多或少相同的问题,后者只是导致更多的间接依赖。

换句话说:我们没有通过useCallback添加额外的依赖层,而是将函数直接放在useEffect中 - 但是仍然需要设置所有依赖项,导致频繁的处理程序更改。

事实上,如果你将handleUserKeyPress 移动到useEffect 中,ESLint 详尽的 deps 规则会告诉你,如果没有指定,缺少哪些确切的规范依赖项 (userText)。

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]); // ESLint will yell here, if `userText` is missing

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>userText</blockquote>
    </div>
  );


ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var  useReducer, useEffect, useState, useRef  = React</script>

【讨论】:

这看起来越来越像 hooks 中一个被忽视的设计缺陷【参考方案4】:

新答案:

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)
  
, [])

当使用setUserText 时,将函数作为参数而不是对象传递,prevUserText 将始终是最新状态。


旧答案:

试试这个,它和你原来的代码一样工作:

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])

因为在您的 useEffect() 方法中,它取决于 userText 变量,但您不要将其放在第二个参数中,否则 userText 将始终绑定到带有参数 @ 的初始值 '' 987654329@.

你不需要这样做,只是想让你知道为什么你的第二个解决方案不起作用。

【讨论】:

通过添加[userText] 与没有第二个参数完全相同,对吧?原因是我在上面的例子中只有userText,没有第二个参数只是意味着在每个道具/状态变化时重新渲染,我看不出它是如何回答我的问题的。 **P/S:** 我不是反对者,无论如何感谢您的回答 嘿 @Isaac ,是的,这和没有第二个参数一样,我只是想让你知道为什么你的第二个解决方案不起作用,因为你的第二个解决方案 useEffect() 依赖于 @987654333 @ 变量,但您没有放入第二个参数。 但是通过添加[userText],也意味着在每次重新渲染时注册和取消注册事件对吧? 没错!这就是为什么我说它与您的第一个解决方案相同。 明白你的意思,如果你真的想在这个例子中只注册一次,那么你需要使用useRef,就像@Maaz Syed Adeeb 的回答一样。【参考方案5】:

您无权访问更改后的 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>
  );  
;

【讨论】:

【参考方案6】:

对于您的用例,useEffect 需要一个依赖数组来跟踪更改,并根据依赖关系确定是否重新渲染。始终建议将依赖数组传递给useEffect。请看下面的代码:

我已经介绍了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>
  );

【讨论】:

我已经尝试过您的解决方案,但它与[userText] 完全相同或没有第二个参数。基本上我们在useEffect 里面放了一个console.log,我们会看到每次重新渲染都会触发日志记录,这也意味着,addEventListender 会在每次重新渲染时运行 我想相信这是一种预期的行为。我更新了我的答案。 在您的沙盒上,您已在 useEffect 挂钩中放置了一条声明 console.log('&gt;');,并且通过使用您更新的代码,它仍然每次都在记录,这也意味着事件仍然在每次重新注册-渲染 但是因为return () =&gt; window.removeEventListener('keydown', handleUserKeyPress),每次重新渲染时,组件都会注册和注销 正是我希望的行为,但你可以观察它@codesandbox.io/s/n5j7qy051j【参考方案7】:

在第二种方法中,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>
  );

我个人不喜欢这个解决方案,感觉是anti-react,我认为第一种方法已经足够好,并且设计用于这种方式。

【讨论】:

您介意包含一些代码来演示如何在第二种方法中实现我的目标吗?【参考方案8】:

您需要一种方法来跟踪之前的状态。 useState 仅帮助您跟踪当前状态。从docs 开始,有一种方法可以访问旧状态,方法是使用另一个钩子。

const prevRef = useRef();
useEffect(() => 
  prevRef.current = userText;
);

我已更新您的示例以使用它。并且成功了。

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>

【讨论】:

以上是关于如何使用 useEffect 挂钩注册事件?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确地将事件侦听器添加到 React useEffect 挂钩?

如何使用 jest/enzyme 测试 useEffect 与 useDispatch 挂钩?

如何在 useEffect 挂钩中使用 Promise.all 获取所有数据?

如何在 useEffect 挂钩中形成 setFieldValue

如何在 React 中使用 useEffect 挂钩调用多个不同的 api

如何将参数传递给 useEffect 挂钩