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