Performance optimization is an important aspect of building scalable and efficient React applications. As React apps grow in complexity, unnecessary re-renders can significantly impact performance. Memoization helps reduce this problem by caching values or components so that React doesn’t recompute or re-render them unnecessarily.
This post explores memoization in React, focusing on three essential tools: React.memo, useMemo, and useCallback. We’ll explain what memoization means, why it’s useful, and how each of these tools works to optimize your app’s rendering process.
1. Understanding Memoization
1.1 What Is Memoization?
Memoization is an optimization technique where the result of a function is stored (cached) so that when the same inputs occur again, the cached result is returned instead of recomputing it.
In React, memoization helps avoid expensive re-renders or recalculations when data or props haven’t changed.
For example:
If a parent component re-renders frequently but its child component’s props remain the same, React will still re-render that child unless we use memoization.
1.2 Why Memoization Matters in React
React re-renders a component whenever:
- Its state changes.
- Its props change.
- Its parent component re-renders.
While React’s virtual DOM efficiently minimizes actual DOM updates, re-rendering can still consume unnecessary CPU time, especially in large applications.
Memoization allows you to control when components or calculations should update, leading to improved performance and smoother user experiences.
2. React.memo – Memoizing Functional Components
2.1 What Is React.memo?
React.memo is a higher-order component (HOC) that wraps a functional component to prevent unnecessary re-renders when its props haven’t changed.
It’s similar to PureComponent for class components but works for functional components.
2.2 Basic Syntax
import React from 'react';
const MyComponent = React.memo(function MyComponent(props) {
return <h1>{props.title}</h1>;
});
export default MyComponent;
When you wrap a component in React.memo, React will compare the new props with the previous props.
If the props are the same, React will skip re-rendering the component.
2.3 Example: Without React.memo
function Child({ value }) {
console.log('Child rendered');
return <p>Value: {value}</p>;
}
function Parent() {
const [count, setCount] = React.useState(0);
const [text, setText] = React.useState('');
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input onChange={(e) => setText(e.target.value)} />
<Child value="Static Value" />
</div>
);
}
In this example, every time you change the text input or click the button, the Child component re-renders—even though its props never change.
2.4 Example: With React.memo
const Child = React.memo(({ value }) => {
console.log('Child rendered');
return <p>Value: {value}</p>;
});
Now, the Child component will not re-render unless the value prop changes.
2.5 Using a Custom Comparison Function
By default, React.memo performs a shallow comparison of props.
If you want deeper control, you can pass a custom comparison function as the second argument.
const Child = React.memo(
({ data }) => {
console.log('Child rendered');
return <p>{data.name}</p>;
},
(prevProps, nextProps) => {
return prevProps.data.name === nextProps.data.name;
}
);
This tells React when it should skip or re-render based on your custom conditions.
2.6 When to Use React.memo
Use React.memo when:
- The component renders the same output for the same props.
- The parent component re-renders often but the child doesn’t need to.
Avoid using React.memo excessively—it adds a small overhead for comparing props. Only apply it when you detect real performance bottlenecks.
3. useMemo – Memoizing Computed Values
3.1 What Is useMemo?
useMemo is a React Hook that memoizes the result of a computation.
It only recomputes the value when one of its dependencies changes.
This is useful when you have expensive calculations or derived data that shouldn’t be recalculated on every render.
3.2 Syntax
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- The first argument is a function that performs the calculation.
- The second argument is a dependency array.
- If dependencies haven’t changed, React reuses the cached result.
3.3 Example Without useMemo
function ExpensiveComponent({ count }) {
const expensiveCalculation = () => {
console.log('Calculating...');
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
};
const total = expensiveCalculation();
return <p>Total: {total}</p>;
}
Here, the expensive calculation runs every time the component re-renders — even if count hasn’t changed.
3.4 Example With useMemo
function ExpensiveComponent({ count }) {
const total = React.useMemo(() => {
console.log('Calculating...');
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
}, [count]);
return <p>Total: {total}</p>;
}
Now, the calculation only runs when count changes, not on every re-render.
3.5 Real-World Example: Filtering Data
function UserList({ users, search }) {
const filteredUsers = React.useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase())
);
}, [users, search]);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This ensures the filtering operation only runs when the search term or the user list changes.
3.6 When to Use useMemo
Use useMemo when:
- You perform expensive calculations.
- You derive new data from props or state.
- You want to avoid recalculations unless necessary.
Avoid it for simple values—it adds overhead if used unnecessarily.
4. useCallback – Memoizing Functions
4.1 What Is useCallback?
useCallback is a React Hook that memoizes callback functions. It ensures that the same function instance is reused between renders unless its dependencies change.
This is useful when passing functions as props to memoized child components.
4.2 Syntax
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
4.3 Example Without useCallback
const Button = React.memo(({ onClick }) => {
console.log('Button rendered');
return <button onClick={onClick}>Click</button>;
});
function App() {
const [count, setCount] = React.useState(0);
const handleClick = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<Button onClick={handleClick} />
</div>
);
}
Even though the Button component is wrapped with React.memo, it still re-renders on every parent update. That’s because handleClick creates a new function on every render.
4.4 Example With useCallback
const Button = React.memo(({ onClick }) => {
console.log('Button rendered');
return <button onClick={onClick}>Click</button>;
});
function App() {
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Button onClick={handleClick} />
</div>
);
}
Now, the handleClick function reference remains stable across renders, preventing unnecessary re-renders of the child component.
4.5 Common Example: Event Handlers in Lists
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
function App() {
const [todos, setTodos] = React.useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Practice useCallback', done: false },
]);
const handleToggle = React.useCallback(
id => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
},
[]
);
return <TodoList todos={todos} onToggle={handleToggle} />;
}
Here, handleToggle is memoized so that it doesn’t change on every render, keeping the list rendering efficient.
5. Comparing React.memo, useMemo, and useCallback
| Feature | Purpose | Used With | Caches |
|---|---|---|---|
| React.memo | Prevents re-render of functional components | Components | Render output |
| useMemo | Memoizes computed values | Inside components | Computation result |
| useCallback | Memoizes callback functions | Inside components | Function reference |
They work together to reduce unnecessary re-renders:
- Use
React.memofor component memoization. - Use
useMemofor value memoization. - Use
useCallbackfor function memoization.
6. Combining Memoization Techniques
You can combine all three techniques for maximum optimization.
Example:
const Child = React.memo(({ onAction, data }) => {
console.log('Child rendered');
return <button onClick={onAction}>{data.label}</button>;
});
function Parent() {
const [count, setCount] = React.useState(0);
const data = React.useMemo(() => ({ label: Count is ${count} }), [count]);
const handleAction = React.useCallback(() => setCount(c => c + 1), []);
return (
<div>
<p>Count: {count}</p>
<Child onAction={handleAction} data={data} />
</div>
);
}
In this example:
React.memoprevents theChildfrom re-rendering unnecessarily.useMemoensures thedataobject reference only changes whencountchanges.useCallbackensures the function reference stays stable.
This combination minimizes unnecessary renders while maintaining reactivity.
7. Common Pitfalls and Misuses
7.1 Overusing Memoization
Memoization adds overhead. Don’t use it everywhere.
Use profiling tools (like React DevTools Profiler) to identify components that actually cause performance bottlenecks.
7.2 Forgetting Dependencies
Always provide correct dependencies for useMemo and useCallback.
If you miss dependencies, your memoized values may become stale.
Bad example:
const memoizedValue = useMemo(() => compute(a, b), []); // Missing dependencies
Good example:
const memoizedValue = useMemo(() => compute(a, b), [a, b]);
7.3 Misunderstanding Shallow Comparison
React.memo does a shallow comparison by default. If your props are objects or arrays, they may always appear “new” even if their content is the same.
To fix this, use useMemo or useCallback to memoize the data or function you pass down.
8. Debugging Performance Issues
Use React DevTools Profiler to visualize component renders.
Steps:
- Open DevTools → Profiler tab.
- Start recording.
- Interact with your app.
- Stop recording and review render counts.
This helps identify components re-rendering unnecessarily, indicating where memoization may help.
9. When Not to Use Memoization
Avoid memoization when:
- The computation is cheap and runs fast.
- Props and state change frequently.
- Component re-renders are not causing performance issues.
Leave a Reply