Hello Sunil
react-usecallback-hook-feature-image

React useCallback Hook : A Quick Guide

In React useCallback hook is a built-in hook that optimizes the performance of functional components by memoizing callback functions.

It allows us to prevent unnecessary re-rendering of child components when the parent component re-renders.

In this blog post, we will dig deeper into useCallback hook. You will know exactly how and when to utilize the useCallback hook and make the best of its performance-enhancing capabilities.

Ready? Let’s dive in!

What is useCallback Hook?

The main purpose of React useCallback hook is to memoize functions. The main reason for this is increasing performance of your React applications.

How is this related? Every time your component re-renders it also re-creates functions that are defined inside it. Memoizing functions helps you prevent this.

🤔 What is memoization?

In programming, memoization is an optimization technique that makes applications more efficient and hence faster.

It does this by storing computation results in cache, and retrieving that same information from the cache the next time it’s needed instead of computing it again.

Memoization is a simple but powerful trick that can help speed up our code, especially when dealing with repetitive and heavy computing functions.

For example, if there is a function to add two numbers, and we give the parameter as 1 and 2 for the first time the function will add these two numbers and return 3.

But if the same inputs come again then we will return the cached value i.e 3 and not compute with the add function again.

When you memoize a function with useCallback hook that function is basically stored in cache.

Quick example. Imagine that something causes your component to re-render. Let’s say there is a change of state. Usually, this re-render would, by default, also cause React to re-create all functions defined in your component.

This may not happen with useCallback hook and memoization. When you memoize a function React may not re-create that function just because the component re-rendered.

Instead, React can skip the re-creation and return the memoized function. This can help you save resources and time and improve performance of your application.

Syntax of useCallback Hook

The syntax of useCallback hook is as follows:

const memoizedCallback = useCallback(callback, dependencies);
  • callback: The function that we want to memorize.
  • dependencies (optional): An array of dependencies that, when changed, will trigger the recreation of the memoized function. If no dependencies are provided, the memoized function will be recreated on every render.

More simplified version of useCallback hook:

const memoizedCallback = useCallback(
 () => {
   doSomething(a, b);
 },
 [a, b],
);

It looks like the useEffect hook, the first argument of useCallback is the function that you want to memoize.

The second argument is an optional array of dependencies that are used to determine whether the memoized function should be recomputed.

If any of the dependencies change, the memoized function will be re-computed; otherwise, the cached function will be returned.

Always remember the primary benefit of useCallback is that it helps to prevent unnecessary re-renders of child components.

Understanding Functions Equality Check

Before diving into useCallback hook use, let’s distinguish the problem useCallback hook solves — referential equality and function equality.

Functions in JavaScript are first-class citizens, meaning that functions here are treated like any other variable. They can be passed as arguments, returned by another function, or assigned as a value to a variable.

Also Read: Higher Order Functions in JavaScript

In short, it can do anything that an object can do in JavaScript.

Let’s write a function factory() that returns functions that sum numbers:

function factory() {
  return (a, b) => a + b;
}

const sumFunc1 = factory();
const sumFunc2 = factory();

console.log(sumFunc1(1, 2)); // => 3
console.log(sumFunc2(1, 2)); // => 3

console.log(sumFunc1 === sumFunc2); // => false
console.log(sumFunc1 === sumFunc1); // => true

sumFunc1 and sumFunc2 are functions that sum two numbers. They have been created by the factory() function.

The functions sumFunc1 and sumFunc2 share the same code source, but they are different function objects. Comparing them sumFunc1 === sumFunc2 evaluates to false.

That’s just how JavaScript objects work. An object (including a function object) equals only to itself.

But how it related to React?

When React re-renders your component, it also recreates the functions you have declared inside your component.

As we have seen already when comparing the equality of a function to another function, they will always be false. Because a function is also an object, it will only equal itself.

In other words, when React re-renders your component, it will see any functions that are declared in your component as being new functions.

This is fine most of the time, and simple functions are easy to compute and will not impact performance.

But the other times when you don’t want the function to be seen as a new function, you can rely on useCallback hook to help you out.

The Purpose of useCallback Hook

Going back to React, when a component re-renders, every function inside of the component is recreated and therefore these functions’ references change between renders.

That’s when useCallback(callback, dependencies) is helpful: given the same dependency values, the hook returns the same function instance between renderings (aka memoization).

const memoized = useCallback(() => {
   // the callback function to be memoized
 },
  // dependencies array
[]);

For Example:

import { useCallback } from 'react';

function MyComponent() {
  // handleClick is the same function object
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);

  // ...
}

handleClick variable has always the same callback function object between renderings of MyComponent.

You might be thinking, “When would I not want a function to be seen as a new function?” Well, there are certain cases when useCallback hook makes more sense:

  • You are passing the function to another component that is also memoized (useMemo)
  • Your function has an internal state it needs to remember
  • Your function is a dependency of another hook, like useEffect for example

Use Case Scenario : useCallback Hook

Let’s take a simple application as case study. This simple app consists of 3 components, Todo, Number and Counter.

react-usecallback-hook-example

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.jsx as shown below.

App.jsx

import { useState } from 'react';
import './App.css';

import Counter from './Counter';
import Number from './Number';
import Todo from './Todo';

function App() {
  console.log('Render App');

  //init items for Todo
  const [items, setItems] = useState([
    '1. Some todo',
    '2. Some todo',
    '3. Some todo',
  ]);

  //add items for Todo
  const add = () => {
    setItems(() => [...items, 'New todo']);
  };

  //init number for Number and Counter
  const [number, setNumber] = useState(0);

  //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>
  );
}

export default App;

As seen in the code, all the states and functions are within App.jsx 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.jsx

import React from 'react';

const Todo = (props) => {
  console.log('Render Todo');
  return (
    <div>
      <h2>My Todo</h2>
      {props.items.map((item, index) => {
        return <p key={index}>{item}</p>;
      })}
      <button onClick={props.add}>Add Todo</button>
    </div>
  );
};

export default Todo;

In Number.jsx

import React from 'react';

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

export default Number;

In Counter.jsx

import React from 'react';

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

export default Counter;

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.

react-usecallback-hook-example-1

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 memo() in its export line:

export default memo(Component);

Wrap memo around the Todo, Number and Counter components.

Todo.jsx

import { memo } from 'react';

const Todo = (props) => {
  console.log('Render Todo');
  return (
    <div>
      <h2>My Todo</h2>
      {props.items.map((item, index) => {
        return <p key={index}>{item}</p>;
      })}
      <button onClick={props.add}>Add Todo</button>
    </div>
  );
};

export default memo(Todo);

Number.jsx

import { memo } from 'react';

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

export default memo(Number);

Counter.jsx

import { memo } from 'react';

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

export default memo(Counter);

At this point, you might think: Great! That should be all. But let’s see the app in action:

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.

react-usecallback-hook-example-2

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.

react-usecallback-hook-referential-equality-example

As a result, when Add Todo button is clicked:

  • App gets re-rendered.
  • The items array and the references for add, increase and decrease gets updated.
  • 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.jsx: 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]);

App.jsx

import { useState, useCallback } from 'react';
import './App.css';

import Counter from './Counter';
import Number from './Number';
import Todo from './Todo';

function App() {
  console.log('Render App');

  //init items for Todo
  const [items, setItems] = useState([
    '1. Some todo',
    '2. Some todo',
    '3. Some todo',
  ]);

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

  //init number for Number and Counter
  const [number, setNumber] = useState(0);

  //handler function for Counter
  const increase = useCallback(() => {
    setNumber(number + 1);
  }, [number]);

  //handler function for Counter
  const decrease = useCallback(() => {
    setNumber(number - 1);
  }, [number]);

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

export default App;

Result:

react-usecallback-hook-example-3

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.

react-usecallback-hook-referential-equality-example-1

Conclusion

The React useCallback hook can be useful for improving performance of your apps, by storing your functions for later use, instead of re-creating them on every re-render.

This can improve re-rendering behavior and performance of heavy components.

And that’s the gist of this Hook! Thanks for reading this article.

We hope it was helpful for React beginners. Please feel free to ask questions in the comments below. Ultimately, practicing and building projects with useCallback hook will help anyone to pick it up faster.

Resource

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

Similar articles you may like

Sunil Pradhan

Hi there 👋 I am a front-end developer passionate about cutting-edge, semantic, pixel-perfect design. Writing helps me to understand things better.

Add comment

Stay Updated

Want to be notified when our article is published? Enter your email address below to be the first to know.

Sunil Pradhan

Hi there 👋 I am a front-end developer passionate about cutting-edge, semantic, pixel-perfect design. Writing helps me to understand things better.