No history yet

Reference Equality Optimization

The Re-rendering Problem

In React, a component re-renders when its state or props change. This is usually what you want. But what if the data inside an object or array prop is the same, but the object or array itself is a new instance? React's default behavior might surprise you.

By default, React performs a shallow comparison to check for changes. It uses the Object.is() comparison, which is very similar to using the strict equality operator (===). This means if you pass a new object or array as a prop on every render, React sees it as a new value and re-renders the child component, even if the contents are identical.

This leads to unnecessary re-renders, which can slow down your application, especially with complex components. The core of this issue is referential integrity—or rather, the lack of it. When a new object is created, it gets a new address in memory, so it's not strictly equal to the old one, even if its properties are the same.

// In a parent component's render method...

const user = { name: 'Alex', age: 30 };

// Every time Parent renders, a NEW user object is created.
// So, <Profile user={user} /> will always re-render,
// even if the name and age haven't changed.
return <Profile user={user} />;

Memoization to the Rescue

To prevent these unnecessary re-renders, you can use a technique called memoization. Memoization is an optimization technique where you cache the result of an expensive function call and return the cached result when the same inputs occur again. In React, this means we can tell a component to “remember” the result of a calculation or the reference to an object or function, and only create a new one when its dependencies change.

React gives us two primary hooks for this: useMemo and useCallback.

React.memo is a higher-order component that optimizes functional components by preventing them from re-rendering unless their props change.

Let's start with React.memo. It's a higher-order component, or HOC for short. You wrap your component in it, and React will automatically perform a shallow comparison on its props. If the props haven't changed, React skips re-rendering the component and reuses the last rendered result.

import React from 'react';

const UserProfile = ({ user }) => {
  console.log('Rendering UserProfile for', user.name);
  return <div>{user.name}</div>;
};

// Wrap the component with React.memo
export default React.memo(UserProfile);

Now, UserProfile will only re-render if the user prop changes. But wait, in our earlier example, we were creating a new user object on every render. So even with React.memo, it would still re-render unnecessarily because the object reference is different.

This is where useMemo comes in. It lets us memoize the value itself.

import { useMemo } from 'react';

function UserDashboard({ name, age }) {
  // The user object is now memoized.
  // It will only be recreated if name or age changes.
  const user = useMemo(() => ({
    name: name,
    age: age
  }), [name, age]);

  return <UserProfile user={user} />;
}

With React.memo and useMemo working together, you prevent both the component from re-rendering and the prop from being recreated unnecessarily. This is a common and powerful optimization pattern.

The same principle applies to functions. If you pass a function as a prop, you create a new function reference on every render. To prevent this, use the useCallback hook. It memoizes the function itself, so it only gets recreated when its dependencies change.

This is especially important when passing event handlers to memoized child components.

import { useState, useCallback } from 'react';
import { memo } from 'react';

const Button = memo(({ onClick }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>Click Me</button>;
});

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

  // Without useCallback, this function is new on every render,
  // causing the memoized Button to re-render.
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // Empty dependency array means this function never changes

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={handleClick} />
    </div>
  );
}

The Catch: When Not to Optimize

With these powerful tools, it’s tempting to wrap every component in React.memo and every value in useMemo. Don't. This is known as , and it can actually harm performance.

Mantaining a cache and checking dependency arrays isn't free. It uses memory and takes time. For simple components that render quickly, the cost of memoization can be higher than the cost of simply re-rendering.

So, when should you optimize? Use profiling tools to find the real bottlenecks in your app. Focus on:

  • Components that render often with the same props.
  • Components with computationally expensive rendering logic.
  • Values that are expensive to calculate.

Rule of thumb: Don't optimize until you have a performance problem. Profile first, then optimize.

Quiz Questions 1/5

In React, if a parent component passes an object prop like user={{ name: 'Alice' }} to a child component on every render, why might the child component re-render unnecessarily, even if the user's name hasn't changed?

Quiz Questions 2/5

What is the primary purpose of wrapping a component with React.memo?