Skip to content
Advertisement

onKeyDown / onKeyUp listener in React

I am trying to add an event listener to an icon in React and it’s not working. My code:

const handleKey = (event) => {
    console.log(event);
}

<MdOutlinePause onKeyDown={handleKey} />

I also have an onClick handler on the same button that functions just fine, so I’m really confused as to why the onKeyDown doesn’t fire. Appreciate any help anyone can provide!

Advertisement

Answer

Using onKeyDown on a certain element will require focus to work as intended.

We can make your MdOutlinePause component focus-able by adding the tabindex attribute. That will allow you to give the icon focus by hitting tab or by clicking on it. The element would look something like this:

<MdOutlinePause tabIndex={0} onKeyDown={handleKey} />

If you want to detect the key event without having to focus the element, you will need to attach an event listener. We can do this with useEffect so this happens onMount, and we’ll attach the event listener to the window so it works regardless of which element has focus:

  /// Don't copy and paste this - see below
  useEffect(() => {
    window.addEventListener("keydown", handleKeyPress);
    });
  }, []);
  /// Don't copy and paste this - see below

This allows you to detect the keyboard event, which is great, but there’s an issue: the eventListener gets added again whenever the page mounts, but is never removed. So you will likely run into issues of your function getting called multiple times on each keyPress.

We can fix this by removing the event listener in the return from useEffect. This is how we specify a function to let the useEffect clean up after itself. This ensures that our useEffect is never adding more than one “keydown” event listener at a time:

  useEffect(() => {
    window.addEventListener("keydown", handleKeyPress);

    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, []);

Here’s a full example on codesandbox

This works great if you don’t need to access state in your handleKeyPress function.


It is a common use case to need to do something with state variables when the user does a certain action, and if we need to access updated state then we run into a problem: we attached handleKeyPress to the event listener onMount and the function never gets updated, so it will always use the initial version of state. This codesandbox illustrates the issue.

We can fix this by adding handleKeyPress to the dependencies array for useEffect, but this results in our event listener being removed and re-added on every render since handleKeyPress will be updated on each render. A better solution is to use the callback pattern with our handleKeyPress so that it is only updated when actually necessary (when the state it depends on is updated). We also want to add handleKeyPress to our useEffect dependency array so we create a new event listener when the handleKeyPress function changes:

  const handleKeyPress = useCallback((event) => {
    // do stuff with stateVariable and event
  }, [stateVariable]);

  useEffect(() => {
    window.addEventListener("keydown", handleKeyPress);

    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, [handleKeyPress]);

Now everything updates appropriately! Here’s a full example of this on codesandbox.


There’s one more solution which lets you access state in the simplest way possible. The downside is that the only state variable you have access to (with updated values) will be the state variable you are updating, so this solution is situational. To access other state variables, you’ll have to use the solution above.

By passing a function to useState, we can access the previous state as the first argument of the function. This allows for a much simpler approach, shown below and in this codesandbox.

  const [text, setText] = useState("");

  const handleKeyPress = event => {
    setText(previousText => `${previousText}${event.key}`);
  };

  useEffect(() => {
    window.addEventListener("keydown", handleKeyPress);

    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, []);

Advertisement