Exploring React Hooks: useCallback

Exploring React Hooks: useCallback

Exploring the Power of useCallback Hook in React

Welcome back to the Exploring React Hooks series! In our previous article, we discussed the useReducer Hook. Today, we'll check out the useCallback Hook.

So, let's start exploring.

What is the useCallback Hook?

The useCallback hook is specifically designed to optimize the rendering performance of React components.

useCallback is a hook that will return a memoized version of the callback function that only changes if one of the dependencies has changed.

It is especially useful when dealing with components that rely on callbacks, which are frequently passed down as props.

Syntax and Usage:

The syntax for useCallback hook is:

const memoizedCallback = useCallback(callback, dependencies);

Here, there are two parameters:

  • callback : the function you want to memoize

  • dependencies : an array of values that, when changed, will trigger the creation of a new memoized callback.

In React, when a component re-renders, all the functions defined within it are recreated, including the ones passed as props. This behavior can lead to unnecessary re-rendering of child components, even if the props passed to them remain unchanged.

Let's check out an example to illustrate the usage of useCallback:

🪝Scenario

Let's consider a scenario where there are two buttons, one to increment the age and the other to increment the salary of a person.

We would add log statements for some of the components. It would be used to indicate that the components are being rendered onto the screen.

Title Component:

We'll start by creating a simple Title component, which will render a simple h2 element displaying the text "useCallback Hook."

We'll log a message indicating that it is being rendered.

import React from "react";

function Title() {
  console.log("Rendering Title");
  return <h2>useCallback Hook</h2>;
}

export default Title;

Count Component:

Next, we will create a Count component, which will display a text and a count value.

We'll pass two props: text (the label to be displayed) and count (the count value to be displayed). It will render the text and count values inside a div element.

We'll log a message indicating that it is being rendered.

import React from "react";

function Count({ text, count }) {
  console.log(`Rendering ${text}`);
  return (
    <div>
      {text} - {count}
    </div>
  );
}

export default Count;

Button Component:

Let's also create a Button component that will render a button and handle a click event.

It will receive two props: handleClick (a function to be executed when the button is clicked) and children (the text content of the button). When the button is clicked, the handleClick function will be executed.

We'll again log a message indicating that it is being rendered.

import React from "react";

function Button({ handleClick, children }) {
  console.log("Rendering button - ", children);
  return <button onClick={handleClick}>{children}</button>;
}

export default Button;

ParentComponent

The ParentComponent will serve as the parent component for other components.

Let's use the useState hook to define two state variables: age and salary, and their respective setter functions setAge and setSalary.

We'll have two functions: incrementAge and incrementSalary, which will be responsible for updating the age and salary states by incrementing their values by 1 and 2000 respectively.

import React, { useState } from "react";
import Count from "./Count";
import Button from "./Button";
import Title from "./Title";

function ParentComponent() {
  const [age, setAge] = useState(25);
  const [salary, setSalary] = useState(50000);

  const incrementAge = () => {
    setAge(age + 1);
  };

  const incrementSalary = () => {
    setSalary(salary + 2000);
  };

  return (
    <div>
      <Title />
      <Count text="Age" count={age} />
      <Button handleClick={incrementAge}>Increment Age</Button>
      <Count text="Salary" count={salary} />
      <Button handleClick={incrementSalary}>Increment Salary</Button>
    </div>
  );
}

export default ParentComponent;

Import the respective components and inside the return statement,

  • The Title component will be rendered without any props.

  • The Count component will be rendered twice, displaying the current age and salary values.

  • The Button components will be rendered alongside the Count components, and when clicked, they will enable the user to increment the age and salary values.

App Component:

Import the ParentComponent into your App Component and run the server.

import React from "react";
import ParentComponent from "./ParentComponent";

function App() {
  return (
    <div className="App">
      <ParentComponent />
    </div>
  );
}

export default App;

Output:

This should be the expected result:

🪝Scenario: Problem 1

When you refresh the browser and open the console, you will see log statements for the rendering of each component:

Let's clear the console. Now,

➡️Upon clicking the Increment Age button, the age value increments, and the component re-renders. However, if you observe the logs, you'll notice that all the components are re-rendered.

➡️Similarly, when you click the Increment Salary button, the salary value increments, and the component re-renders. Again, all the components are re-rendered according to the logs.

This issue can become problematic when dealing with a large number of components. In such cases, unnecessary re-renders can lead to performance issues.

To address this, we need to optimize the rendering process and restrict re-renders to only the components that require updating.

🪝Scenario: Optimizing with React.memo

To address the performance issues, React offers a higher-order component React.memo().

React.memo is a higher-order component that will prevent a functional component from being re-rendered if it's props or state do not change.

Wrapping a functional component with React.memo() memoizes its rendered output and avoids unnecessary re-rendering when the props or state remain unchanged.

Let's make the necessary changes:

Title Component:

When exporting, wrap the component with React.memo().

export default React.memo(Title);

Count Component:

When exporting, wrap the component with React.memo().

export default React.memo(Count);

Button Component:

When exporting, wrap the component with React.memo().

export default React.memo(Button);

With the optimization in place, components only re-render when there are changes in props or state.

However, we encounter another issue!!

🪝Scenario: Problem 2

Let's clear the console.

➡️Now, clicking the Increment Age button triggers fewer logs, but the Increment Salary button still causes the re-rendering of its component.

➡️Similarly, clicking the Increment Salary button re-renders the Increment Age button.

This happens because new functions are created during each parent component render, causing React.memo() to consider them as changed props.

Functions cannot be directly compared for equality, even if they have the same behavior. Therefore, the function before the re-render is different from the function after the re-render.

We need to tell React that there is no need to create a new increment salary function when we update the age or vice-versa.

Here's where useCallback comes into action!!

🪝Scenario: Introducing useCallback Hook

To resolve this, we'll introduce the useCallback Hook. By wrapping our callback functions with useCallback, we can memoize them and ensure they are only recreated when their dependencies change. This technique allows us to prevent unnecessary re-renders of components that rely on these functions.

🪝Scenario: The Modified Solution

ParentComponent

In the ParentComponent, import the useCallback Hook and utilize it by passing in an arrow function as the first parameter. The second parameter is the dependency list, which in this case is the 'age' variable. Repeat the same process for the 'salary' variable.

Now in both cases, we return the cached function which is then passed as a prop to the child components.

import React, { useState, useCallback } from "react";
import Count from "./Count";
import Button from "./Button";
import Title from "./Title";

function ParentComponent() {
  const [age, setAge] = useState(25);
  const [salary, setSalary] = useState(50000);

  const incrementAge = useCallback(() => {
    setAge(age + 1);
  }, [age]);

  const incrementSalary = useCallback(() => {
    setSalary(salary + 2000);
  }, [salary]);

  return (
    <div>
      <Title />
      <Count text="Age" count={age} />
      <Button handleClick={incrementAge}>Increment Age</Button>
      <Count text="Salary" count={salary} />
      <Button handleClick={incrementSalary}>Increment Salary</Button>
    </div>
  );
}

export default ParentComponent;

By using useCallback, these functions are memoized and will only be re-created if their respective dependencies (age or salary) change

Run the server:

Output:

Let's again clear the console.

➡️After implementing the optimization, only two components are re-rendered when clicking the Increment Age button, while the salary button remains unaffected.

➡️Similarly, clicking the Increment Salary button results in only two re-renders, with the age button no longer being re-rendered.

By incorporating the useCallback Hook and React.memo() into our components, we have successfully optimized all our components.

To Summarize:

To summarize, the useCallback Hook is a valuable tool for optimizing React component performance. By memoizing functions, we can prevent unnecessary re-renders and achieve better rendering efficiency.

When dealing with components that have complex rendering logic or handle large lists, leveraging useCallback can result in significant performance enhancements. It is crucial to identify callback functions with stable dependencies and utilize useCallback to memoize them effectively, ensuring optimal performance in your React applications.


References


Thank's for taking the time to read this blog. Hope you found it informative and enjoyable! Connect with me on Twitter.

Stay tuned for our next article: useMemoHook.

Catch you guys on the next one. Cheers .. ✌️

Did you find this article valuable?

Support Raj Sarkar by becoming a sponsor. Any amount is appreciated!