AiTechWorlds
AiTechWorlds
React re-renders components when state or props change. Most of the time this is fast enough and you shouldn't think about it. But some operations are expensive — filtering 10,000 items, computing statistics, creating complex objects — and some component trees re-render unnecessarily. useMemo and useCallback are the tools to address both.
function ProductList({ products, searchQuery }: Props) {
// This runs on EVERY render — even if products and searchQuery didn't change
const filtered = products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => a.price - b.price);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
If this component's parent re-renders for unrelated reasons (say, a header updates), filtered gets recalculated from scratch even if products and searchQuery haven't changed. For small datasets this doesn't matter. For thousands of products with complex filtering, it adds up.
useMemo caches a calculated value and only recomputes it when its dependencies change:
import { useMemo } from "react";
function ProductList({ products, searchQuery, category }: Props) {
const filtered = useMemo(() => {
return products
.filter(p => !category || p.category === category)
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => a.price - b.price);
}, [products, searchQuery, category]); // only recompute when these change
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
The array at the end is the dependency array — identical to useEffect. If any dependency changes, the memo is invalidated and the function runs again.
Functions are recreated on every render. When a function is passed as a prop to a child component, the child sees a new function reference on every parent render, causing the child to re-render even if nothing actually changed:
// Without useCallback
function Parent() {
const [count, setCount] = useState(0);
// New function reference on every render
const handleClick = () => console.log("clicked");
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment {count}</button>
<ExpensiveChild onClick={handleClick} />
</div>
);
}
Every time count changes, Parent re-renders, creating a new handleClick function. If ExpensiveChild uses React.memo, it checks if props changed — but handleClick IS a new function, so it still re-renders.
import { useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
// Same function reference unless dependencies change
const handleClick = useCallback(() => {
console.log("clicked");
}, []); // empty array = created once
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment {count}</button>
<ExpensiveChild onClick={handleClick} />
</div>
);
}
Now handleClick has the same reference across renders unless its dependencies change. Paired with React.memo on ExpensiveChild, the child genuinely won't re-render.
// Without memo — re-renders whenever parent re-renders
function CourseCard({ course }: { course: Course }) {
return (
<div className="card">
<h3>{course.title}</h3>
<p>${course.price}</p>
</div>
);
}
// With memo — only re-renders if course prop changes
const CourseCard = React.memo(function CourseCard({ course }: { course: Course }) {
return (
<div className="card">
<h3>{course.title}</h3>
<p>${course.price}</p>
</div>
);
});
React.memo does a shallow comparison of props. If all props pass === equality, the component skips rendering. This is where useCallback matters — without it, a function prop will always fail equality even if the function does the same thing.
// A filtered list with many items and an expensive child component
const CourseCard = React.memo(function CourseCard({
course,
onEnroll
}: {
course: Course;
onEnroll: (id: string) => void;
}) {
console.log(`Rendering: ${course.title}`);
return (
<div>
<h3>{course.title}</h3>
<button onClick={() => onEnroll(course.id)}>Enroll</button>
</div>
);
});
function CoursesPage({ courses }: { courses: Course[] }) {
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const [cart, setCart] = useState<string[]>([]);
// Memoize the filtered list
const filtered = useMemo(() => {
return courses
.filter(c => category === "all" || c.category === category)
.filter(c => c.title.toLowerCase().includes(search.toLowerCase()));
}, [courses, category, search]);
// Memoize the callback (stable reference for React.memo to work)
const handleEnroll = useCallback((id: string) => {
setCart(prev => [...prev, id]);
}, []); // setCart is stable, no deps needed
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<div className="grid grid-cols-3 gap-4">
{filtered.map(course => (
<CourseCard key={course.id} course={course} onEnroll={handleEnroll} />
))}
</div>
</div>
);
}
Now: changing search or category recalculates filtered and re-renders the matching cards. Changing cart (after enrolling) does NOT trigger course card re-renders.
This is important: memoization has a cost. Every useMemo and useCallback adds overhead — React has to store the cached value and compare dependencies on every render.
Don't use useMemo for:
Don't use useCallback for:
React.memoThe rule: profile first, optimize second. React DevTools has a Profiler tab that shows which components are slow and why. Don't add useMemo everywhere preemptively — it makes code harder to read and may not help.
Look for components rendering more than expected, or renders taking more than a few milliseconds. Those are your actual bottlenecks.
Next lesson: useRef and forwarding refs — accessing DOM nodes and storing mutable values.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises