Let's Talk About Hooks - Part 3 (useCallback and useRef)
Atif Aiman
Posted on July 30, 2022
Salam, and well, hello there!
We are now in the third series of the React Hook series, and it is time for the next 2 hooks, which are useCallback
and useRef
!
These two hooks are the hook that I use most other than useState
and useEffect
, so you might need to keep in mind that you might, as well, use these hooks to do wonders in your app.
So, in this article, these are the topics that I will cover:
useRef
- The Reference Hook For Unmonitored ThingsuseCallback
- The Next Level of Your Callback Function!- The Difference Between
useMemo
AnduseCallback
- The Misconception of
useEffect
,useMemo
AnduseCallback
- Conclusion
Well, time to get going!
useRef
- The Reference Hook For Unmonitored Things
Before we jump to the way how useRef
works, let's just do some revision on what is ref
, by the way.
Refs provide a way to access DOM nodes or React elements created in the render method. - React Documentation
So, to access your DOM elements, let's say, your <div>
component, you pass your component to the ref, so you don't have to do something like document.getElementById()
or something similar. Plus, using ref
helps you to keep track of the components to do a lot of things, like programmatically styling the components, or extracting the form's values.
Don't get me wrong here. I wouldn't say that document.getElementById()
shouldn't be used, in fact, I advise you to actually learn how to use them, so you can also understand how ref
simplifies things in React.
So, how is the syntax, you ask? Well, look below!
const theRef = useRef(initialValue);
Yes, it is that simple. The hook only needs one parameter, which is the initial value. Hmmmm, it should be the component, right?
Well, before mounting, your component isn't there yet, but later on, the component will be mounted and ready to be referred. So, useRef
will handle this hassle, and update with the component that you will bind later on.
But then, initialization will always be undefined? Hmmm, about that, I will get back to this to explain first how to use useRef
fully, and then we will get back to this question.
So, useRef
will return the ref
, which is the thing that you want to refer to. How can I bind this to the component?
const theRef = useRef();
return (
<div ref={theRef} />
);
In your component, you can pass ref
props to any of the HTML components, and then pass the created ref
to the prop. So, if you console the value of theRef
later on, you will get the component object, and from there, you can do a lot of things, such as theRef.target.classList.add('force')
!
But bear this in mind! Ref is not something monitored by React lifecycle. That means, the ref is not affected by rerenders at all, but instead only affected by the changes of the ref
itself. So, that means, we can update the ref too? The answer is yes! As much as you do DOM manipulation, that is you updating the ref, but it doesn't trigger the rerender.
So, if I can update the ref without triggering the rerender, does that mean that throughout the React lifecycle, the ref
value will not be affected? Yes, it won't!
You can actually use useRef
for something other than DOM manipulation. Let's say, you want to keep track of something, maybe the number of clicks, but you don't want to trigger the rerenders, then useRef
will be a perfect hook for you! With this, initializing ref
with something will make sense.
Let's look to another example of useRef
that is not a DOM-thing.
const clickAmount = useRef(0);
const handleClick = (e) => {
e.preventDefault();
clickAmount++;
}
return (
<button onClick={handleClick} />
);
What do you think if I click the button above? The handleClick
will add 1 to clickAmount
each time. However, there will be no rerender. Yes, no rerenders!
Okay, let's add some complexity to the component.
const [theState, setTheState] = useState(0);
const clickAmount = useRef(0);
const randomThing = 0;
const handleClick = (e) => {
e.preventDefault();
clickAmount++;
}
const handleUpdateState = (e) => {
e.preventDefault();
setTheState((prevState) => prevState + 1);
}
const handleUpdateVar = (e) => {
e.preventDefault();
randomThing++;
}
return (
<div>
<button name="updateRef" onClick={handleClick} />
<button name="updateState" onClick{handleUpdateState} />
<button name="updateVar" onClick{handleUpdateVar} />
</div>
);
Whoaaa, a lot of complexity here. First, let the force calm you for a second, and let me guide you through the way.
Let us consider several cases:
- I click
updateVar
and then I clickupdateState
- I click
updateVar
and then I clickupdateRef
- I click
updateRef
and then I clickupdateState
FOR THE FIRST CASE, when I click updateVar
, the value of randomThing
will increase by 1. Then I click updateState
and theState
will increase by 1. But what do you think happened to randomThing
? The answer is, that it will reset to 0 because the component is rerendered and all variables that are not wrapped inside the hook or functions will be reset to the initial value that is assigned to the variable.
FOR THE SECOND CASE, when I click updateVar
, the value of randomThing
will increase by 1. Then I click updateRef
, and the value of clickAmount
will increase by 1. But, what do you think happened to randomThing
? The answer is, it won't change! Remember that useRef
didn't trigger the rerenders, so randomThing
will keep its value until the rerenders.
FOR THE THIRD CASE, when I click updateRef
, the value of clickAmount
will increase by 1. Then I click updateState
, and theState
will increase by 1. But, what do you think happened to clickAmount
? The answer is, that clickAmount
won't change! Yes, as I say that ref
won't be bothered by rerenders, so the clickAmount
won't be reset and it keeps the value until the component unmount.
To summarize this
- State will always trigger rerenders.
- Declared variables inside the component, that are not wrapped in hooks or functions, will always be reset during rerenders.
- Ref, on the other hand, will keep the value, even after the rerenders since
ref
is not affected by the rerenders. Unless unmounting happens, then all inside components become non-existent, including refs.
Sheesh, after the long explanation of useRef
, let's dive into useCallback
. Brace yourself for yet another long explanation 🥶
useCallback
- The Next Level of Your Callback Function!
Let's get knowledge of what callback is!
A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action. -MDN Web Docs
As you can see, a callback function is indeed, just another kind of function. The way of writing is the same, it is just how you use the function.
const useForce = () => {
// Do anything you want
}
const approachEnemy = (callback) => {
// Do what you need to
callback();
}
approachEnemy(useForce);
The first function, useForce
is the function for when you will use the force to do things. And the second function, approachEnemy
is the function for when you want to approach the enemy. If you noticed, I passed useForce
inside approachEnemy
so that means that I will useForce
every time I approachEnemy
. useForce
is what we call the callback function. With this way of writing function, we can change what we want to pass to the first function, providing flexibility to the first function. With this, instead of useForce
, I can instead useLightning
to approachEnemy
too! 😈
Well, if you ever passed a function as a prop to a component, that is a callback as well!
const CompParent = () => {
const myFn = () => {}
return <CompChild onFunc={myFn} />
}
const CompChild = ({ onFunc }) => (
<button onClick={onFunc} />
);
But, of course, adding events and all sorts of things makes it different, but passing a function as a parameter is considered a callback function. I hope you get the idea!
Oooooookay, back to the topic. So, for a callback function, it matters when you want to trigger the function. Let's say if I pass a callback function, when do I want it to trigger? You can put it anywhere in the function to call the passed callback, but it might as well be complex when you throw something else in the mix, like loops and conditionals.
Going back to the React topic, we are usually writing the functions to handle things, like handling events, triggering API, or maybe your DOM manipulations like focusing and blurring elements.
const handleClick = (e) => {
e.preventDefault();
};
return <button onClick={handleClick} />;
Do you know, that onClick
is an event function that triggers when the user clicks the element? Passing a function to the onClick
only means that handleClick
is a callback function. handleClick
won't trigger, unless the onClick
function is triggered. But doing this way, every time you click the button, the function will be triggered.
Let's get to the more complex component!
const [anakinSide, setAnakinSide] = useState('jedi');
const announceSide = () => {
console.log(`I am now a ${anakinSide}`);
};
return (
<div>
<button onClick={announceSide} />
<button onClick={() => setAnakinSide('sith')} />
</div>
);
So, for this case, I would like to announce which side Anakin is currently on when I click the button. And then, I create another button to change Anakin's side. But just imagine, it must be annoying if I keep telling you the same thing a thousand times that Anakin is a jedi, when you know he didn't change side yet, unless he is! So, I would like to only announce Anakin's side, only when there is a change in Anakin's side.
To do this, useCallback
will serve its purpose!
const [anakinSide, setAnakinSide] = useState('jedi');
const announceSide = useCallback(() => {
console.log(`I am now a ${anakinSide}`);
}, [anakinSide]);
return (
<div>
<button onClick={announceSide} />
<button onClick={() => setAnakinSide('sith')} />
</div>
);
Now, I wrapped announceSide
function with a useCallback
hook, and I passed a dependency, which is anakinSide
. When this happens, every time you click the button to announce which side is Anakin is on, it will check anakinSide
cache to see if there is any changes to the previous change. If there is no changes, then announceSide
won't trigger! That means, the component will only announce when Anakin changes side, despite many attempts to do announcement. So, let's see how callback is written!
const theFn = useCallback(callback, [arrayOfDependencies]);
So, only two things that you need to pass to the useCallback
hooks, which are the callback function, and the array of dependencies. When there is any changes to any of the dependencies, the callback will be triggered.
Well, this hooks sound similar to what you read before? 🤔
The Difference Between useMemo
And useCallback
As you guessed, useMemo
and useCallback
indeed has 100% similar structure of using the hook. However, there are some points that you need to pay attention to.
First, useCallback
should be used for, as you guessed, the callback function. That means, the purpose is to run the function, but it will try to memoize the function based on the dependencies. While useMemo
memoize not just the dependencies, but the value itself.
To put it into context, let's dive into the following examples.
const saberColorOptions = useMemo(() => {
return ["blue", "green", "purple", "red"];
}, []);
const shoutLikeChewbacca = () => useCallback(() => {
alert("roarrrrrrr");
}, [];
For useMemo
example, I declared saberColorOptions
that returns the array. Although I didn't put any dependency, useMemo
will always cache the value. You can say that useMemo
will "keep his eye on" the value of saberColorOptions
if there is any change. So, saberColorOptions
' value won't change, despite thousands of rerenders triggered.
For useCallback
example, I create a function called shoutLikeChewbacca
. If I passed the function to another function as a callback, it will always run once, since I didn't pass any dependency. So, it keeps the cache of the function, and not the value.
useMemo
is used to assign value and keep cache, while useCallback
is to cache the dependency to run the function.
The Misconception of useEffect
, useMemo
And useCallback
These three hooks require 2 things, which is a function, and array of dependencies. So, I would understand the difference between useMemo
and useCallback
, but now useEffect
?
You need to know that useEffect
is a hook that shaped based on component lifecycles. It will always trigger during the rerenders, while it meets the change of one of the dependencies. While useMemo
and useCallback
is NOT dependent to component lifecycles, but rather the cache. That means the rerenders doesn't affect the hook, but instead the changes of the dependencies. This might look the same at first, but let me give an example.
Let's say I have a state called warshipName
. If I trigger the state setters, I will trigger the rerenders. useEffect
which contains warship
as a dependency will be triggered, whether warshipName
changes value or not, as long as the state setters is triggered. useMemo
and useCallback
on the other hand, monitor its cache instead, so they will only be triggered if warshipName
value changes.
Other than that, since useEffect
is based on component lifecycles, it is understandable that useEffect
is one of the most common hook used to handle effects after rerenders. However, useMemo
and useCallback
will create a cache that monitors the value of all dependencies. Which means, that using useMemo
and useCallback
ALWAYS come with a cost. Use useMemo
and useCallback
when there is necessity or when it involves some certain of complexity. The example given is actually quite simple, where it is better if you get rid of the hooks altogether, and just use a simple callback function instead. Like I said in previous article on useMemo
, only use the hooks when it uses a lot of resources, so you won't have to repetitively execute the same function only when necessary.
Conclusion
Yeah, I have covered 6 hooks at this point, and there are still a lot of hooks provided by React for your perusal. And throughout my experiences, I keep studying on how people use these hooks to create their own hook. In my early years, I was so naive to try to optimise everything using hooks, but little did I know that I did it backward most of the time. Well, the learning process never stop!
My take is, memoization doesn't equal to performance. Memoization on simple things often jeopardise performance more than it shouldn't. At one phase, you wish that you can abstract a lot of things, just to realise you make things more complicated and slows down the performance.
However, never falter, for these hooks do not exist for no reason, it is just you need to really know when to actually use it! Just learn how to use it, apply it in your personal projects, and see how it actually is in action, so you already have a hook when the time comes.
Well, until next time, keep yourself at the high ground at all times, and peace be upon ya!
Posted on July 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.