Questions
This optimized component re-renders unnecessarily. How do you fix it with `useCallback`?
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.
- Explain why
OptimizedChildre-renders unnecessarily despite being wrapped inReact.memo. - Fix the
Appcomponent using theuseCallbackhook to preventOptimizedChildfrom re-rendering whencountchanges.
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>
);
} The Solution
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> ); }
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.
- 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
handleClickinApp), a new function object is created on every single render of theAppcomponent. React.memoand Shallow Comparison:React.memois a Higher-Order Component that prevents a functional component from re-rendering if its props have not changed. However,React.memoperforms a shallow comparison of props. WhenAppre-renders, it creates a newhandleClickfunction. Even though the code insidehandleClickis the same, its reference is different from the previous render.- Defeating Memoization:
React.memosees that theonClickprop (which is thehandleClickfunction) has a new reference, concludes that the prop has changed, and therefore re-rendersOptimizedChild, 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?