useCallback and useMemo are both memoization hooks in React, but they serve fundamentally different purposes and operate on different types of values.
useCallback(fn, deps) — returns a memoized function reference that only changes when one of its dependencies changes. The function body itself is not re-created on every render.
useMemo(() => value, deps) — returns a memoized computed value (the return value of the function you pass to it). The computation only runs again when dependencies change.
How they work under the hood: When a component re-renders, React evaluates the dependency array of both hooks. If all dependencies are the same (by Object.is comparison), the hook returns its cached result without re-computing. If any dependency has changed, the hook re-runs and stores the new result.
Key difference in one sentence:
When to use useCallback:
When to use useMemo:
Complete code example:
1import { useState, useCallback, useMemo, memo } from "react";23// Memoized child — only re-renders when props change4const TodoList = memo(({ todos, onDelete }) => {5 console.log("TodoList rendered");6 return (7 <ul>8 {todos.map(todo => (9 <li key={todo.id}>10 {todo.text}11 <button onClick={() => onDelete(todo.id)}>Delete</button>12 </li>13 ))}14 </ul>15 );16});1718function App() {19 const [todos, setTodos] = useState([20 { id: 1, text: "Learn React" },21 { id: 2, text: "Build a project" },22 ]);23 const [filter, setFilter] = useState("");2425 // useCallback: stable function reference26 const handleDelete = useCallback((id: number) => {27 setTodos(prev => prev.filter(t => t.id !== id));28 }, []); // empty deps — function never changes2930 // useMemo: expensive filtered/sorted computation31 const filteredTodos = useMemo(() => {32 console.log("Filtering todos...");33 return todos34 .filter(t => t.text.toLowerCase().includes(filter.toLowerCase()))35 .sort((a, b) => a.text.localeCompare(b.text));36 }, [todos, filter]);3738 return (39 <div>40 <input41 value={filter}42 onChange={e => setFilter(e.target.value)}43 placeholder="Filter todos..."44 />45 <TodoList todos={filteredTodos} onDelete={handleDelete} />46 </div>47 );48}
Common mistake — unnecessary memoization: Not every function or value needs to be memoized. Overuse adds overhead (hook comparison logic, closure maintenance) that can actually hurt performance for simple cases.
Rule of thumb: Only use these hooks when you have measurable performance issues — typically visible in React DevTools Profiler as unnecessary re-renders of expensive child components.