DeployU
Interviews / Frontend Engineering / This component re-renders unnecessarily. How do you prevent it?

Questions

This component re-renders unnecessarily. How do you prevent it?

debugging Performance Interactive Quiz Code Examples

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.

  1. Explain why ExpensiveComponent re-renders when the App component re-renders, even if the user data is logically the same.
  2. Optimize the App component and ExpensiveComponent to prevent these unnecessary re-renders.
Your Code
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>
  );
}
Click "Run Code" to see output...
Click "Run Code" to see test results...

The Explanation: React’s Reconciliation and Prop References

Wrong Approach

[object Object]

Right Approach

[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:

  1. The App component re-renders when its count state changes.
  2. Inside App’s render function, the user object ({ id: 1, name: 'Alice' }) is created inline.
  3. Even though the id and name properties are always 1 and 'Alice', creating the object inline means that on every render of App, a new object reference for user is generated.
  4. When App passes this new user object (with a new reference) as a prop to ExpensiveComponent, React sees that ExpensiveComponent has received a “new” prop.
  5. Therefore, ExpensiveComponent re-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:

  1. useMemo (in the parent component): Memoize the user object itself. This ensures its reference remains stable across App re-renders as long as its dependencies (which are constant in this case) don’t change.
  2. React.memo (on the child component): Wrap ExpensiveComponent with React.memo. This is a Higher-Order Component that will prevent ExpensiveComponent from 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?