How to avoid running effects on every render in React (useRefVariable hook).
If you have built a reusable React component before, chances are you’ve noticed that it is easy to arrive at the land of unnecessary optimizations. Your component might require a callback to be passed and if this callback is used inside a useEffect
, then you might end with an effect that is executed on every render.
// pseudocode
function MyReusableComponent({ onDoubleTap }) {
React.useEffect(() => {
createDoubleTapListener(onDoubleTap)
return () => removeDoubleTapListener(onDoubleTap)
}, [
/* This is the important part */
onDoubleTap
])
return ...
}
As you can see in the example above, if a user passes an arrow function to this component, it will cause the effect to run on every render which will remove the listener and add a new one:
<MyReusableComponent onDoubleTap={() => setState(...)} />
Normally this is okay but in some rare cases this is less than ideal. Let’s say your listener executes often, or you are polling an API and must recreate the polling mechanism on every render, or even if your listener is heavy to create so you want to create it once!
How can we avoid this?
There are two ways to solve this. Either you leave it to the parent component to useCallback
when noticing side effects or lag (which requires lots of debugging and could be noticeable only to a handful of users). Or you prevent the effect from running with the help of useRef
.
Let’s create a custom hook that will maintain an always updated reference to the callback and we can use inside the effect:
function useRefVariable(value) {
const ref = useRef()
ref.current = value
return ref
}
This custom hook will hold a reference and update the value of that reference on every render, so we can update your custom component to never have to create the listener again:
// pseudocode
function MyReusableComponent({ onDoubleTap }) {
const onDoubleTapRef = useRefVariable(onDoubleTap)
React.useEffect(() => {
const listener = (...args) => onDoubleTapRef.current(...args)
createDoubleTapListener(listener)
return () => removeDoubleTapListener(listener)
}, [
onDoubleTapRef
])
return ...
}
As you know, the reference will remain the same throughout the lifecycle of the component (until unmounted) and the value of that reference will forever remain up to date, thanks to our custom hook.
This little 4-liner hook prevents running effects on every render and best of all, avoids unnecessary optimizations on the parent component(s).