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


77

我正在学习有关如何使用钩子注册事件的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}); 将新键入的键追加到当前状态并设置为新状态,但是,它会忘记旧状态并用新状态重写。

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

Answers:


90

处理此类情况的最佳方法是查看事件处理程序中的操作。如果仅使用先前状态设置状态,则最好使用回调模式并仅在初始安装时注册事件侦听器。如果您不使用callback patternhttps://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>
  );

与其他人相比,您的解决方案对我来说最有意义,非常感谢!
艾萨克(Isaac)

很高兴为您提供帮助(y)
Shubham Khatri

7
我认为重要的是要prevUserText 用来引用的先前状态userText。如果我需要访问多个状态怎么办?如何获得以前所有状态的访问权限?
Joey Yi Zhao

1
如果我们使用useReducer而不是,如何获得以前的状态useState
Eesa

1
@tractatusviii我认为这篇文章将帮助您:stackoverflow.com/questions/55840294/...
Shubham卡特里

17

问题

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

你是对的。它没有意义的重启事件中处理useEffect每个渲染。

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

这是陈旧的闭包值问题

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

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

1.状态更新器功能

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

✔微创方法
✖仅访问自己的先前状态,不能访问其他状态


2. useReducer- “作弊模式”

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

变体2a:减速器功能内的逻辑
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}`;  
}, "");

变体2b:减速器功能之外的逻辑-类似于useState更新器功能
const [userText, setUserText] = useReducer((state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);

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


3. useCallback/事件处理程序里面useEffect

如果您进入handleUserKeyPress内部useEffect,ESLint详尽的deps规则将告诉您,缺少哪些确切的规范依赖(userText):

useCallback 是等效的替代品,具有更多的依赖关系间接方式:

注意:虽然可以以多种方式应用此变体,但它不适合出现问题的情况,因此出于完整性考虑而列出。原因:每次按键都会重新启动事件监听器。

✔通用的务实解决方案
✔最小的侵入性
✖手动依赖项的管理
useCallback使功能定义更加冗长/混乱


4. 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) };
}, []);

✔对于不用于重新渲染数据流的回调/事件处理程序
✔无需管理依赖
项React仅由React docs建议作为最后一个选项
✖更强制性的方法

请查看以下链接以获取更多信息:1 2 3


3
哇,这真是个答案!
kremuwa

8

新答案:

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将始终将参数绑定到''带有arguments的初始值[]

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


通过增加 [userText]与没有第二个参数完全相同,对吗?原因是我仅userText在上面的示例中具有该属性,并且没有第二个参数只是意味着在每个道具/状态更改时重新渲染,我看不到它如何回答我的问题。** P / S:**我不是拒绝投票的人,还是感谢您的回答
Isaac

嘿@Isaac,是的,与没有第二个参数的情况一样,我只想让您知道为什么第二个解决方案不起作用,因为第二个解决方案useEffect()取决于userText变量,但没有放入第二个参数。
Spark.Bao

但是通过添加[userText],这还意味着在每次重新渲染时都注册和注销事件吗?
艾萨克(Isaac)

究竟!那就是为什么我说您的第一个解决方案是相同的。
Spark.Bao

1
有了您的意思,如果在此示例中您真的只想注册一次,则需要使用useRef@Maaz Syed Adeeb的答案。
Spark.Bao

1

对于您的用例,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>
  );

编辑q98jov5kvq


我已经尝试过您的解决方案,但这与[userText]是否有第二个参数完全相同。基本上,我们放入了一个console.log内部useEffect,我们将看到日志记录正在触发每个重新渲染,这也意味着,addEventListender正在运行每个重新渲染
Isaac

我想相信这是一种预期的行为。我更新了答案。
codejockie

在沙箱,你已经把一份声明console.log('>');useEffect挂钩,并通过使用更新后的代码,它仍然记录每次,这也意味着该事件仍然登记在每个重新渲染
艾萨克

1
但由于return () => {window.removeEventListener('keydown', handleUserKeyPress)},在每一个重新渲染,组件将注册和注销
艾萨克

1
确实是我所希望的行为,但是您可以在@ codeandbox.io
Isaac

0

您需要一种跟踪先前状态的方法。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>


-1

在第二种方法中,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,我认为第一种方法足够好,并且设计为可以使用这种方法。


您介意包含一些代码来演示如何实现第二种方法的目标吗?
艾萨克(Isaac)

-1

您无权访问已更改的useText状态。您可以将其与prevState进行比较。将状态存储在变量中,例如:state像这样:

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>
  );  
};
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.