Using Custom React Hooks to Listen to DOM Events
Tags: react, javascript, webdev
Published: 2020-03-26 05:06:54

---
title: Using Custom React Hooks to Listen to DOM Events
published: true
date: 2020-03-23 13:00:00 UTC
tags: react, javascript, webdev
canonical_url: https://nimblewebdeveloper.com/blog/using-custom-react-hooks-to-listen-to-dom-events
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/wm6hie3npoyyxa69i1vd.png
---

Listening to DOM events in React can be a little tricky and can quickly get messy. Even worse, it can be hard to trigger effects and state updates based on DOM events. Luckily, we can easily use custom hooks to 'reactify' our DOM event listeners. Read on!

## Watch Video
Rather watch than read? I recorded a video of this post too!
Note: I didn't have an external mic for this video, so please forgive the typing and fan noise!

{% vimeo 400112751 %}

## Get Started

To get started, you're going to need a (surprise...) React app.

If you don't already have one, I recommend using `create-react-app` to quickly scaffold a new React app with all the tools you need for quick development.

Jump into your favourite terminal and run the command below. If you'd like to change the name of the folder your app is created in, change `custom-react-hooks-dom-events` to the folder name you'd like (this folder will be created).

```sh  
npx create-react-app custom-react-hooks-dom-events  
  
```

Open up the project in your favourite code editor. I'm using VS Code.

If you already have a React project, open that and get started.

## Add a new hook

We're going to add a hook to listen to viewport width and changes. I'm going to put this code in a separate file so it's easy to reuse throughout the project.

Create a new file called `useViewportWidth.js`

I'm adding this file in `src/useViewportWidth.js`, but if you have a larger project, you'll probably want to have it somewhere more sensible.

Open up our new file and add the following code;

```javascript  
// src/useViewportWidth.js  
  
// We'll need useState and useEffect from react  
//This let's us 'reactify' our values  
import { useState, useEffect } from "react";  
  
// This is our custom hook  
// It's just a function that returns a value for now  
const useViewportWidth = () => {  
    
  // Just a test  
  const viewportWidth = "Viewport Width goes here"  
  
  // Our hook should return something  
  return viewportWidth  
}  
  
export default useViewportWidth  
  
  
```

At this point this code is just a test to make sure our hook works.

## Test that it works

I'm going to edit `src/App.js` for this demo, but you should open the component where you'd like to use the hook value.

```javascript  
// src/App.js  
  
//  
// ... other code  
//  
  
// Import our hook  
import useViewportWidth from "./useViewportWidth";  
  
function App() {  
  // ... other code  
    
  // Add our hook call  
  // We're setting the result of our hook  
  // to a constant called 'viewportWidth'  
  const viewportWidth = useViewportWidth();  
  
  
  // Render something  
  return (  
    <div className="App">  
      <header className="App-header">  
      {/\* We're just going to render the viewport width for now.  
      You could use this however you like \*/}  
        <p>The viewport width is: {viewportWidth}</p>  
      </header>  
    </div>  
  );  
}  
  
```

Here we imported the hook from the file we made earlier, and then called the hook within our component. Finally we rendered the result of the hook (viewportWidth) to the DOM.

If everything has gone well, you should see something like;

<img src="https://cdn.sanity.io/images/uvwftosn/production/73aaae2ffef5c3bf49f1e0496681859bbd5ff636-1564x812.png" alt="Screenshot of app showing "viewport width is viewport width goes here""/>
## Make it useful

So far so good, but that's not really very useful yet. We need some information about the viewport.

We'll achieve this in a couple of steps.

1. Add a function to get the width of the viewport
2. Create a state object and setter via **useState**
3. Bind an event listener to changes in the viewport size

### Get the viewport width

We're going to add a function to get the width of the viewport. Now this could go in a separate file again if you want it to be reusable, but for simplicity I'm going to include it in the hook.

Update your `src/useViewportWidth.js` to look like this;

```javascript  
import { useState, useEffect } from "react";  
  
const useViewportWidth = () => {  
    
  // Get the viewport width  
  const getViewportWidth = () => {  
    let e = window,  
      a = "inner";  
    if (!("innerWidth" in window)) {  
      a = "client";  
      e = document.documentElement || document.body;  
    }  
    return e[a + "Width"];  
  };  
  
  return viewportWidth;  
};  
export default useViewportWidth;  
  
  
```

Here we've added a function called **getViewportWidth** which does exactly what it says on the tin. It returns the width of the viewport (excluding scrollbars), and is reasonably cross browser compatible. It checks if **window.innerWidth** exists and if not uses **document.clientWidth**.

### Add the viewport width to a state object

Remember how in React we need to add values into "state" if we want to perform actions (side-effects) based on their values? Side-effects might be two-way bindings, rendering etc.

So after our **getViewportWidth** function, we're going to add the following line.

```javascript  
const [viewportWidth, setViewportWidth] = useState(getViewportWidth())  
  
```

What this does is sets up a state variable (viewportWidth) and sets it to the initial viewport width.

### Bind and event listener to the viewport width

Finally, we need to add an event listener to listen to changes in the viewport width. We can do this via `window.addEventListener`, but there are a couple of things we need to do to 'reactify' it.

We're going to add a **useEffect** hook, just after the last line we added (useState) to run a side-effect when the component mounts.

```javascript  
// Run an effect when the component mounts  
useEffect(() => {  
  // We're going to create an 'onResize' event handler which will update our state  
  const setFromEvent = () => setViewportWidth(getViewportWidth());  
    
  // Add an event listener for resize, which will update our state  
  window.addEventListener('resize', setFromEvent)  
    
  //Finally, remember to unbind the event listener on unmount  
  return () => {  
    window.removeEventListner('resize', setFromEvent)  
  }  
}, []); // Empty parentheses will cause this to run once at mount  
  
```

Our useEffect hook only runs once on component mount, and adds an event listener to the window resize event. The event listener sets our state variable to the new size of the viewport. Finally, we return a function to be called on unmount which will tidy up and remove the event listener.

## Here's one I prepared earlier

If you put it all together correctly, your `src/useViewportWidth.js` file should look like this;

You can feel free to copy/paste this into your project if you want.

```javascript  
import { useState, useEffect } from "react";  
  
const useViewportWidth = () => {  
  const getViewportWidth = () => {  
    let e = window,  
      a = "inner";  
    if (!("innerWidth" in window)) {  
      a = "client";  
      e = document.documentElement || document.body;  
    }  
    return e[a + "Width"];  
  };  
  
  const [viewportWidth, setViewportWidth] = useState(getViewportWidth());  
  
  useEffect(() => {  
    const setFromEvent = () => setViewportWidth(getViewportWidth());  
  
    window.addEventListener("resize", setFromEvent);  
  
    return () => {  
      window.removeEventListener("resize", setFromEvent);  
    };  
  }, []);  
  
  return viewportWidth;  
};  
export default useViewportWidth;  
  
  
```

And if you save and switch to your app should should see;

<img src="https://cdn.sanity.io/images/uvwftosn/production/69f4c561714f1c3afdfef0fa9272d5374956e1f8-2878x1750.gif" alt="null"/>
## Wrapping up

Awesome, you should now have seen how we can use a custom React hook to bind to a DOM event. See if you can use this to listen to the scroll position (scroll top) instead. Hit me up on twitter if you have an questions, feedback or suggestions, I'd love to hear them. I'm [@baffledbasti on twitter](https://twitter.com/baffledbasti).

## Before you go...

One last thing before you go. You might have noticed that our custom React hook will fire on every single update of the DOM event we're listening to. Consequently, any side-effects or renders that result from those updates will also run every single time. This might be many times per second! If you're using this in production it could have significant performance implications.

One thing we can do to this code to make it have a slightly smaller performance impact is throttle our hook updates.

The idea behind throttling is that we only allow an event to occur once per some period of time. Any additional triggers in this period are ignored.

Below is an example of how we can throttle our hook updates using the **throttle** function from the lodash library. You may not want to include lodash for just this function (although with tree shaking you can get around that).

### Two (and a bit) steps to throttling

#### 1. Import our throttle function

It's not really a step, but import the throttle function from lodash like so

```javascript  
// Only import the throttle function  
// this way tree shaking can only include this function  
import {throttle} from 'lodash'  
  
```

#### 2. Create a throttled version of our set state function

The throttle function from lodash works by taking a function and returning a throttled version. (You can [read the lodash docs about throttle](https://lodash.com/docs/latest#throttle) function if you like).

Remember our function we created to pass to the `window.addEventListener`?

We're going to create a throttled version.

```javascript  
  
// ...  
// ...  
  
// This is unchanged  
const setFromEvent = () => setViewportWidth(getViewportWidth());  
  
// Create a throttled version  
// that only fires every 100ms  
const throttledSet = throttlee(setFromEvent, 100, {  
  leading: true,  
  trailing: true  
});  
  
```

We created a throttled version of our function (called **throttledSet** ) that only fires every 100ms. We also specified that we want it to fire on the leading edge and the trailing edge.

If you're interested in understanding throttling and what the leading and trailing edge triggering are about, check out our post Understanding event throttling.

#### 3. Update our event binding

Finally, change your event bindings to call our new throttled function instead of the old one.'

```javascript  
useEffect(() => {  
    
  // ...  
    
  window.addEventListener('resize', throttledSet);  
  return () => {  
    window.removeEventListener('resize', throttledSet);  
  }  
    
})  
  
```

If you save and run your code again, you'll notice the viewportWidth is only updated at most twice per 100ms. Of course this may be hard to see, so if you'd like to see it with your own eyes, try setting the throttle window to 1000ms (1 second) and see what happens.