Main ways to prevent unnecessary renders in React:
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>
);
}
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. 🚀
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>
);
}
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
);
}
);
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>
);
}
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>
);
});
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>
);
}
}
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>
);
});
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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;
// ❌ 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} />;
});
// ❌ 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
}
// ❌ 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>
);
}
✅ Main techniques for preventing unnecessary renders:
React.memo
for functional componentsuseMemo
for caching computed valuesuseCallback
for stable functionsPureComponent
for class components✅ Recommendations:
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 💪