Blog Logo

Nov 5 2024 ~ 4 min read

Optimizing React Components with useCallback


Optimizing React Components with useCallback banner.

In my React projects, I’ve often encountered cases where my components re-render more than necessary, which can slow things down. One tool that can help reduce these extra re-renders is the useCallback hook. I’ll walk through how I used it to optimize a component and measure the difference.

Problem: Extra Re-Renders

When I pass functions as props to a component, React creates a new version of that function every time the parent component re-renders. If the child component is wrapped with React.memo to prevent it from re-rendering when its props haven’t changed, this will still cause it to re-render because React sees the new function as a different prop each time.

In this example, I set up two components:

One without useCallback, where the function recreates on each render. One with useCallback, where I use useCallback to “remember” the function and only recreate it if any of its dependencies change.

Setup

Here’s how I set up the code.

First, I created a child component with a button that increments a count when clicked. This child component logs how many times it renders, so I can measure the effect of using useCallback. Here’s the ChildComponent code:

import React, { memo, useEffect, useRef } from 'react';

const ChildComponent = memo(({ onIncrement }) => {
  const renderCount = useRef(0);
  renderCount.current += 1;

  useEffect(() => {
    console.log(`ChildComponent rendered ${renderCount.current} times`);
  });

  return (
    <button onClick={onIncrement}>
      Increment
    </button>
  );
});

export default ChildComponent;

I wrapped ChildComponent in React.memo so it only re-renders when its props change. I also used useRef to keep track of how many times the component renders.

Next, I created two versions of a parent component: one without useCallback and one with useCallback.

Version 1: Without useCallback

In this version, I didn’t use useCallback, so every time the parent component re-renders, React creates a new function for increment and passes it down to the ChildComponent as a prop.

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

function WithoutUseCallback() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <h2>Without useCallback</h2>
      <p>Count: {count}</p>
      <ChildComponent onIncrement={increment} />
    </div>
  );
}

export default WithoutUseCallback;

Even though increment does the same thing on each render, React recreates it every time the parent renders. This means that ChildComponent sees a new onIncrement prop each time, which causes it to re-render.

Version 2: With useCallback

In this version, I used useCallback to memoize the increment function. This means React will keep the same function reference for increment between renders, unless one of its dependencies changes. Here’s what the code looks like:


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

function WithUseCallback() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h2>With useCallback</h2>
      <p>Count: {count}</p>
      <ChildComponent onIncrement={increment} />
    </div>
  );
}

export default WithUseCallback;

Here, I wrapped increment in useCallback, so React will reuse the same function reference on each render. Now, ChildComponent will only re-render if its actual prop (onIncrement) changes, which should reduce the total number of renders. Benchmarking Results

Results

To see the effect, I rendered both versions side-by-side and clicked the increment buttons for each. I watched the console logs, which show how many times ChildComponent rendered.

In the Without useCallback version, ChildComponent re-renders every time I click the button. In the With useCallback version, ChildComponent renders only once at the start and doesn’t re-render with each button click.

This confirmed that useCallback reduced the number of renders in my child component. This can make a difference in cases where I have a deeply nested component tree or a complex child component.

Demo

In the demo below, you’ll get a hands-on look at how useCallback can help optimize components by reducing unnecessary re-renders. Start by clicking the Open Sandbox button or adjusting the side handle to view the preview. You can open your browser’s DevTools console to see the component render counts in real time.


Headshot of Samarth

Hi, I'm Samarth. I'm a software engineer based in Los Angeles. You can follow me on Twitter, see some of my work on GitHub, or read more about me on LinkedIn.