ReactT2_1200x303

Learn how to use the useCallback hook to avoid unnecessary re-renders in our application, and the useRef hook to keep track of references.

In this article, we’re going to learn more about two specific React hooks that were released in the React 16.8 version: the useCallback hook and the useRef hook. We’ll understand more about how these two specific hooks work under the hood, the right use cases for each of them and how we can benefit from them in our real applications.

No More Classes

Back in October 2018, the React team released a version of React that we can now safely say was one of the most important releases in the short history of React. They released a new feature called React Hooks—a new way that we can use to manage our state application very easily, removing the classes from our components, so we can have more concise code and split our state logic.

In our React applications before React Hooks, to manage our state we would have class components. For example, if we wanted to create a state to have a counter, this is how we would do it:

  1. First, we would create our component, and our state would be a simple counter.
class App extends Component {
 constructor(props) {
   super(props);
   this.state = {
     counter: 0
   };
 }
 render() {
   return (
     <div>
       <h1>counter: {this.state.counter}</h1>
     </div>
   );
 }
}
  1. Then, we would create two functions: one to increment the counter and other to decrement the counter.
incrementCounter = () => {
 this.setState(prevState => {
   return {
     counter: prevState.counter + 1
   };
 });
};
decrementCounter = () => {
 this.setState(prevState => {
   return {
     counter: prevState.counter - 1
   };
 });
};
  1. After that, we would create two buttons that would trigger each function, and increment or decrement our counter depending on the button.
<button onClick={this.incrementCounter}>+</button>
<button onClick={this.decrementCounter}>-</button>

A lot of people were against this approach of having to create classes to deal with our state in React. They were in favor of something classier and cleaner. The solution that the React team found for it? React Hooks.

With React Hooks we can replace all our class components in our applications with functional components, which means: no more class components! We’re now able to use function components in our applications without having to create a single class component to manage our state.

The hook that we use to manage our state is the useState hook. First, we import the useState hook from React.

import React, { useState } from "react";

The useState hook takes an initial state as an argument, and it returns an array with two elements: the state and the updater function.

const [counter, setCounter] = useState(0); 

So, now, all we have to do is call the setCounter updater function to update our counter state. Magic!

import React, { useState } from "react";
const App = () => {
 const [counter, setCounter] = useState(0);
 return (
   <div>
     <h1>counter: {counter}</h1>
     <button onClick={() => setCounter(counter + 1)}>+</button>
     <button onClick={() => setCounter(counter - 1)}>-</button>
   </div>
 );
};

This is a quick recap of React Hooks. If you want to learn more about them, I’d really recommend you read the documentation and practice.

Now that we've covered the background of React Hooks, let's learn specifically about the useCallback and useRef hooks, which were released in the original 16.8 set. 

useCallback

The useCallback hook has a primary and specific function: avoid unnecessary re-renders in your code, making your application faster and more efficient.

The useCallback hook receives a function as a parameter, and also an array of dependencies. The useCallback hook will return a memoized version of the callback, and it’ll only be changed if one of the dependencies has changed.

useCallback(() => {
 myCallbackFunction()
}, [dependencies]);

You can also pass an empty array of dependencies. This will execute the function only once. If you don’t pass an array, this will return a new value on every call.

useCallback(() => {
 myCallbackFunction()
}, []);

Let’s create an example so we can understand more easily how this hook works. We’re going to create a component called Notes, which will be our parent component. This component will have a state called notes, which will be all our notes, and a function called addNote that will add a random note every time we click a button.

const Notes = () => {
 const [notes, setNotes] = useState([]);
 const addNote = () => {
   const newNote = "random";
   setNotes(n => [...n, newNote]);
 };
 return (
   <div>
   <h1>Button:</h1>
   {notes.map((note, index) => (
     <p key={index}>{note}</p>
   ))}
   </div>
 );
};

Now, let’s create our Button component. We’re going to create a simple button and pass a prop called addNote that will add a note every time we click it. We put a console.log inside our Button component, so every time our component re-renders it’ll console it.

const Button = ({ addNote }) => {
 console.log("Button re-rendered :( ");
 return (
   <div>
   <button onClick={addNote}>Add</button>
   </div>
 );
};

Let’s import our Button component and pass our addNote function as a prop and try to add a note. We can see that we can add a note successfully, but also our Button component re-renders every time, and it shouldn’t. The only thing that’s changing in our app is the notes state, not the Button.

This is a totally unnecessary re-render in our application, and this is what the useCallback hook can help us avoid. So, in this case, how we could use the useCallback hook to avoid an unnecessary re-render in our component?

We can wrap the addNote function with the useCallback hook, and pass as a dependency the setNotes updater function, because the only thing that’s a dependency of our Button component is the setNotes.

const addNote = useCallback(() => {
 const newNote = "random";
 setNotes(n => [...n, newNote]);
}, [setNotes]);

But if we look at the console, we can see that the Button component is still re-rendering.

We know that React will re-render every component by default unless we use something that can prevent this. In this case, we can use the React.memo to prevent re-rendering our Button component unless a prop has changed—in our case, the addNote prop. But, since we’re using the useCallback hook, it’ll never change, so our Button component will never be re-rendered. This is how our Button will look:

const Button = React.memo(({ addNote }) => {
 console.log("Button re-rendered :( ");
return (
   <div>
   <button onClick={addNote}>Add</button>
   </div>
 );
});

Now we have a very performative and effective component, avoiding unnecessary re-renders in our components. The useCallbackhook is pretty simple at first, but you must pay attention to where and when to use this hook, otherwise it won’t help you at all.

Now that we learned about the useCallback hook, let’s take a look at another hook that can help you a lot in your projects and applications: the useRef hook.

useRef

If you used class components before the React 16.8 version, you know that this is how we would create a reference to a component or an element:

class Button extends React.Component {
constructor(props) {
  super(props);
  this.buttonRef = React.createRef();
}
render() {
  return (
    <button ref={this.buttonRef}>
      {this.props.children}
    </button>
  )
}
}

Import the createRef method from React, and pass it to the element that you want. Pretty simple.

But, now, we can do everything that we were doing with class components, with functional components! We can now manage our state logic inside a functional component, we can have “lifecycle methods” and we can create references and pass them to elements by using the useRef hook.

The useRef hook allows us to return a mutable ref object (a DOM node or element created in the render method).

import React, { useRef } from "react";
const Button = ({ children }) => {
 const buttonRef = useRef();
 return (
   <button ref={buttonRef}>{children}</button>
 )
}

But, what’s the difference between the createRef and the useRef? Well, pretty simple: the createRef hook creates a new reference every time it renders, and the useRef hook will return the same reference each time.

We learned a few minutes ago that an unnecessary re-render is something that we want to avoid in our application—that’s why we should use the useRef hook instead of createRef. Migrating from one to another will not be that hard, and the useRef will improve your life a lot.

The useRef hook holds the actual value in its .currrent method. With this method, we can access the actual HTML element, in our case, a button. By using the .currrent method, we can do some things and change HTML elements imperatively using some node instances, such as .focus, .contains, .cloneNode, etc.

Let’s imagine that we have an input and a button, and we want to focus the input every time we click the button. This can be very helpful in some forms situations that you have in your application How would we do that?

Well, we could create a reference using the useRef hook, and change the .currrent of that reference to focus the input every time we click the button, by using the .focus node instance.

import React, { useRef } from "react";
const App = () => {
 const inputRef = useRef();
 const focusInput = () => {
   inputRef.current.focus();
 };
 return (
   <div>
     <input type="text" ref={inputRef} />
     <button onClick={() => focusInput()}>Focus</button>
   </div>
 );
};

The useRef hook is also very useful if we want to save some value inside it—for example, our state value.

Let’s imagine that we have a counter, and every time we increment or decrement that specific counter, we can store the value of the counter inside the ref. We can do this by using the .currrent method. This is how we would do it:

import React, { useRef, useState } from "react";
const App = () => {
 const [counter, setCounter] = useState(0);
 const counterRef = useRef(counter);
 const incrementCounter = () => {
   setCounter(counter => counter + 1);
   counterRef.current = counter;
 }
 const decrementCounter = () => {
   setCounter(counter => counter - 1);
   counterRef.current = counter;
 }
 return (
 <div>
   <h1>Counter state: {counter}</h1>
   <h1>Counter ref: {counter}</h1>
   <button onClick={() => incrementCounter()}>+</button>
   <button onClick={() => decrementCounter()}>-</button>
 </div>
 );
};

You can notice that every time we change the counter, incrementing or decrementing, we’re using the .currrent method to save the value. This way we can use it in the future if we want to.

useRefs in your React application are amazing, but at the same time they can be very tricky. Here you can find a list of all HTML node instances that we can use with refs.

If you want to learn more about React Hooks, we’ve got a lot of hooks that might be interesting to learn and understand their specific use cases. Such as the useContext hook, a way that we can pass data throughout our components without having to manually pass props down through multiple levels. Or the useEffect hook, very similar to the useCallback hook, but instead of returning a memoized callback, it returns a memoized value. And we can use the useEffect hook to perform lifecycle methods in our functional components.

Conclusion

In this article, we learned more about the useRef and the useCallback hooks, two of the hooks that were released in React 16.8. We learned how to use the useCallback hook to avoid unnecessary re-renders in our code, avoiding a lot of re-renders and compromising the user’s experience in our application. We learned that the useRef hook lets us return a mutable ref object, holding a value in the .current method; and by using this method we can do some nice things such as focus elements, create and compare element nodes, etc.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.