Questions
This component re-renders unnecessarily. How do you prevent it?
The Scenario
You’re working on a React application and have an ExpensiveComponent that performs a lot of calculations or renders complex UI. To monitor its performance, you’ve added a console.log that fires every time it re-renders.
This ExpensiveComponent receives a user object as a prop from its parent, App. You’ve noticed that ExpensiveComponent re-renders every time the App component re-renders (e.g., when you click a button in App), even if the user object’s content (id and name) hasn’t logically changed. This leads to unnecessary work and can degrade application performance.
The Challenge
You’ve been given the code for the App and ExpensiveComponent.
- Explain why
ExpensiveComponentre-renders when theAppcomponent re-renders, even if theuserdata is logically the same. - Optimize the
Appcomponent andExpensiveComponentto prevent these unnecessary re-renders.
import React, { useState } from 'react';
// An "expensive" component that logs every re-render
function ExpensiveComponent({ user }) {
console.log('Re-rendering ExpensiveComponent...');
// Simulate some heavy computation
let sum = 0;
for (let i = 0; i < 10000000; i++) {
sum += i;
}
return (
<div style={{ border: '1px solid red', padding: '10px', margin: '10px' }}>
<h3>Expensive Component</h3>
<p>User ID: {user.id}</p>
<p>User Name: {user.name}</p>
<p>Simulated heavy computation result: {sum}</p>
</div>
);
}
export default function App() {
const [count, setCount] = useState(0); // State to force App re-renders
// THE BUG: The user object is created inline on every render of App.
// Its reference changes even if id and name are logically the same.
const user = { id: 1, name: 'Alice' };
return (
<div style={{ padding: '20px' }}>
<h1>App Component</h1>
<button onClick={() => setCount(c => c + 1)}>
Force App Re-render (Count: {count})
</button>
<hr />
<ExpensiveComponent user={user} />
</div>
);
} The Explanation: React’s Reconciliation and Prop References
[object Object]
[object Object]
Why the Problem Occurs
In React, a component re-renders when its state or props change. When a parent component re-renders, by default, all of its child components also re-render. React then performs a “reconciliation” process, comparing the new virtual DOM with the previous one to determine the minimal actual DOM updates.
The problem in the buggy code is subtle but critical:
- The
Appcomponent re-renders when itscountstate changes. - Inside
App’s render function, theuserobject ({ id: 1, name: 'Alice' }) is created inline. - Even though the
idandnameproperties are always1and'Alice', creating the object inline means that on every render ofApp, a new object reference foruseris generated. - When
Apppasses this newuserobject (with a new reference) as a prop toExpensiveComponent, React sees thatExpensiveComponenthas received a “new” prop. - Therefore,
ExpensiveComponentre-renders, even though the data it contains is logically the same.
The Fix: Memoization with useMemo and React.memo
To prevent this unnecessary re-render, we need to ensure that the user prop’s reference only changes when its actual content changes. We can achieve this using two memoization techniques:
useMemo(in the parent component): Memoize theuserobject itself. This ensures its reference remains stable acrossAppre-renders as long as its dependencies (which are constant in this case) don’t change.React.memo(on the child component): WrapExpensiveComponentwithReact.memo. This is a Higher-Order Component that will preventExpensiveComponentfrom re-rendering if its props haven’t shallowly changed.
Here is the corrected implementation:
import React, { useState, useMemo, memo } from 'react';
// Wrap the expensive component with React.memo
const MemoizedExpensiveComponent = memo(function ExpensiveComponent({ user }) {
console.log('Re-rendering ExpensiveComponent...');
let sum = 0;
for (let i = 0; i < 10000000; i++) {
sum += i;
}
return (
<div style={{ border: '1px solid red', padding: '10px', margin: '10px' }}>
<h3>Expensive Component</h3>
<p>User ID: {user.id}</p>
<p>User Name: {user.name}</p>
<p>Simulated heavy computation result: {sum}</p>
</div>
);
});
export default function App() {
const [count, setCount] = useState(0);
// FIX: Memoize the user object using useMemo.
// The user object's reference will now only change if its dependencies change.
// In this case, the dependencies are an empty array, so it's created once.
const user = useMemo(() => ({ id: 1, name: 'Alice' }), []);
return (
<div style={{ padding: '20px' }}>
<h1>App Component</h1>
<button onClick={() => setCount(c => c + 1)}>
Force App Re-render (Count: {count})
</button>
<hr />
{/* Use the memoized version of the component */}
<MemoizedExpensiveComponent user={user} />
</div>
);
}
With useMemo ensuring a stable user object reference and React.memo preventing re-renders when props are shallowly equal, ExpensiveComponent will now only re-render when its user prop actually changes (i.e., its reference changes, which useMemo now controls).
Practice Question
Which of the following is NOT a direct cause for a React functional component to re-render?