How to prevent unnecessary renders in React?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Medium
#React

Brief Answer

Main ways to prevent unnecessary renders in React:

  1. React.memo — memoization of functional components
  2. useMemo — caching computation results
  3. useCallback — preventing function recreation
  4. PureComponent — for class components
  5. Proper component structure — separating parts that change less frequently
  6. Stable objects and arrays — moving them outside the component or memoizing
  7. Proper Context usage — splitting into smaller contexts

Example using React.memo:

// Component re-renders only when name changes
const UserGreeting = React.memo(function UserGreeting({ name }) {
  console.log("UserGreeting renders");
  return <h1>Hello, {name}!</h1>;
});
 
function App() {
  const [name, setName] = useState("Maria");
  const [counter, setCounter] = useState(0);
  
  return (
    <div>
      <UserGreeting name={name} />
      <button onClick={() => setName("Alex")}>
        Change name
      </button>
      <button onClick={() => setCounter(c => c + 1)}>
        Counter: {counter}
      </button>
    </div>
  );
}

Full Answer

Preventing unnecessary re-renders is one of the key aspects of optimizing React application performance. React, by default, re-renders components even when it’s not always necessary, so it’s important to know how to control this. 🚀

1. React.memo for Functional Components

React.memo is a HOC (Higher-Order Component) that memoizes a component, preventing it from re-rendering if props haven’t changed:

const MovieCard = React.memo(function MovieCard({ title, rating }) {
  console.log(`Render: ${title}`);
  
  return (
    <div className="movie-card">
      <h3>{title}</h3>
      <span>Rating: {rating}/10</span>
    </div>
  );
});
 
// Usage
function MovieList() {
  const [movies, setMovies] = useState(initialMovies);
  const [filter, setFilter] = useState("");
  
  // MovieCard only re-renders when a specific movie changes
  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
        placeholder="Filter"
      />
      {movies.map(movie => (
        <MovieCard 
          key={movie.id} 
          title={movie.title} 
          rating={movie.rating} 
        />
      ))}
    </div>
  );
}

Custom Comparator for React.memo

You can set up custom prop comparison logic:

const ProjectItem = React.memo(
  function ProjectItem({ project, onSelect }) {
    return (
      <div onClick={() => onSelect(project.id)}>
        {project.name} 
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Re-render only when id or name changes
    return (
      prevProps.project.id === nextProps.project.id &&
      prevProps.project.name === nextProps.project.name
    );
  }
);

2. useMemo for Computed Values

useMemo allows caching expensive calculation results between renders:

function ProductList({ products, category }) {
  // Filtering only runs when products or category change
  const filteredProducts = useMemo(() => {
    console.log("Filtering products");
    return products.filter(p => p.category === category);
  }, [products, category]);
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

3. useCallback for Functions

useCallback prevents creating new functions on every render:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");
 
  // Function isn't recreated on every render
  const handleDelete = useCallback((id) => {
    setTodos(todos => todos.filter(todo => todo.id !== id));
  }, []); // Empty dependencies because we use functional update
  
  return (
    <div>
      <input 
        value={newTodo} 
        onChange={(e) => setNewTodo(e.target.value)} 
      />
      <button onClick={() => {
        if (newTodo) {
          setTodos([...todos, { id: Date.now(), text: newTodo }]);
          setNewTodo("");
        }
      }}>
        Add
      </button>
      
      {todos.map(todo => (
        // TodoItem won't re-render when only the input text changes
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onDelete={handleDelete} 
        />
      ))}
    </div>
  );
}
 
const TodoItem = React.memo(function TodoItem({ todo, onDelete }) {
  console.log(`Render: ${todo.text}`);
  return (
    <div>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

4. PureComponent for Class Components

PureComponent is the equivalent of React.memo for class components:

class UserProfile extends React.PureComponent {
  render() {
    console.log("UserProfile renders");
    const { name, email } = this.props;
    
    return (
      <div className="profile">
        <h2>{name}</h2>
        <p>{email}</p>
      </div>
    );
  }
}

5. Proper Component Structure

Separating components by frequency of changes:

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  
  // Frequently changing component (on every input)
  return (
    <div>
      <SearchBar 
        query={query} 
        onChange={setQuery} 
      />
      {/* Rarely changing component (only on new results) */}
      <SearchResults results={results} />
    </div>
  );
}
 
// Optimized components
const SearchBar = React.memo(function SearchBar({ query, onChange }) {
  return (
    <input 
      value={query} 
      onChange={(e) => onChange(e.target.value)} 
      placeholder="Search" 
    />
  );
});
 
const SearchResults = React.memo(function SearchResults({ results }) {
  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
});

6. Stable Objects and Arrays

Preventing creation of new objects on every render:

// ❌ Bad: new style object on every render
function Button({ primary }) {
  return (
    <button style={{ 
      color: primary ? 'white' : 'black',
      background: primary ? 'blue' : 'gray'
    }}>
      Click me
    </button>
  );
}
 
// ✅ Good: memoized styles
function Button({ primary }) {
  const buttonStyle = useMemo(() => ({
    color: primary ? 'white' : 'black',
    background: primary ? 'blue' : 'gray'
  }), [primary]);
  
  return <button style={buttonStyle}>Click me</button>;
}
 
// Alternative: static objects
const primaryStyle = { color: 'white', background: 'blue' };
const secondaryStyle = { color: 'black', background: 'gray' };
 
function Button({ primary }) {
  return (
    <button style={primary ? primaryStyle : secondaryStyle}>
      Click me
    </button>
  );
}

7. Optimizing Context

Splitting context into smaller pieces:

// ❌ Bad: one large context
const AppContext = React.createContext();
 
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  return (
    <AppContext.Provider value={{ 
      user, setUser, 
      theme, setTheme, 
      notifications, setNotifications 
    }}>
      {children}
    </AppContext.Provider>
  );
}
 
// ✅ Good: separated contexts
const UserContext = React.createContext();
const ThemeContext = React.createContext();
const NotificationContext = React.createContext();
 
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <NotificationContext.Provider value={{ notifications, setNotifications }}>
          {children}
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

8. Using the Reselect Library with Redux

For applications using Redux:

import { createSelector } from 'reselect';
 
// Base selectors
const getUsers = state => state.users;
const getFilter = state => state.filter;
 
// Memoized selector
const getFilteredUsers = createSelector(
  [getUsers, getFilter],
  (users, filter) => {
    console.log('Filtering users');
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }
);
 
// Usage in component
function UserList() {
  const filteredUsers = useSelector(getFilteredUsers);
  // Filtering only runs when users or filter change
  
  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Tools for Finding Issues

1. React DevTools Profiler

Profiling to identify unnecessary renders:

// Add <React.Profiler> at key points
function App() {
  const handleRender = (id, phase, actualDuration) => {
    console.log(`Component ${id} rendered in ${actualDuration}ms`);
  };
  
  return (
    <React.Profiler id="Navigation" onRender={handleRender}>
      <Navigation />
    </React.Profiler>
  );
}

2. why-did-you-render Library

Automatically shows preventable re-renders:

// Setup in wdyr.js file
import React from 'react';
 
if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}
 
// Usage in component
function ExampleComponent(props) {
  // ... component code
}
 
ExampleComponent.whyDidYouRender = true;

Common Mistakes

1. Premature Optimization

// ❌ Too early: optimizing simple components
const Text = React.memo(function Text({ content }) {
  return <p>{content}</p>;
});
 
// ✅ Correct: only optimize components with complex logic or deep trees
const ComplexChart = React.memo(function ComplexChart({ data }) {
  // Complex calculations and large component tree
  return <ChartComponent data={data} />;
});

2. Dependencies in useCallback/useMemo Arrays

// ❌ Incorrect: missing dependencies
function UserList({ users, onUserSelect }) {
  const handleClick = useCallback((userId) => {
    console.log(`Selected: ${userId}`);
    onUserSelect(userId);
  }, []); // Missing onUserSelect dependency
  
  // ... component code
}
 
// ✅ Correct: all dependencies included
function UserList({ users, onUserSelect }) {
  const handleClick = useCallback((userId) => {
    console.log(`Selected: ${userId}`);
    onUserSelect(userId);
  }, [onUserSelect]);
  
  // ... component code
}

3. Improper React.memo Usage

// ❌ Inefficient: object created on every parent render
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      <MemoizedChild 
        data={{ value: 42 }} // New object on every render
      />
    </div>
  );
}
 
const MemoizedChild = React.memo(function Child({ data }) {
  console.log("Child renders despite memo");
  return <div>{data.value}</div>;
});
 
// ✅ Efficient: stable object reference
function Parent() {
  const [count, setCount] = useState(0);
  
  // Object created once
  const data = useMemo(() => ({ value: 42 }), []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      <MemoizedChild data={data} />
    </div>
  );
}

Summary

Main techniques for preventing unnecessary renders:

  • React.memo for functional components
  • useMemo for caching computed values
  • useCallback for stable functions
  • PureComponent for class components
  • Splitting components by update frequency
  • Avoiding creation of new objects/arrays during render
  • Splitting context into smaller pieces

Recommendations:

  • Don’t optimize prematurely
  • Use profiling tools to identify issues
  • Pay attention to dependencies in hooks
  • Apply optimizations first to components that re-render most frequently

Proper application of these techniques can significantly improve React application performance, especially when working with large data lists or complex interfaces. 🚀


Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site, and improve yourself every day 💪