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.memo
for component memoization. - Use
useMemo
for value memoization. - Use
useCallback
for 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.memo
prevents theChild
from re-rendering unnecessarily.useMemo
ensures thedata
object reference only changes whencount
changes.useCallback
ensures 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