TutorialsCourses

Create a useMousePosition Hook with useEffect and useState in React

Intro

Hooks added a lot of functionality to stateless components. Not only did they add functionality but reusability and sharing of logic across your app. You can combine many of the hooks to leverage each other to build out this reusable logic.

Setup

We'll create a separate file called useMousePosition and import a function that we'll export later. We just expect that it will return a position that is the mouse position. As you can see we have no logic here. We are just requesting a position to render.

The reason we start the function with use is that React provides an eslint plugin that will lint any function that starts with use and make sure you are not violating any of the hook requirements.

import React from "react";
import { useMousePosition } from "./useMousePosition";

const App = () => {
  const position = useMousePosition();

  return <div />;
};

export default App;

Setup our useState

Lets create our function and setup our useState. The useState will take an initial state and we'll just initialize it with {x: 0, y: 0}. This will return an array that we can destructure. It will destructure to the state position, and then a function to update the state.

import { useState, useEffect } from "react";

export const useMousePosition = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return position;
};

If you're confused about the destructuring of the array here is the equivalent.

const positionState = useState({ x: 0, y: 0 });
const position = positionState[0];
const setPosition = positionState[1];

This is just an ES6 and beyond feature.

Setup our useEffect

Now we setup our useEffect. Within our useEffect we listen on the window for any mouse movements. We need to be sure and clean up any effects that we make and to do that we return a function. This will run when the component is unmounted, or anything in our second argument array changes.

The second argument to useEffect is empty because we only need to attach the window listener once. React will look at the array each render and see if anything has changed. If you didn't pass the array it would always reapply this effect every re-render.

useEffect(() => {
  window.addEventListener("mousemove");

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

Updating state

You'll notice we didn't apply a function to our addEventListener. When registering an event listener you supply a function. To unregister you need to call removeEventListener with the same function. We no longer have this like we do with classes to store the exact function to unregister.

To do that we can just create a function inside of useEffect. This function will then receive our event and call our setPosition update function with the values.

const setFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });

Putting it all together our hook will look like this.

import { useEffect, useState } from "react";

export const useMousePosition = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const setFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", setFromEvent);

    return () => {
      window.removeEventListener("mousemove", setFromEvent);
    };
  }, []);

  return position;
};

Using the Hook

Now that we're returning our state with an x/y we can render it and move our mouse around and see our div update.

const App = () => {
  const position = useMousePosition();

  return (
    <div>
      {position.x}:{position.y}
    </div>
  );
};