DEV Community

Cover image for Using Custom React Hooks to Listen to DOM Events
Seb
Seb

Posted on • Originally published at nimblewebdeveloper.com on

Using Custom React Hooks to Listen to DOM Events

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!

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).

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

Enter fullscreen mode Exit fullscreen mode

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;

// 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  


Enter fullscreen mode Exit fullscreen mode

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.

// 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>  
  );  
}  

Enter fullscreen mode Exit fullscreen mode

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;

Screenshot of app showing

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;

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;  


Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

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.

// 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  

Enter fullscreen mode Exit fullscreen mode

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.

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;  


Enter fullscreen mode Exit fullscreen mode

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

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.

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

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

Enter fullscreen mode Exit fullscreen mode

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 function if you like).

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

We're going to create a throttled version.


// ...  
// ...  

// 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  
});  

Enter fullscreen mode Exit fullscreen mode

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.'

useEffect(() => {  

  // ...  

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

})  

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)