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.memo
when 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
useCallback
when:- 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)