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;
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);
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})`
});
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);
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";
}
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>
);
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:
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";
}
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...?
};
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>
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
};
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>
);
}
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!
Top comments (2)
Why create one when you can get all awesome hooks in a single library?
Try scriptkavi/hooks. Copy paste style and easy to integrate with its own CLI
I like how you point out you can use the return values differently!