在Reactjs中监听文档的按键事件

114

我想要在按下 esc 键时绑定并关闭当前的 react bootstrap 弹出框。以下是代码:

_handleEscKey: function(event) {
  console.log(event);
  if (event.keyCode == 27) {
    this.state.activePopover.hide();
  }
},

componentWillMount: function() {
  BannerDataStore.addChangeListener(this._onchange);
  document.addEventListener("click", this._handleDocumentClick, false);
  document.addEventListener("keyPress", this._handleEscKey, false);
},

componentWillUnmount: function() {
  BannerDataStore.removeChangeListener(this._onchange);
  document.removeEventListener("click", this._handleDocumentClick, false);
  document.removeEventListener("keyPress", this._handleEscKey, false);
},

但是当我按下任何键时,控制台中没有任何日志记录。 我已经尝试在窗口上监听,并使用不同的情况'keypress','keyup'等来尝试监听,但似乎我做错了什么。


说句实话,我已经为React发布了一个键按下库,旨在使所有这些变得更加容易:https://github.com/jedverity/react-keydown/ - glortho
7个回答

75

你应该使用keydown而不是keypress

keypress(已弃用)通常仅用于按下会产生字符输出的键,根据文档所述。

keypress(已弃用)

keypress事件在按下一个通常产生字符值的键时触发。

keydown

keydown事件在按下键时触发。


63

我自己也遇到了类似的问题。我将使用你的代码来说明如何解决它。

// for other devs who might not know keyCodes
var ESCAPE_KEY = 27;

_handleKeyDown = (event) => {
    switch( event.keyCode ) {
        case ESCAPE_KEY:
            this.state.activePopover.hide();
            break;
        default: 
            break;
    }
},

// componentWillMount deprecated in React 16.3
componentDidMount(){
    BannerDataStore.addChangeListener(this._onchange);
    document.addEventListener("click", this._handleDocumentClick, false);
    document.addEventListener("keydown", this._handleKeyDown);
},


componentWillUnmount() {
    BannerDataStore.removeChangeListener(this._onchange);
    document.removeEventListener("click", this._handleDocumentClick, false);
    document.removeEventListener("keydown", this._handleKeyDown);
},

由于您使用了createClass的方式,无需将某些方法绑定到this,因为在定义的每个方法中,this都是隐含的。

这里有一个使用React组件创建的createClass方法的可工作的jsfiddle,链接在此.


9
由于绑定操作每次都会产生一个新的实例,因此这种方式不能正确地删除事件侦听器。确保缓存绑定返回的结果,以便正确地添加和从文档中删除。 - Steven10172
@Steven10172 说得好,因为在React.createClass方法中并没有真正定义constructor,所以你可以在getInitialState()中绑定。 - Chris Sullivan
关于上面的评论,这是一个很好的绑定和使用事件监听器的示例。https://dev59.com/gVwY5IYBdhLWcg3wgX6V - Craig Myles
1
注意,componentWillMount 在 React 16.3 版本中已被弃用。我认为你应该在 componentDidMount 中注册事件监听器。 - Igor Akkerman
重要提示:请确保这两个步骤在最高级别上完成:1)handleKeyDown函数定义和2)监听器设置。我之前在一个子组件中完成了这两个步骤,但程序运行不起来。后来将其移至主组件中,问题得以解决。 - Marlo

53
如果您可以使用React Hooks,则一个好的方法是使用useEffect,这样事件侦听器将仅订阅一次,并在卸载组件时正确取消订阅。
下面的示例摘自https://usehooks.com/useEventListener/:
// Hook
function useEventListener(eventName, handler, element = window){
  // Create a ref that stores handler
  const savedHandler = useRef();

  // Update ref.current value if handler changes.
  // This allows our effect below to always get latest handler ...
  // ... without us needing to pass it in effect deps array ...
  // ... and potentially cause effect to re-run every render.
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(
    () => {
      // Make sure element supports addEventListener
      // On 
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;

      // Create event listener that calls handler function stored in ref
      const eventListener = event => savedHandler.current(event);

      // Add event listener
      element.addEventListener(eventName, eventListener);

      // Remove event listener on cleanup
      return () => {
        element.removeEventListener(eventName, eventListener);
      };
    },
    [eventName, element] // Re-run if eventName or element changes
  );
};
你也可以从npm安装它,例如:npm i @use-it/event-listener - 在这里查看项目 - https://github.com/donavon/use-event-listener。然后,在组件中使用它,你只需要在函数式组件内调用它并传递事件名称和处理程序即可。例如,如果你想每次按下Escape键时console.log记录日志:
import useEventListener from '@use-it/event-listener'

const ESCAPE_KEYS = ['27', 'Escape'];

const App = () => {
  function handler({ key }) {
    if (ESCAPE_KEYS.includes(String(key))) {
      console.log('Escape key pressed!');
    }
  }

  useEventListener('keydown', handler);

  return <span>hello world</span>;
}

1
如果 App 不是一个功能组件,就不能使用它。 - ashubuntu
3
谢谢您发布这篇文章,它帮助我修复了全局键盘处理程序中的大量内存泄漏问题。顺便说一下,“将监听器保存到ref”效果非常关键——不要将事件处理程序传递给在useEffect依赖数组中添加到document.body.onKeyDown的地方! - aendra
1
@aendrew:将处理程序保存到 ref 和仅声明函数有何区别? - thelonglqd
@thelonglqd 我认为因为否则它们会被添加多次作为事件处理程序 - 不过别引用我说的话,那是半年前的事情了,我的记忆有点模糊! - aendra
1
任何试图在NextJS上实现此功能的人,只需在useEventListener方法中添加一个窗口对象验证检查,以确保它在客户端运行。还要删除参数elementif (typeof window === 'undefined') return; const element = window; - KeshavDulal
终于有东西可以用了!非常好的答案,谢谢! - Gass

4
这是一篇与此问题更相关的Jt oso答案版本。我认为这比使用外部库或API钩子来绑定/解绑侦听器的其他答案要简单得多。
var KEY_ESCAPE = 27;
...
    function handleKeyDown(event) {
        if (event.keyCode === KEY_ESCAPE) {
            /* do your action here */
        }
    }
...
    <div onKeyDown={handleKeyDown}>
...

7
首先需要将焦点放在该项上。如果您想要一个全局事件监听器,可能不会被触发,因为初始时body元素获得了焦点。 - n1ru4l
1
你实际上可以使用 if (event.key === 'Escape') - Yifan Ai

3

我对一个可切换的div有同样的要求。

下面的代码是在items.map((item)=>的调用中:

  <div
    tabindex="0"
    onClick={()=> update(item.id)}
    onKeyDown={()=> update(item.id)}
   >
      {renderItem(item)}
  </div>

这对我很有帮助!

1

我想要全局事件监听器,但因使用React Portals导致出现奇怪的行为。即使在文档中取消了portal模态组件上的事件,事件仍会触发在文档元素上。

我转而只在包装整个组件树的根对象上使用事件监听器。问题在于初始情况下body是聚焦的,而不是根元素,因此当你在树内部聚焦元素时,事件将首先被触发一次。

我的解决方案是添加tabindex并使用effect hook自动聚焦它。

import React from "react";

export default GlobalEventContainer = ({ children, ...props }) => {
  const rootRef = React.useRef(null);
  useEffect(() => {
    if (document.activeElement === document.body && rootContainer.current) 
      rootContainer.current.focus();
    }
  });

  return <div {...props} tabIndex="0" ref={rootRef}>{children}</div>
};

0

我在编写一个类似Wordle的克隆程序时遇到了类似的问题,即如何识别按键并使它们按照我的预期工作。以下是我的解决方案:

import { useCallback, useEffect, useState } from "react";

const [random, setRandom] = useState(0.0);
const [key, setKey] = useState("");

const registerKeyPress = useCallback((e) => {
  setRandom(Math.random());
  setKey(e.key);
}, []);
useEffect(() => {
  if (key === "x") console.log("do something");
  }, [random]);
useEffect(() => {
  window.addEventListener("keydown", registerKeyPress);
}, [registerKeyPress]);

我正在使用随机数来触发useEffect()函数,该函数实际上处理按键操作,因为如果按下的键与上次按下的键相同,则不会发生任何事情。(例如,在我的wordle克隆中,单词gloom或任何带有双字母或使用退格键删除多个字母的单词都无法正常工作。)

** 这将导致useEffect的依赖数组中缺少值的警告。也许有更合适的方法来组织钩子(如果有人知道,请评论),但这些警告不会导致错误。您可以使用以下注释来消除它们: // eslint-disable-next-line react-hooks/exhaustive-deps **


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接