When you are building a component library, or working on a large project, one of the issues you will run into is having too much logic in your components.
If your component starts to look like this, it’s time to create some custom hooks:
export function Card(props) {
const [ isOpen, setIsOpen ] = useState(false)
const [ isFocused, setIsFocused] = useState(false)
useEffect( () => {
// do something on load
},[]);
useEffect( () => {
// do something when className changes
},[props.className]);
...more state or effects
}
If you have multiple components using state and effects in a similar way, you should pull those out into a custom hook. This allows you to reuse that code in a maintainable way.
Merge ClassNames Hook Example
Let’s look at a simple Card component:
export function Card(props) {
return (
<div>{props.children}</div>
)
}
I’ve kept this really simple so we just focus on the hooks. Let’s say you want to use a default class of my-card, as well as allow the Card to set a custom class.
export function Card(props) {
return (
<div
className={`my-card ${props.className}`}>
{props.children}
</div>
)
}
Easy enough.
What if we want to allow removing the default class?
export function Card(props) {
return (
<div
className={props.removeDefaultClass ?
props.className :
`my-card ${props.className}`}>
{props.children}
</div>
)
}
This code works, but it has some problems. It adds an unnecessary space, it’s not very readable, and it won’t handle changes easily.
We can use a library like clsx to clean this up a bit, and even extract the logic out to a function.
import clsx from 'clsx';
export function Card(props) {
const classes = () => {
return clsx(props.removeDefaultClass ?
props.className :
`my-card ${props.className}`);
}
return (
<div className={classes}>{props.children}</div>
)
}
This is better, but still not ideal.
Using clsx allows you to use logic to apply multiple classNames, and make sure there’s no extra whitespace. We still need to copy/paste this function to every component, and if we ever need to make changes, we have to do that in every file.
Let’s move it to a hook instead.
// ./hooks/useMergeClasses.js
import clsx from 'clsx';
export function useMergeClasses(defaultClasses, propsClasses, removeDefaultClass) {
if( removeDefaultClass ) {
return propsClasses;
}
return clsx(defaultClasses, propsClasses);
}
This is much better. It’s readable, reusable, and maintainable.
A custom React hook is just a function that starts with the word “use”. There are some rules you should be aware of:
- You can use other hooks inside a hook
- The hook has to be used at the top level of a React function
- You cannot use a hook nested in another function or conditional
Besides those rules, you can think of hooks as normal functions.
Now we can use this custom hook in our Card component like this:
import { useMergeClassses } from './hooks/useMergeClasses.js';
export function Card(props) {
const classes = useMergeClasses('my-card', props.className, props.removeDefaultClass);
return (
<div className={classes}>{props.children}</div>
)
}
Now if you need to make a change, you can do it in one file. You can also use this hook in other projects.
Now do it with all your logic.
The logic here is a simple example, but what about when the logic is more complex?
This same principle applies to any logic you need that can be reusable, like boolean based state (open/closed state for modals and disclosures), dealing with API data, event listeners, and lots more. There are even React hook libraries with code you can copy into your project.
Not everything can be extracted to a hook, but much of it can be if you write your components in a way that focuses on reusability.
By extracting our logic into hooks, we make our library or project logic reusable and maintainable. Hook all the things!