DeployU
Interviews / Frontend Engineering / This optimized component re-renders unnecessarily. How do you fix it with `useCallback`?

Questions

This optimized component re-renders unnecessarily. How do you fix it with `useCallback`?

practical Performance Interactive Quiz Code Examples

A developer has an App component that manages a count state. It renders a child component, OptimizedChild, which is wrapped in React.memo to prevent unnecessary re-renders. The App component passes a handleClick function as a prop to OptimizedChild.

However, they’ve noticed a performance issue: OptimizedChild still re-renders every time the App component re-renders (e.g., when count changes), even though OptimizedChild’s props (specifically the handleClick function) seem to be the same. This defeats the purpose of React.memo.

The Challenge

You’ve been given the App and OptimizedChild components.

  1. Explain why OptimizedChild re-renders unnecessarily despite being wrapped in React.memo.
  2. Fix the App component using the useCallback hook to prevent OptimizedChild from re-rendering when count changes.
Your Code
import React, { useState, memo } from 'react';

// OptimizedChild is wrapped in React.memo
const OptimizedChild = memo(function OptimizedChild({ onClick }) {
  console.log('Re-rendering OptimizedChild...');
  return (
    <button onClick={onClick} style={{ padding: '10px', margin: '10px' }}>
      Click me (Child)
    </button>
  );
});

export default function App() {
  const [count, setCount] = useState(0);

  // THE BUG: This function is re-created on every render of App.
  // Its reference changes, causing OptimizedChild to re-render.
  const handleClick = () => {
    console.log('Child button clicked!');
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Parent Count: {count}</h1>
      <button onClick={() => setCount(c => c + 1)}>
        Increment Parent Count
      </button>
      <hr />
      <OptimizedChild onClick={handleClick} />
    </div>
  );
}
Click "Run Code" to see output...
Click "Run Code" to see test results...

The Solution

Wrong Approach

export default function App() { const [count, setCount] = useState(0); // THE BUG: This function is re-created on every render of App. // Its reference changes, causing OptimizedChild to re-render. const handleClick = () => { console.log('Child button clicked!'); }; return ( <div style={{ padding: '20px' }}> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(c => c + 1)}> Increment Parent Count </button> <hr /> <OptimizedChild onClick={handleClick} /> </div> ); }

Right Approach

export default function App() { const [count, setCount] = useState(0); // FIX: Memoize the handleClick function using useCallback. // The function's reference will now only change if its dependencies change. // In this case, handleClick doesn't depend on 'count' or any other state/props, // so an empty dependency array ensures it's created only once. const handleClick = useCallback(() => { console.log('Child button clicked!'); }, []); // Empty dependency array means this function is stable across renders return ( <div style={{ padding: '20px' }}> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(c => c + 1)}> Increment Parent Count </button> <hr /> <OptimizedChild onClick={handleClick} /> </div> ); }

Why This Works

The problem lies in how JavaScript handles functions and how React.memo performs its shallow comparison of props.

  1. Functions are Objects: In JavaScript, functions are first-class citizens, meaning they are objects. When you define a function directly inside a component’s render function (like handleClick in App), a new function object is created on every single render of the App component.
  2. React.memo and Shallow Comparison: React.memo is a Higher-Order Component that prevents a functional component from re-rendering if its props have not changed. However, React.memo performs a shallow comparison of props. When App re-renders, it creates a new handleClick function. Even though the code inside handleClick is the same, its reference is different from the previous render.
  3. Defeating Memoization: React.memo sees that the onClick prop (which is the handleClick function) has a new reference, concludes that the prop has changed, and therefore re-renders OptimizedChild, defeating the optimization.

The Fix: Memoizing Functions with useCallback

To prevent this unnecessary re-render, we need to ensure that the handleClick function’s reference remains stable across renders, as long as its dependencies don’t change. This is precisely what the useCallback hook is for.

useCallback returns a memoized version of the callback function. It will only re-create the function (and thus change its reference) if any of its dependencies (specified in the dependency array) have changed.

With useCallback, the handleClick function’s reference remains stable across App re-renders (because its dependencies [] never change). React.memo on OptimizedChild now correctly sees that the onClick prop’s reference hasn’t changed, and thus prevents OptimizedChild from re-rendering unnecessarily.

Practice Question

What is the primary purpose of the useCallback hook?