Development

Creating Custom React Hooks with useHug

Building custom React Hooks is a great way to encapsulate behaviors and reuse them throughout your application. Learn how huggable behaviors can help.

7 min
February 14, 2022
Chris Held
Development Lead

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)

Get started

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

   -- CODE line-numbers language-javascript --
   <!--
     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;
   -->

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

   -- CODE line-numbers language-javascript --
   <!--
     const [hovering, setHovering] = useState(false);
     const [pressed, setPressed] = useState(false);
   -->

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

   -- CODE line-numbers language-javascript --
   <!--
     const animationProps = useSpring({
       transform: `scale(${pressed ? 0.8 : 1})`
     });
   -->

Leveraging useSpring for animation

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.

   -- CODE line-numbers language-javascript --
   <!--
     const onMouseEnter = () => setHovering(true);
     const onMouseLeave = () => {
       setHovering(false);
       setPressed(false);
     };
     const onMouseDown = () => setPressed(true);
     const onMouseUp = () => setPressed(false);
   -->

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

   -- CODE line-numbers language-javascript --
   <!--
     let className = "huggable";

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

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.

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

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.

What we've got so far

![huggable animation in action](https://assets-global.website-files.com/5d7678efce0c5793e8bc3be5/620be0e83939f960e15bde40_custom-react-hook-example-animated.gif)

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.

   -- CODE line-numbers language-javascript --
   <!--
     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";
     }
   -->

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

   -- CODE line-numbers language-javascript --
   <!--
     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...?
     };
   -->

What we want to return

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:

   -- CODE line-numbers language-javascript --
   <!--
     const huggableProps = useHug();

     return <a href="/contact" {...huggableProps}>Contact Us</a>
   -->

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:

   -- CODE line-numbers language-javascript --
   <!--
     return {
       onMouseDown,
       onMouseEnter,
       onMouseLeave,
       onMouseUp,
       className,
       style
     };
   -->

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

   -- CODE line-numbers language-javascript --
   <!--
     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>
       );
     }
   -->

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.

Key takeaways

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.

Access on CodeSandbox

If you'd like to see the full code, [here it is on codesandbox](https://codesandbox.io/s/huggable-qt088?file=/src/App.js).

Feel free to fork it and play around, I'd love to see what you come up with!

More React resources

Building SVG Line Charts in React

Intro to React Testing with Jest and React Testing Library

React: Optimize Components with React.memo, useMemo, and useCallback

Intro to Styling in React JavaScript

React Prototyping with Apollo Client Schemas

Actionable UX audit kit

  • Guide with Checklist
  • UX Audit Template for Figma
  • UX Audit Report Template for Figma
  • Walkthrough Video
By filling out this form you agree to receive our super helpful design newsletter and announcements from the Headway design crew.

Create better products in just 10 minutes per week

Learn how to launch and grow products less chaos.

See what our crew shares inside our private slack channels to stay on top of industry trends.

By filling out this form you agree to receive a super helpful weekly newsletter and announcements from the Headway crew.