24 Apr 2024 · Software Engineering

    A React useCallback Hook Primer

    17 min read
    Contents

    React hooks, a feature introduced in version 16.8, helps developers code functional components. It makes writing functional components more appealing.

    React has several Hooks spanning various categories—state, context, ref, effect, performance and others. Amongst all these,useCallback can improve the performance of React apps. This hook is part of a family of hooks known as performance hooks, which include useMemo and memo.

    In this article, we’ll dive into the useCallback hook, its purpose, benefits, and the best cases for its application.

    Prerequisites

    To get the best from this article, you should have the following:

    • Fundamental understanding of React, JavaScript, and hooks.
    • Basic understanding of the useMemo hook – though I will still discuss it in this article.
    • Google Chrome Browser.

    What is the useCallback hook?

    The useCallback hook is a built-in hook in React that lets you memoize a callback function by preventing it from being recreated on every render. In simple terms, it means that the callback function is cached and does not get redefined on every render. This will optimize and improve the overall performance of your application.

    When you define a function inside a component, it is recreated on every render, even if the component’s state or props have not changed. This can lead to unnecessary re-renders, which can slow down your application’s performance. The useCallback hook helps you avoid this problem by memoizing the function and only recreating it when necessary.

    To understand the useCallback hook better, it’s important to grasp the concept of memoization. Memoization is an optimization technique that stores results from a computational event (like a function) in the cache, and on subsequent calls, it fetches the results directly from the cache without recomputing the result. This technique is very useful when dealing with expensive (time-consuming) computations or heavy data processing.

    Memoization can be compared to a real-world scenario where you ask someone to calculate the result of 2 multiplied by 200. After a while, you randomly ask the same question again. You naturally expect that person to give you the answer immediately because they have already done the calculation before. This time-saving concept is similar to memoization, where previously computed results are stored and reused when needed.

    The useCallback syntax

    This hook follows a very simple pattern for utilization. It takes two arguments: the function you want to memoize, and the dependencies array.

    useCallback(function, dependencies)
    const updatedFunction = useCallback(
      (args) => {
        //action goes in here	
      },
      [dependencies] 
    );
    • The first argument is the function you want to memoize.
    • The second argument is an array of dependencies. The elements in this array are the values on which the function to be memoized depends. If any of these values change, the function will be recreated.

    Note, if you omit the dependencies array, the function will be re-defined on every render.

    How to Memoize a React Component

    In React, we implement memoization via React.memo(), which is a higher-order component. The React.memo serves as a wrapper for a component and returns a memoized output of that component, which prevents the component or sub-components from unnecessary re-rendering.

    There are two ways by which we can use React.memo in our component. We can either use it to wrap the entire component or add it to the part where we export the component.

    In the example below you will find the first way of using it:

    const newComponent = React.memo((props) => {
        return (
          //render with props
        );
    });
    
    export default newComponent;

    In the syntax above, the newComponent component is wrapped with React.memo(), which creates a memoized version of the component. This memoized version of the component will only re-render if the props passed to it have changed.

    And here is the second way you can use React.memo:

    const newComponent = (props) => {
      //render with props
    }
    
    export default React.memo(newComponent);

    The syntax above denotes that we can memoize a component by simply passing it as an argument to React.memo and exporting the result.

    NoteReact.memo has nothing to do with React hooks. It is an in-built method in React used to aid the optimization of our React applications. If you prefer using a hook to memoize your component, you can use memo in place of React.memo.

    When to use the useCallback hook

    Now you understand how the useCallback hook can optimize your app, let’s see some use cases:

    • When you need to pass a function as props to a child component.
    • If you have a function that is expensive to compute and you need to call it in multiple places.
    • When dealing with functional components.
    • When you are working with a function that relies on external data or state.

    Note: Given the scenarios highlighted above, it’s still important to weigh the benefits and drawbacks of the hook and use it judiciously only where needed.

    The difference between useCallback and useMemo

    According to the React documentation, there is a hook called useMemo which also falls under the performance hooks category. Despite being under the same category of hooks division, these hooks differ in purpose and usage.

    Let’s look at the useMemo syntax before discussing how it differs from useCallback.

    Just like useCallback, this hook also takes two arguments – a function and the dependencies array.

    const updatedValue = useMemo((args) => {
      //function body that returns the value to memoize	
      },
      [dependencies] 
    );

    The major difference between the hooks is that the useCallback returns a memoized function while the useMemo returns a memoized value. It means that useMemo can help prevent unnecessary computations as it caches the computed value of the function and useCallback can help prevent unnecessary re-renders as it returns the memoized function that can be passed as props to the children’s components.

    The useCallback hook is ideal for functions that take a long time to compute or depend on external resources. In contrast, useMemo is ideal for values that take a long time to compute or depend on external resources.

    We can verify how each method works by running the following code in Playcode.io. We can call useMemo inside App.jsx to see the outputs.

    import React, {useMemo, useState} from 'react';
    
    const addition = (counter) => {
      let newValue = counter;
      for (let n = 0; n <= 10; n++) {
        newValue += 1;
      }
      return newValue;
    }
    
    export function App(props) {
      const [initial, setInitial] = useState(0);
    
      const result = useMemo(()=> {
        return addition(initial);
      }, [initial])
      console.log("useMemo:", result )
      return (
        <div className='App'>
          <h1>Hello this is the {result}</h1>
        </div>
      );
    }

    When using useMemo the console prints: “useMemo: 11”

    Let’s now test what useCallback results to in the same scenario:

    import React, {useCallback, useState} from 'react';
    
    export function App(props) {
      const [count, setCount] = useState(0);
    
      // Define a callback function using useCallback
      const handleClick = useCallback(() => {
        return count;
      }, [count]);
    
      console.log("useCallback:", handleClick )
    
      return (
        <div className='App'>
           <p>Count: {count}</p>
        </div>
      );
    }

    In this example, the console prints a function “useCallback: f()”. When using this function in a properly-bootstrapped React project, the returned function is ( ) => { return count }.

    Benefits of using the useCallback hook

    There are several advantages attached to using the useCallback hook. Here are a few:

    • Performance optimization: This hook optimizes the performance of your application by preventing a series of unnecessary re-rendering in your components.
    • Restricting rendering of child components: The useCallback hook in React allows us to selectively render important child components in a parent component. By using the useCallback hook, we can create memoized functions and pass them as props to child components. This ensures that only the necessary child components are rendered and updated when specific actions occur, resulting in improved performance.
    • Preventing memory leaks: Since the hook returns the memoized function, it prevents recreating functions, which can lead to memory leaks.

    Drawbacks of the useCallback hook

    Before making use of this hook, take into consideration its challenges, then weigh if it is still important to apply it in a particular case. The drawbacks:

    • Complex code: While this hook can help you create memoized functions, it can as well make your code complex. You must strike a balance between the usage of the hook and the complexity it adds to your code. Hence, only use the hook only when you need to memoize an expensive function which needs to be passed down to children components as a prop.
    • Excessive memory usage: If you do not use the useCallbck hook properly, it can lead to excessive memory usage. For instance, if a memoized function holds onto references to objects or variables that are no longer needed, those resources may not be freed up by garbage collection and could use more memory than needed.

    A practical example of the useCallback hook

    In this section, you will see how to use the hook. We will see how React.memo falls short in a Single Page Application (SPA) and the need for the useCallback hook.

    In this simple application, we have a Parent component with the name Parent.jsx and three children components, namely Button.jsxTitle.jsx, and Display.jsx which all rely on props from the Parent component.

    Bootstrap a new React Project using Vite

    Let’s bootstrap a new project with Vite:

    npm create vite@latest useCallback-hook --template react
    

    Clean up the default folder structure

    First, try to clean up the folder structure by removing styles and the asset folder. The purpose of this article is to explain the useCallback hook so I won’t be considering styling the web app.

    Next, create a component folder inside the src folder. In the component folder, create four files – Parent.jsxTitle.jsxButton.jsx, and Display.jsx.

    The file structure:

    src
    └── component
        ├── Parent.jsx
        ├── Title.jsx
        ├── Button.jsx
        └── Display.jsx
    

    The Parent.jsx content:

    // Parent.jsx
    
    import React, { useState } from "react";
    import Title from "./Title";
    import Button from "./Button";
    import Display from "./Display";
    
    const Parent = () => {
      const [salary, setSalary] = useState(2000);
      const [age, setAge] = useState(30);
    
      const incrementAge = () => {
        setAge(age + 5);
      };
    
      const incrementSalary = () => {
        setSalary(salary + 100);
      };
      return (
        <div>
          <Title />
          <Display text="age" displayvalue={age} />
          <Button handleClick={incrementAge}>Update Age</Button>
          <Display text="salary" displayvalue={salary} />
          <Button handleClick={incrementSalary}>Update Salary</Button>
        </div>
      );
    };
    
    export default Parent;

    The content of Title.jsx are as follows:

    // Title.jsx
    
    import React from "react";
    
    const Title = () => {
        console.log("Title Component is rendered");
      return (
        <h1>useCallback Hook.</h1>
      );
    };
    
    export default Title;

    The Display.jsx file contains the following:

    // Display.jsx
    
    import React from "react";
    
    const Display = ({ text, displayvalue }) => {
      console.log("Display Component Rendered ", { displayvalue });
    
      return (
        <p>
          This person's {text} is {displayvalue}
        </p>
      );
    };
    
    export default Display;

    Then, Button.jsx is as follows:

    // Button.jsx
    
    import React from "react";
    
    const Button = ({ handleClick, children }) => {
      console.log("Button Component Renders - ", { children });
      return <button onClick={handleClick}>{children}</button>;
    };
    
    export default Button;

    Finally, App.jsx contains these lines:

    import Parent from "./components/Parent";
    
    function App() {
      return (
        <>
          <Parent />
        </>
      );
    }
    
    export default App;

    Launch the project on your browser by running these commands:

    $ npm install
    $ npm run dev

    Here is the User Interface (UI):

    Let’s break down the UI:

    • Title: which says “useCallback hook”.
    • Display: renders the person’s age and salary.
    • Button: increments the age or the salary.

    To get how performance works in the web application, I have added a console log statement to all the children’s components. The console log statements will help you visualize which component renders when you trigger an action.

    By default, all the components render when you first launch the web application and you can confirm that by checking the logs in the console.

    In the snapshot above, all five components are rendered. Recall that there are just three children components but five instances – where the Title component is instantiated once, while Display and Button are instantiated twice inside the parent.jsx file.

    Next, clear the console and click the “Update Age” button, which causes a re-render for every component. You can confirm this by reading the console.

    When you click on the “Update Salary Button”, all components re-render. This is not ideal, as it can lead to performance issues. For example, if we have a very large application, interaction with even a small part of the app will cause all components to re-render, and the application may take a long time to load, resulting in poor user experience.

    The question now is how to optimize this simple application so that when you click the “Update Age” or “Update Salary” buttons, the Title and Age components do not re-render.

    Memoizing the Sample Project using React.memo()

    To memoize the sample application, wrap the children’s components with React.memo.

    After making the change, the new components should look like this:

    The Title.jsx file:

    // Title.jsx
    
    import React from "react";
    
    const Title = () => {
        console.log("Title Component is rendered");
      return (
        <h1>useCallback Hook.</h1>
      );
    };
    
    export default React.memo(Title);

    The Button.jsx file looks like this:

    // Button.jsx
    
    import React from "react";
    
    const Button = ({ handleClick, children }) => {
      console.log("Button Component Renders - ", { children });
      return <button onClick={handleClick}>{children}</button>;
    };
    
    export default React.memo(Button);

    And the Display.jsx component:

    // Display.jsx
    
    import React from "react";
    
    const Display = ({ text, displayvalue }) => {
      console.log("Display Component Rendered ", { displayvalue });
    
      return (
        <p>
          This person's {text} is {displayvalue}
        </p>
      );
    };
    
    export default React.memo(Display);

    Let’s test if React.memo solved the issue we first encountered. Back in the browser, clear the console and click the buttons.

    You get fewer re-renders, but we’re not done yet: the “Update Salary” button should not re-render when you click “Update Age`. The same thing can be said when you click the “Update Salary” button.

    The extra re-render is caused because these functions in the Parent.jsx file are recreated on every re-render:

    const incrementAge = () => {
       setAge(age + 5);
    };
    
    const incrementSalary = () => {
       setSalary(salary + 100);
    };
    

    When dealing with functions, you need to consider referential equality: Two functions having the same behaviour do not make them equal to each other.

    In our example case, the version of the functions before the components re-renders are different from the version of the functions after the components render.

    Referential Equality

    When dealing with equality in JavaScript, there are three methods by which this can be done:

    • == is the loose equality operator.
    • === is the strict equality operator.
    • object.is() is similar to the strict equality operator. It checks if two values are identical, without any type coercion.

    In React, when a component re-renders, it triggers a process known as reconciliation. The reconciliation process takes place within the Virtual DOM, a virtual representation of the actual DOM. It compares the previous state of the component with its new state, as well as the previous props with the new props.

    To perform this comparison accurately, React employs a technique called the diffing algorithm. This algorithm utilizes the Object.is() method to compare values without any type coercion, enabling React to precisely detect any actual changes between the previous and new states/props within the Virtual DOM.

    By pinpointing the differences, React can apply targeted updates to the real DOM only where necessary, minimizing unnecessary manipulations and enhancing performance. This combination of the diffing algorithm, reconciliation within the Virtual DOM, and the Object.is() method helps React optimize rendering and keep the user interface in sync with the component’s latest state.

    In JavaScript, we treat functions as first-class citizens, which means that we can use them just like any other value or object. This treatment includes assigning them to variables, passing them as arguments to other functions, and returning them from functions. JavaScript considers functions as objects because they can be assigned to variables making them easily referenced in the memory.

    Also, in JavaScript, objects, no matter how close or related, are not equal to each other because they have different references in the memory. This explains why the function before a component is rendered is different from the function after since functions are seen as objects.

    In order to grasp the concept behind referential equality, navigate to your console tab then define two variables and assign primitive data types to them, then compare them using the equality operator. You will get the boolean value of true returned but if you do this for things like arrays and objects the boolean value false is returned. If you perform this operation in your console, you will get this:

    Why did React.memo not fully memoize the application?

    Having understood what referential equality is, we can now understand what the problem is. React.memo does not prevent re-rendering in the children components because the props passed to the children components are functions.

    Recall that the functions before and after component rendering are not the same. Here, React.memo noticed that the props which are functions coming from the Parent component have changed, so it doesn’t stop the components from re-rendering.

    The big question is how to let React know that the props have not changed so re-rendering is not triggered.

    The solution to that is the useCallback hook which will fully memoize our application.

    Integrating useCallback into the application.

    Firstly, import the useCallback hook at the top inside the component where you need it (Parent.jsx).

    import { useCallback, useState } from "react";

    Next, rewrite the functions you want to memoize using the useCallback hook.

    const incrementAge = useCallback(() => {
       setAge(age + 5);
    }, [age]);
    
    const incrementSalary = useCallback(() => {
       setSalary(salary + 100);
    }, [salary]);

    You now get memoized callback functions cached and passed as props to the children components.

    Finally, test the application to check that the issue has been resolved. On the first load of the application, all components are rendered. But when you click on the buttons only the components affiliated with that button render unlike the occurrence earlier on.

    When you click on the update age button, only the Display and Button components related to that action re-renders.

    When you also click on the update salary button, only the Display and Button components related to that action re-renders.

    We have successfully optimized the application, improving its overall performance.

    Conclusion

    The useCallback hook is a powerful tool that can be used to improve the performance of React components. By memoizing callback functions, useCallback can prevent unnecessary re-renders, which can lead to a smoother user experience.

    In this article, we explored the key concepts related to useCallback, including its purpose, usage, benefits, and best practices. We also compared useCallback with other related hooks like useMemo and highlighted their differences and appropriate use cases.

    Here are some key takeaways from this article:

    • useCallback is a hook that can be used to memoize callback functions.
    • Memoization is a technique that can be used to cache the results of a function call so that it does not need to be re-evaluated on every render.
    • Memoization can be used to improve the performance of React components by preventing unnecessary re-renders.
    • useCallback is most commonly used to memoize callback functions that are passed to child components.
    • useCallback can also be used to memoize callback functions that are used in other contexts, such as event handlers and timers.

    When using useCallback, it is important to be aware of the following best practices:

    • Only use useCallback on callback functions that are truly performance-critical.
    • Specify the dependencies of useCallback carefully to ensure that the function is only memoized when necessary.

    By following these best practices, you can use useCallback to improve the performance of your React components without sacrificing code clarity or maintainability.

    I hope this article has been helpful!

    Resources

    For further knowledge on the subject discussed, check these materials:

    One thought on “A React useCallback Hook Primer

    1. This isn’t *technically* correct – useCallback won’t memoize, cache, or prevent the function from being re-defined. What it does do is make sure that the function is re-defined with the same ID unless a dependency changes. This can prevent re-renders in child components that use that callback, assuming they would re-render when passed a new callback.

      Subtle difference, but a meaningful one when working in codebases of substantial size as people tend to over-use this hook.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Bobate Olusegun is a talented Frontend Developer and passionate web3 Enthusiast hailing from Lagos, Nigeria. When he's not skillfully navigating the digital realm through his trusty laptop, you'll likely find Segun immersed in the pages of thought-provoking books, exploring the realms of Crypto and Life Experience.
    Avatar
    Reviewed by:
    I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.