Thinking in React Tutorial: a custom hook to capture keystrokes in TypeScript

Thinking in React can feel weird if you're not used to it. In this post, I'm going to show you a React-way to think about and use the native browser's APIs and hook them into your React app.

Specifically, I'm going to show you how to listen for keystrokes in a React-y way. Keyboard shortcuts are making a comeback in webapps -- but this pattern is more than listening for keystrokes: it's a way of thinking: the React Way.

Here's what we're going to build:

You can find the completed Codepen here:


We're going to build a custom React hook that listens for keystrokes and lets us know when a certain key is pressed.

As you might have guessed, we have a book that teaches intermediate React patterns with TypeScript. It's called Fullstack React with TypeScript and you can get a copy here

In this post, we're going to look at:

  • How to create our own hook

  • The useEffect() hook and when to use it

  • Where to hook into the native browser APIs in React

  • How to use TypeScript to type the whole thing

The end goal: usePressObserver hook#

When we're done, we will have a custom hook called usePressObserver.

usePressObserver will accept a watchKey argument, which specifies the key we're watching. It will return a pressed variable which will either return true or false depending on if the key is pressed down.

We can use that pressed value in another component. In this case, we'll pass it as the active prop to the Button component:

So how do we get there?

The browser supports "event listeners".#

You can listen to any key press by listening to the "keydown" event, like this:

And you can listen to any key release by listening to the "keyup" event, like this:

Now, what is the second argument here? handlePressStart and handlePressFinish are callback functions -- that is, functions that the browser will call whenever the appropriate event happens.

So far, so good, but if we're using React where do we even put this??

If we just throw a document.addEventListener into a component we will add a new listener on every re-render. Not what we want.

Does React provide any functionality that will allow us to only call a function only once? Yes. Well, sort of.

The useEffect Hook#

The useEffect hook lets you perform side effects. The API to useEffect looks like this:

effectFn is a function argument. That is, it is the function that contains the effect we want to do. We'll look at that in a moment.

arrayOfThingsToWatch is an array of variables which React will watch. If any of those changes, React will call the effectFn again -- but if they don't change, it won't be called again, which is exactly what we want.

So pseudocode of what we're trying to do looks like this:

So let's fill in the blanks.

Defining the usePressObserver Hook#

The first thing we want to do is define our custom hook. Here's the skeleton:

We start by defining a Settings interface, which defines our options for this hook. In this case, we'll just have one option watchKey, which is the key we want to watch.

Next, we use the useState hook to keep track of if this key is pressed or not.

I'm going to assume you're familiar with the useState hook. If not, check out our blog post which is An introduction to Hooks in React

Notice, too, that we return pressed as the result of this hook. pressed is the primary value we're trying to extract, so that's what we return from the hook.

So this gives us a hint to our implementation of handlePressStart -- what needs to happen there? We need to call setPressed to tell React that our key is pressed.

Notice something here: we have to check to make sure the key that was pressed is the key we care about. Remember way back above that we said "keydown" will trigger for any key. So we have to filter our key code here.

handlePressFinish is similar:

Are we done? Not quite. We need to "clean up" after ourselves. Because React will re-render components, we need to make sure that if this component is re-rendered that we removeEventListeners that we created.

We do this by returning a cleanup function from the useEffect hook:

Putting it all together, we get this:

It can look daunting altogether, but when you break it down it's not too bad.

For the sake of space, I've had to leave a few bits of code out, but you can find the whole code example here on Codesandbox:


So there you go! That's how you think about some native browser APIs in a React-y way.

If you want to learn more about intermediate React patterns with TypeScript, check out Fullstack React with TypeScript: