React.memo
React.memo is a higher-order component that skips re-rendering when props haven't changed (shallow comparison by default).
import { memo } from "react";
interface UserCardProps {
name: string;
email: string;
}
const UserCard = memo(function UserCard({ name, email }: UserCardProps) {
console.log("UserCard rendered");
return (
<div className="card">
<h3>{name}</h3>
<p>{email}</p>
</div>
);
});
// Parent re-renders won't cause UserCard to re-render
// unless name or email actually change
function UserList() {
const [filter, setFilter] = useState("");
return (
<div>
<input value={filter} onChange={(e)=> setFilter(e.target.value)} />
<UserCard name="Alice" email="alice@example.com" />
</div>
);
}Custom Comparison
You can provide a custom comparison function when shallow equality isn't enough:
const Chart = memo(
function Chart({ data, config }: ChartProps) {
return <canvas ref={renderChart(data, config)} />;
},
(prevProps, nextProps) => {
// Only re-render if data length changed or config.type changed
return (
prevProps.data.length === nextProps.data.length &&
prevProps.config.type === nextProps.config.type
);
}
);useMemo
useMemo caches the result of an expensive computation between re-renders.
function ProductList({ products, filter }: ProductListProps) {
// Without useMemo, this filters on EVERY render
const filteredProducts = useMemo(() => {
return products.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
// Expensive sort — only recalculated when filteredProducts changes
const sortedProducts = useMemo(() => {
return [...filteredProducts].sort((a, b) => b.rating - a.rating);
}, [filteredProducts]);
return (
<ul>
{sortedProducts.map((p) => (
<li key={p.id}>{p.name} — {p.rating}</li>
))}
</ul>
);
}Stabilizing Object References
useMemo is also useful for keeping object references stable so child memo components don't re-render:
function Dashboard({ userId }: { userId: string }) {
const [refreshCount, setRefreshCount] = useState(0);
// Without useMemo, a new object is created every render,
// breaking React.memo on SettingsPanel
const settings = useMemo(
() => ({ theme: "dark", userId }),
[userId]
);
return (
<div>
<button onClick={()=> setRefreshCount((c)=> c + 1)}>Refresh</button>
<SettingsPanel settings={settings} />
</div>
);
}
const SettingsPanel = memo(function SettingsPanel({
settings,
}: {
settings: { theme: string; userId: string };
}) {
console.log("SettingsPanel rendered");
return <div>Theme: {settings.theme}</div>;
});useCallback
useCallback memoizes a function reference. It is essentially useMemo for functions.
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
// Without useCallback, a new function is created every render,
// causing TodoItem (wrapped in memo) to re-render every time
const handleDelete = useCallback((id: string) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
}, []);
const handleToggle = useCallback((id: string) => {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}, []);
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
onToggle={handleToggle}
/>
))}
</ul>
);
}
const TodoItem = memo(function TodoItem({
todo,
onDelete,
onToggle,
}: TodoItemProps) {
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={()=> onToggle(todo.id)}
/>
{todo.text}
<button onClick={()=> onDelete(todo.id)}>Delete</button>
</li>
);
});When NOT to Memoize
Memoization has a cost — memory for the cached value and comparison overhead on every render. Avoid it when:
- The component is cheap to render. Memoizing a
<span>is slower than just re-rendering it. - Props change on every render anyway.
memowill run the comparison and re-render regardless. - You're wrapping everything "just in case." Profile first, optimize second.
// BAD: Pointless memoization — primitive props change every render
function Counter() {
const [count, setCount] = useState(0);
// count changes every click, so useMemo does nothing useful
const doubled = useMemo(() => count * 2, [count]);
return <span>{doubled}</span>;
}
// GOOD: Just compute it inline — multiplication is trivial
function Counter() {
const [count, setCount] = useState(0);
return <span>{count * 2}</span>;
}The Memoization Decision Checklist
Before reaching for memo, useMemo, or useCallback, ask:
- Is the component actually slow? Use React Profiler to measure.
- Can you lift state down or restructure to avoid the re-render entirely?
- Do the dependencies change frequently? If so, memoization won't help.
- Is the child wrapped in
React.memo?useCallbackonly helps if the child checks prop equality.