Skip to main content

Command Palette

Search for a command to run...

A Look At React Hooks: useCallback

Introduction to the useCallback Hook

Updated
A Look At React Hooks: useCallback
V

I'm a solutions engineer lead, GitHub Star, Director of WomenDevsSG, and co founder of ragTech. I work at the intersection of tech, systems, and leadership, and this blog is where I share my journey through all three. Expect honest reflections, real experiences, and thoughts that are still forming rather than polished career advice.

Welcome to A Look at React Hooks, a beginner-friendly series on React Hooks. In this article, let's look at the useCallback Hook.

What is useCallback?

At a glance, it seems that this Hook is pretty straightforward. It accepts 2 arguments: a callback function and an array of dependencies. It returns a memoized callback function when any value in the dependency array has changed.

In code, it looks like (from React Hooks docs):

const memoizedCallback = useCallback(
  () => {doSomething(a, b);}, //callback function
  [a, b], //dependency array
);

Memoization

The word 'memoized' or 'memoization' is an optimization technique to speed up expensive function calls by returning cached results instead of re-computing if the inputs of the function are the same.

For this Hook, understanding why we need to memoize a callback function is essential, so we know when to use it. So let illustrate that with a simple example.

An Example

Take a look at this simple app. It consists of 3 components, Todo, Number and Counter.

Capture.PNG

The Todo component displays items that were passed from props. The Number component displays the current value of number while the Counter component is responsible for the 2 buttons that can change the value of number. These 3 components are wrapped inside App.js as shown below.

function App() {
  console.log("Render App");
  //init items for Todo
  const [items, setItems] = useState([
    "1. Some todo",
    "2. Some todo",
    "3. Some todo"
  ]);
  //init number for Number and Counter
  const [number, setNumber] = useState(0);
  //add items for Todo
  const add = () => {
    setItems(() => [...items, "New todo"]);
  };
  //handler function for Counter
  const increase = () => {
    setNumber(number + 1);
  };
  //handler function for Counter
  const decrease = () => {
    setNumber(number - 1);
  };
  return (
    <div>
      <Todo items={items} add={add} />
      <Number number={number} />
      <Counter incr={increase} decr={decrease} />
    </div>
  );
}

As seen in the code, all the states and functions are within App.js while the 3 child components are merely acting as separate containers for this example.

In each component, we have a console.log statement to track when they are rendered.

In todo.js

function Todo(props) {
  console.log("Render Todo");
  return (
    <div>
      <h2>My Todo</h2>
      {props.items.map((item) => {
        return <p>{item}</p>;
      })}
      <button onClick={props.add}>Add Todo</button>
    </div>
  );
}

In number.js

function Number(props) {
  console.log("Render Number");
  return (
    <div>
      <h2>Counter</h2>
      <p>The number is: {props.number}</p>
    </div>
  );
}

In counter.js

function Counter(props) {
  console.log("Render Counter");
  return (
    <div>
      <button onClick={props.incr}>Increase</button>
      <button onClick={props.decr}>Decrease</button>
    </div>
  );
}

If we run our app, notice in the clip below that the console logs when a component is re-rendered. If we only click the Counter buttons and update the number value, the Todo component also re-renders along with Number and Counter. It is unnecessary for Todo to re-render if its values stay the same.

pure.gif

It goes for the same when an item is added to the Todo component and the items array gets updated. Both Counter and Number gets unnecessarily re-rendered even though they have nothing to do with items.

This can be an issue if a component is very huge and needs to load over a hundred items on its list. If it keeps being re-rendered even when its items do not change, it can cause performance issues in the app. There is no need to re-render the components that unrelated to a particular state update.

Solution

React.memo() is a built-in React feature that renders a memoized component and skip unnecessary re-rendering. So each component will only re-render if it detects a change in their props.

So, we can wrap the component with React.memo() in its export line:

export default React.memo(Component);

Wrap React.memo around the Todo, Number and Counter components. At this point, you might think: Great! That should be all. But let's see the app in action:

onlymemo.gif

Uh-oh! Why is it still rendering unnecessary components? When the 'Add Todo' button is clicked, we expect only App and Todo to re-render. Instead, we get App, Todo and Counter, but Number is behaving correctly.

Why is this so?

Let's go back and see how App returns the 3 components.

  return (
    <div>
      <Todo items={items} add={add} />
      <Number number={number} />
      <Counter incr={increase} decr={decrease} />
    </div>
  );

As you can see, the 3 functions written in App: add, increase and decrease are passed as props in Todo and Counter. Notice that only the value number is passed into Number as props.

In React, whenever a component re-renders, a new instance of the function in it gets generated. Therefore, every time App renders, add, increase and decrease are re-created. So their references now points to different functions in memory. Hence, in terms of referential equality, the functions before re-render are not the same as the functions after the re-render. See diagram below to visualize.

re.png

As a result, when 'Add Todo' button is clicked:

  1. App gets re-rendered.
  2. The items array and the references for add, increase and decrease gets updated.
  3. React.memo accounts for these changes and re-renders the components with items, add, increase and decrease as their props.

On the other hand, Number does not get re-rendered when 'Add Todo' button is clicked because there is no change to the number prop.

So how do we prevent the reference of the functions the same?

useCallback to the rescue

As previously mentioned, the Hook takes a callback function as its argument and a dependency array as its second. To solve the issue in our example, we simply need to wrap our handler functions in App.js: add, increase and decrease inside the Hook. This prevents the unnecessary re-rendering behaviour because it ensures the same callback function reference is returned when there is no change in their dependency.

For example, let's edit the add function first.

 //add items for Todo - before
 const add = () => {
    setItems(() => [...items, "New todo"]);
 };
 //add items for Todo - after
 const add = useCallback(() => {
    setItems(() => [...items, "New todo"]);
 }, [items])

Now, the function add will only be updated if items change. Do the same for increase and decrease functions. These functions should only be updated when number changes. They should look like:

const increase = useCallback(() => {
    setNumber(number + 1);
}, [number]);

const decrease = useCallback(() => {
    setNumber(number - 1);
}, [number]);

Result

solved.gif

Yay! Now only the relevant components will re-render. add only result in Todo updating and re-rendering. And increase or decrease will re-render Number and Counter components.

3.png

Conclusion

And that's the gist of this Hook! Thanks for reading this article. I hope it was helpful for React beginners. Please feel free to ask questions in the comments below. Ultimately, practising and building projects with this Hook will help anyone to pick it up faster.

The next Hook in this series will be: useMemo. It will be the last basic Hook in this series. After that, this series will continue for advanced/custom Hooks. Stay tuned and cheers!


Resources

  • https://reactjs.org/docs/hooks-reference.html#usecallback
  • https://reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render
N

Great source for learning useCallback! well done!

1
S

Such a great and easy to understand article Victoria! Thank you for that! One question, how did developers handle this before hooks?

G

If I use contexts in any project , then we are not passing a props, so there will be no need to to use callbacks right, as the component will only re-render the component if the state value which it uses changed. Am I right?

T

That's the article I needed! Very well explained, thanks Victoria Lo :)

F

Interesting hook! Well this is my first time seeing this hook in action. I'll keep note of what it does for future use.

1
B

Increase and Decrease anyway are not getting called again and again when the component gets rerendered then why did you use useCallback ?

1
V

Hi thanks for the question. Every time a React component re-renders, a new function gets created. So without the useCallback, new increase and decrease functions are created every time the App component renders.

This is known as referential equality. The increase and decrease functions that loads on every render does not point to the same functions in memory (even though they have the same name, logic, etc.).

Using useCallback tells React to cache the increase and decrease functions if number does not change. When number is changed, the function is then updated.

So, only using React.memo() will still result in unnecessary components rendering because the increase and decrease functions passed as props in Counter are returned as new functions each time app renders. The React.memo(Counter) component detects a change in the component's props and therefore will re-render it. onlymemo.gif

I hope that explains it. I will edit the article to make it more clear. Thanks!

3
B

What will happen if I use React.memo() only and not useCallback

E

This was super useful to me Victoria Lo.

1
C

Great article as always, Victoria Lo! Well done! 👏

1