When working with React, it's common to run into performance issues caused by unnecessary component re-renders. This usually happens when child components are being re-rendered even if their props haven't changed.
Luckily, React gives us two simple tools to optimize this: React.memo and useCallback.
In this post, I'll explain how they work and when to use them — with practical examples.
✅ The Problem: Too many re-renders
Imagine you have a parent component that updates state frequently, and a child component that receives a prop (like a callback function). Even if the child doesn't need to re-render, it will — because the function prop is re-created on every render.
Here’s a simplified version of that scenario:
import { useState } from 'react';
function MyButton({ onClick }: { onClick: () => void }) {
console.log('Button rendered');
return <button onClick={onClick}>Click me</button>;
}
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Clicked');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<MyButton onClick={handleClick} />
</div>
);
}
Even if handleClick doesn’t change, it’s recreated on every render — causing MyButton to re-render too.
💡 The Solution: React.memo + useCallback
React.memo
React.memo is a higher-order component that memoizes a component, preventing unnecessary re-renders if the props haven't changed.
const MyButton = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('Button rendered');
return <button onClick={onClick}>Click me</button>;
});
But this alone isn't enough — if you're passing a new function on every render, it still counts as a new prop.
useCallback
That's where useCallback comes in. It memoizes the function itself, so its reference only changes when its dependencies change.
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // empty deps = function is stable
Together, these two tools ensure that MyButton won't re-render unless it actually needs to.
🔧 Full Example
import React from 'react';
import { useState, useCallback } from 'react';
const MyButton = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('Button rendered');
return <button onClick={onClick}>Click me</button>;
});
export function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<MyButton onClick={handleClick} />
</div>
);
}
✅ Now MyButton only renders once unless handleClick or other props change.
🧠 When to use (and when not to)
-
Use
React.memowhen your component:- Receives stable props (like functions or values that rarely change).
- Is pure and doesn't depend on external state.
- Is relatively expensive to render.
-
Use
useCallbackwhen:- You're passing functions as props to memoized children.
- You want to keep a stable function reference between renders.
⚠️ Avoid overusing these tools — they add some complexity. Use them when profiling shows re-renders are hurting performance.
🚀 Final Thoughts
React.memo and useCallback are powerful tools to improve performance and avoid unnecessary rendering. Together, they help keep your app efficient and responsive.
Have you used React.memo or useCallback in your projects?
Let me know how — or if you’ve run into any gotchas! 👇
🔗 Follow me for more practical React + TypeScript tips!
Top comments (0)