DEV Community

loading...
Headway

useHug: Creating custom React Hooks 🥰

Chris Held
・4 min read

Building custom hooks is a great way to encapsulate behaviors and reuse them throughout your application. To demonstrate this, we're going to build out the idea of "hugging" elements of our UI. Our huggable behavior will:

  • Change the mouse cursor on hover (we want our user to know what needs a hug).
  • Scale the element down on click (this is a firm hug, some squishiness is expected).
  • Change the mouse cursor while clicking (to show our appreciation).

I find the first step to making something reusable is to use it once, so let's implement this in a component:

import React, { useState } from "react";
import { animated, useSpring } from "react-spring";

const Huggable = () => {
  const [hovering, setHovering] = useState(false);
  const [pressed, setPressed] = useState(false);
  const animationProps = useSpring({
    transform: `scale(${pressed ? 0.8 : 1})`
  });
  const onMouseEnter = () => setHovering(true);
  const onMouseLeave = () => {
    setHovering(false);
    setPressed(false);
  };
  const onMouseDown = () => setPressed(true);
  const onMouseUp = () => setPressed(false);

  let className = "huggable";

  if (pressed) {
    className += " hugging-cursor";
  } else if (hovering) {
    className += " huggable-cursor";
  }

  return (
    <animated.div
      className={className}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
      onMouseDown={onMouseDown}
      onMouseUp={onMouseUp}
      style={animationProps}
            role="button"
    >
      Hug me!
    </animated.div>
  );
};

export default Huggable;
Enter fullscreen mode Exit fullscreen mode

There are a few things going on here so we'll take a closer look:

const [hovering, setHovering] = useState(false);
const [pressed, setPressed] = useState(false);
Enter fullscreen mode Exit fullscreen mode

There are two states that we want to track here, is the user hovering and have they pressed the button.

const animationProps = useSpring({
  transform: `scale(${pressed ? 0.8 : 1})`
});
Enter fullscreen mode Exit fullscreen mode

We take advantage of react-spring's useSpring hook to create an animation. We could also use CSS transforms here but react-spring does a lot of math for us to give us really good looking animations without much work.

const onMouseEnter = () => setHovering(true);
const onMouseLeave = () => {
  setHovering(false);
  setPressed(false);
};
const onMouseDown = () => setPressed(true);
const onMouseUp = () => setPressed(false);
Enter fullscreen mode Exit fullscreen mode

These event handlers will be used to manage our hovering / pressed state, which in turn will drive our behavior.

let className = "huggable";

if (pressed) {
  className += " hugging-cursor";
} else if (hovering) {
  className += " huggable-cursor";
}
Enter fullscreen mode Exit fullscreen mode

We set a className here dynamically based on our pressed / hovering state. This is used to control some basic styles as well as custom cursors when hovering. This might have been a little easier had I used JSS or styled components, but this served my needs just fine and will hopefully make sense to a wider audience.

return (
    <animated.div
      className={className}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
      onMouseDown={onMouseDown}
      onMouseUp={onMouseUp}
      style={animationProps}
            role="button"
    >
      Hug me!
    </animated.div>
  );
Enter fullscreen mode Exit fullscreen mode

Finally, our markup. Not much to see here as we're just passing down the props we defined above, but it's worth pointing out the animated tag, which is required by react-spring.

Here's what we've got so far:

huggable animation in action

Not bad! Now let's try and isolate what we want to encapsulate in a hook. We know this should be applicable to any element, so we won't want to use any of the markup. That leaves the state management, event handlers, the animation, and our classes:

const [hovering, setHovering] = useState(false);
const [pressed, setPressed] = useState(false);
const animationProps = useSpring({
  transform: `scale(${pressed ? 0.8 : 1})`
});
const onMouseEnter = () => setHovering(true);
const onMouseLeave = () => {
  setHovering(false);
  setPressed(false);
};
const onMouseDown = () => setPressed(true);
const onMouseUp = () => setPressed(false);

let className = "huggable";

if (pressed) {
  className += " hugging-cursor";
} else if (hovering) {
  className += " huggable-cursor";
}
Enter fullscreen mode Exit fullscreen mode

If we copy that into it's own function it looks something like this:

const useHug = () => {
  const [hovering, setHovering] = useState(false);
  const [pressed, setPressed] = useState(false);
  const style = useSpring({
    transform: `scale(${pressed ? 0.8 : 1})`
  });
  const onMouseEnter = () => setHovering(true);
  const onMouseLeave = () => {
    setHovering(false);
    setPressed(false);
  };
  const onMouseDown = () => setPressed(true);
  const onMouseUp = () => setPressed(false);

  let className = "";

  if (pressed) {
    className += "hugging-cursor";
  } else if (hovering) {
    className += "huggable-cursor";
  }

  //TODO: return...?
};
Enter fullscreen mode Exit fullscreen mode

All that's left now is what we want to return. This is an important decision as it defines what consuming components can do with our hook. In this case, I really want a consumer to be able to import the hook as one object and spread it over an html element, like so:

const huggableProps = useHug();

return <a href="/contact" {...huggableProps}>Contact Us</a>
Enter fullscreen mode Exit fullscreen mode

This makes our hook easy to consume and use while keeping some flexibility in case an element wants to pick and choose what events to use. In order to do that we have to leave off our state variables, since they aren't valid properties for html elements. This is what our return statement winds up looking like:

return {
  onMouseDown,
  onMouseEnter,
  onMouseLeave,
  onMouseUp,
  className,
  style
};
Enter fullscreen mode Exit fullscreen mode

Now that we've got our hook, the only thing left to do is to use it:

export default function App() {
  const { className, ...hugProps } = useHug();
  const buttonHugProps = useHug();
  return (
    <div className="App">
      <animated.section className={`huggable ${className}`} {...hugProps}>
        I like hugs!
      </animated.section>

      <br />
      <br />
      <animated.button {...buttonHugProps} type="button">
        buttons need hugs too
      </animated.button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the above example we've implemented our useHug hook in two ways, by taking all of the props and spreading them out over an element, and another by separating out the className prop and using that to compose a css class with our consuming element's existing className. We also make use of the animated tag to ensure our app animates correctly with react-spring.

Although this example may seem kind of silly, a lot of the process for extracting logic into a custom hook would remain the same, no matter what you're building. As you identify patterns in your code it's a good practice to look for ways you can abstract application logic or behavior in the same way you would abstract a common UI element like a modal or input. This approach can help set you up for success as your application grows over time and prevent future developers (or future you) from reinventing the wheel on something you've already implemented a few times.

If you'd like to see the full code, here it is on codesandbox. Feel free to fork it and play around, I'd love to see what you come up with!

Discussion (1)

Collapse
kelseyleftwich profile image
Kelsey Leftwich

I like how you point out you can use the return values differently!