React Hooks: useCallback Explained

·

5 min read

React Hooks: useCallback Explained

Introduction

The useCallback hook is used to optimize and memoize (cache) a callback function in React. By memoizing the callback function we can prevent unnecessary re-rendering of the child components.

In this article, we will explore how to use the useCalllback to improve the performance of our application.

Using useCallback hook

Syntex:

const cachedFn = useCallback(fn, dependencies);

// import useCallback from react
import { useCallback } from 'react';

function MyComponent(){
  const cachedFun = useCallback(
     () => {   },
     [a, b]
   );
   // ...
}

The useCallback takes two arguments:

  1. A function that we want to catch: The useCallback will return the same function during re-render if dependencies haven't changed. If any of the dependencies changed, then the useCallabck will return a new function.

  2. dependencies array: This array contains all the variables that are referenced inside the function like the state, props and all variables that are present in the component body.

    Note that the useCallback doesn't call the function that we pass as an argument, it cashes the function.

The problem that useCallback solves

In JavaScript, variables inside of a function get created when we call the functions, and they get destroyed when the function completes execution.

In each function call, variables get a different memory address. They may have the same content, but under the hood, they are different variables.

In React, every time a component is rendered, variables inside the component body are created, and when the component re-renders, all the variables are destroyed and created again.

Creating and destroying variables don't affect the component's performance. But problems arise when we pass those variables to the child components as props. As we know that a component re-render if its props change.

To solve this problem we use the useCallback, which caches the variables, and prevent the creation of variables unnecessarily at every render.

Let's understand it with an example.

import React, { useState } from "react";

export const MyCounter = () => {
  const [count, setCount] = useState(0);
  console.log("MyCounter render");

  const handleClick = () => {
    setCount((preCount) => preCount + 1);
  };

  return (
    <>
      <p> Count value is :{count}</p>
      <ChildCounter handleClick={handleClick} />
    </>
  );
};

const ChildCounter = ({ handleClick }) => {
  console.log("ChildCounter render");
  return (
    <>
      <button onClick={handleClick}> Increment</button>
    </>
  );
};

In this example, We are passing a handler function from the ParentCounter component to the ChildCounter component. In the ChildCounter component, we call this function when the Increment button is clicked.

Clicking the Increment button updates the count state variable in the ParentCounter component, and updating the count state triggers a re-render. Whenever ParentCounter re-renders, its child component, ChildCounter, also re-renders.

In React when the parent component re-renders, all children also re-render. But sometimes we don't want to re-render child components. In the above example, it is unnecessary to re-render the ChildCounter component when ParentCounter re-renders.

We can prevent this behaviour using useCallback and React.memo().

What is React.memo()?

React.memo() is a higher-order component(HOC) provided by React. It is used to optimize the performance of a functional component by memoizing its rendered output.

When a component wrapped in React.memo(), receives a new prop, React will compare the previous props with the new props. If they are the same, React will use memoized version of the component, and avoid re-rendering it. This can be beneficial when the component's rendering is computationally expensive or when re-rendering is unnecessary because the component's output would be the same.

Here's an example of how to use it:

import React from 'react';

const MyComponent = React.memo((props) => {
  // Component logic and rendering
});

export default MyComponent;

In this example, MyComponent is wrapped with Ract.memo(). React will automatically memoize the component based on the equality of its props. If the props remain the same between renders, the component will not be re-rendered and thus improving the performance of the application.

Using useCallback to solve the problem

Let's write the counter example with useCallback() and React.memo(), to prevent unnecessary rendering of the ChildCounter component.

The ChildCounter takes the handleClick function as props. This handleClick function is recreated every time the ParentCounter component re-render, so to prevent it, we can use the useCallback. Now every time the ParentCounter component re-render clickHandler remains the same.

Now, we need to wrap the ChildCounter component with React.memo(). As we know that React.memo() memoized the wrapped component, and only re-render the component if the props change.

import React, { useState, useCallback } from "react";

export const MyCounter = () => {
  const [count, setCount] = useState(0);
  console.log("MyCounter render");

  const handleClick = useCallback(() => {
    setCount((preCount) => preCount + 1);
  }, []);

  return (
    <>
      <p> Count value is :{count}</p>
      <ChildCounter handleClick={handleClick} />
    </>
  );
};

// Wrap ChildCounter component with React.memo()
const ChildCounter = React.memo(({ handleClick }) => {
  console.log("ChildCounter render");
  return (
    <>
      <button onClick={handleClick}> increment</button>
    </>
  );
});

Best practices

When using the useCallback hook, there are some best practices that we should keep in mind:

  • Identify expensive computations: Use the useCallback to memoize callback functions that involve expensive computations or operations. This prevents unnecessary re-creation of callback functions on each render.

  • Pass dependencies in the dependency array: Make sure to include all the dependencies that the callback function relies on in the dependency array.

    This ensures that the callback function is re-created only when the dependencies change.


References