DEV Community

Cover image for Let's Talk About Hooks - Part 3 (useCallback and useRef)
Atif Aiman
Atif Aiman

Posted on

Let's Talk About Hooks - Part 3 (useCallback and useRef)

Salam, and well, hello there!

Obi Wan Kenobi saying "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:

  1. useRef - The Reference Hook For Unmonitored Things
  2. useCallback - The Next Level of Your Callback Function!
  3. The Difference Between useMemo And useCallback
  4. The Misconception of useEffect, useMemo And useCallback
  5. 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);
Enter fullscreen mode Exit fullscreen mode

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} />
);
Enter fullscreen mode Exit fullscreen mode

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} />
);
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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 click updateState
  • I click updateVar and then I click updateRef
  • I click updateRef and then I click updateState

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);
Enter fullscreen mode Exit fullscreen mode

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} />
);
Enter fullscreen mode Exit fullscreen mode

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} />;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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");
}, [];
Enter fullscreen mode Exit fullscreen mode

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.

The High Ground Meme

Well, until next time, keep yourself at the high ground at all times, and peace be upon ya!

Latest comments (1)

Collapse
 
tsimmz profile image
Tyler Simoni

The description of functionality for the useCallback hook makes it seem almost like a debounce function, which is not the case.

The useCallback hook caches the function passed in as a callback and uses the dependency array to check to see if the function needs redefined in order to have access to updated reactive variables used in the callback function. If there are no changes to the dependency array, the function will still be able to be called, it just will call it with potentially stale data.

This is helpful for passing functions as props to children components, especially those that update parent component state, to prevent unnecessary re-renders if a parent re-renders.

React Docs - useCallback