AiTechWorlds
AiTechWorlds
useMemo and useCallback are hooks that cache a value or function across renders so React doesn't recompute it unless its inputs actually changed.
Think of a chef who preps a sauce once at the start of service and ladles it out for every order, instead of remaking it from scratch each time a ticket comes in. The sauce (a memoized value) is only remade when an ingredient changes. Most component renders are fast enough that you never need this — but expensive work like filtering 10,000 items or unnecessary child re-renders is exactly the "remaking the sauce every ticket" problem these two hooks solve.
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