How to Implement useCallback Hook in React

In this article, you will learn how to implement the useCallback Hook in React to optimize your application’s performance. We will cover what useCallback is, why it is used to prevent unnecessary re-renders, and provide a clear, step-by-step code example demonstrating its practical implementation.

What is the useCallback Hook?

The useCallback Hook is a React hook that returns a memoized version of a callback function. It only changes if one of its dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

In JavaScript, functions are objects, and every time a component re-renders, any function defined within it is recreated with a new memory address. By wrapping a function in useCallback, React caches the function instance between renders.

The Syntax of useCallback

The useCallback Hook accepts two arguments: the function you want to memoize, and an array of dependencies.

import { useCallback } from 'react';

const memoizedFunction = useCallback(() => {
  // Your function logic here
}, [dependency1, dependency2]);

Step-by-Step Implementation Example

To see useCallback in action, consider a scenario where a parent component passes a function to a child component. Without useCallback, the child component re-renders every time the parent component’s state changes, even if the child’s props haven’t logically changed.

Step 1: Create the Child Component

We use React.memo to prevent the child component from re-rendering unless its props change.

import React from 'react';

const Button = React.memo(({ handleClick, children }) => {
  console.log(`Rendering button: ${children}`);
  return (
    <button onClick={handleClick}>
      {children}
    </button>
  );
});

export default Button;

Step 2: Implement useCallback in the Parent Component

In the parent component, we have two state variables: count and theme. If we do not use useCallback for the increment function, the Button component will re-render every time the theme changes, because a new increment function is created on every render.

By implementing useCallback, we ensure the function reference remains identical unless count changes.

import React, { useState, useCallback } from 'react';
import Button from './Button';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [darkTheme, setDarkTheme] = useState(false);

  // Implement useCallback to memoize the increment function
  const incrementCount = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // Empty dependency array because we use functional state updates

  const toggleTheme = () => {
    setDarkTheme((prevTheme) => !prevTheme);
  };

  const themeStyle = {
    backgroundColor: darkTheme ? '#333' : '#FFF',
    color: darkTheme ? '#FFF' : '#333',
    padding: '20px'
  };

  return (
    <div style={themeStyle}>
      <h1>Count: {count}</h1>
      {/* Passing the memoized callback to the child */}
      <Button handleClick={incrementCount}>Increment</Button>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

export default ParentComponent;

When to Use and When to Avoid useCallback

While useCallback is powerful, you should not wrap every function in it. Memoization has a performance cost because React has to do extra work to store and compare dependencies.