I’m hooked on Hooks
Using Hooks in React is the standard way of writing React in 2023. The old ways do still work. You can still write class components. Heck, you can even still write React without JSX. You can still use a horse and buggy to travel the Oregon Trail, though all these alternatives are equally likely to result in dysentery.
Now, let’s look at some custom hooks you should be using — and one that you should not be using.
Hooks you SHOULD Use
usePrevious
One thing we lost in the transition from lifecycle methods in class components to hooks in function components was access to the previous props. With lifecycle methods, we could use componentDidUpdate(prevProps) to receive the prevProps and compare them to this.props to possibly trigger some action. With hooks, we can use a useEffect , and add that particular prop as a dependency, but all we know is that the value changed, not what the change was. Enter usePrevious:
import { useEffect, useRef } from 'react';
export const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
What I like about this is just how simple it is; usePrevious receives some value of type T , and will return either T or undefined. First, the hook creates a ref which is just some container to hold our value in. After that, we see a useEffect. Now remember that the useEffect will run after the current render. On the initial render, this hook returns undefined , since the ref does not yet contain any value. Now that the render phase is complete, our useEffect runs and updates the ref’s value. On the next render, this will be the value that gets returned, and the new value will be placed inside the ref’s container after the render completes.
Now, let’s look at its usage below:
export const SomeComponent = (props: SomeComponentProps) => {
const previousFoo = usePrevious(props.foo);
if (previousFoo !== props.foo) {
doSomething();
}
return <div />
}
Additionally, this is not limited to just props. We can pass anything like state or even some computed value to usePrevious to keep track of the previous value.
useIsMounted
Tell me if you’ve ever seen this warning:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
No? You haven’t? Hmm, well, if not, I suppose you can skip this one if you must, but I still think it can be helpful to know.
The reason this warning occurs is that something is attempting to set the state of a component that is no longer mounted. There’s many ways this can occur such as setting a state in the callback of a timeout, setting a state after awaiting a promise to resolve, or really any number of ways. Though it can be difficult, if not impossible, to reduce these scenarios, we can at least avoid attempting to update the state if we know the component is not mounted.
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
};
This is very similar to our usePrevious hook. We create a ref to hold a boolean value. Then we have a useEffect, which again, runs after the render. Finally, we return that ref. When the useEffect runs, it will set the value to true. Note that the dependency array for the useEffect is empty. This means it’ll run on the initial render and will run the cleanup function when it un-mounts. In this hook, that cleanup function sets the value of isMounted to false.
Now, let’s look at how to use it:
export const SomeComponent = (props: SomeComponentProps) => {
const [data, setData] = useState<TData>();
const isMounted = useIsMounted();
const someFunction = useCallback(async () => {
const response = await someNetworkRequest();
if (isMounted.current) {
setData(response);
}
}, [])
return <SomeOtherComponent someFunction={someFunction} />
}
All we have to do after we await the response is check the isMounted ref to make sure our component is still mounted. If it is, then we’re free to update the state.
useIsFirstRender
Ever wanted to know if this is the first time your component has rendered? Well, have I got the hook for you! For the low, low price of just $29.99/month, you can be the proud new owner of:
const useIsFirstRender = () => {
const isFirst = useRef(true)
if (isFirst.current) {
isFirst.current = false
return true
}
return isFirst.current
}
Again, this one is very simple. We use a ref to hold a boolean that’s initially set to true. On that initial render, isFirst.current will be true, so the if block runs, and we update the value to false, and returns true . On all subsequent rerenders, isFirst.current will be false , thus not meeting the condition in the if check, and so the hooks return isFirst.current, which, again, is false.
usePageFocus
If your users are anything like me, then they can quickly lose focus. When your users lose focus, so might your app’s window. When your app’s window loses focus, you might need to trigger some logic, and when you trigger some logic, that logic might want a glass of milk. When you give a glass of milk to some logic, it then might want a cookie, and users hate having to click to accept cookies, and… uh… wait… I think I lost focus here…. Anyway… here’s a hook that can help you with tracking page focus:
export const usePageFocus = () => {
const [isFocused, setIsFocused] = useState(document.hasFocus());
const handleFocus = () => {
setIsFocused(document.hasFocus());
};
useEffect(() => {
window.addEventListener('blur', handleFocus);
window.addEventListener('focus', handleFocus);
return () => {
window.removeEventListener('blur', handleFocus);
window.removeEventListener('focus', handleFocus);
};
}, []);
return isFocused;
};
This one is very similar to our previous hooks. I’ll go a bit quicker here, but ultimately, we use a useEffect to change the state when the window blurs and when the window focuses.
Here’s an example of its usage:
export const SomeComponent = () => {
const isFocused = usePageFocus();
if (!isFocused) {
triggerIsPausedEvent(true);
}
When the page loses focus, we trigger some isPausedEvent and our app can handle it accordingly. Easy!
Hooks You Should NOT Use
Did you make it this far? No? Wait, if you didn’t make it this far, how could you even be answering my question? Ha, you couldn’t! I caught you! Lying won’t get you anywhere in life, and it will also cause problems in your app if you lie to React.
One thing you hopefully noticed in the Should Use hooks is that they all respect the rules of hooks. See here if you’re unfamiliar. The problem is that you can definitely break these rules by lying. Here’s one hook that breaks the rules and thus should be avoided.
useEffectOnce
I get it, you want to run some effect, but only once. Maybe you’re converting a class component to a function component, and that class component uses the componentDidMount lifecycle method. You may be tempted to use one of these:
const useEffectOnce = (effect: EffectCallback) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(effect, [])
}
It’ll run the effect only once and accomplish what you need, right? Maybe, but take a look at that comment. We’re disabling a lint check to make this work. This is because, as I said before, we’re breaking the rules of hooks. The dependency array must be exhaustive, meaning all dependencies must be listed. This will absolutely cause you issues using Strict Mode (see here) and, in the future, will cause you pain as the React team adds new features.
These rules are in place to ensure that if you follow them, your code will not only work today but long into the future as well. If you break the rules, you will eventually pay the price.
Here’s an easy alternative:
export const SomeComponent = () => {
const isFirstRender = useIsFirstRender();
useEffect(() => {
if (!isFirstRender) {
return;
}
yourEffectHere();
}, [allOf, your, dependencies, goHere]
});
We can use the useIsFirstRender hook in our useEffect , and if it is not the first render, simply return early.
Conclusion
Hooks are awesome and can be incredibly powerful. Just be sure to follow the rules. They’re there for a reason.
How to Use React Hooks the Right Way to Solve Common Problems was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.