useCallback and useMemo — are React hooks for performance optimization through memoization:
Simple example of differences:
// useCallback returns a memoized function
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);
// useMemo returns a memoized value
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b]
);
Both hooks solve the problem of unnecessary re-renders and recalculations in React components, but are applied in different situations.
Returns a memoized version of a callback function that only changes when dependencies change:
function ParentComponent() {
const [count, setCount] = useState(0);
// Without useCallback: new function on every render
const handleClick = () => {
console.log(count);
};
// With useCallback: same function until count changes
const memoizedHandleClick = useCallback(() => {
console.log(count);
}, [count]);
return <ChildComponent onClick={memoizedHandleClick} />;
}
const ExpensiveChild = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback Child will re-render
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Empty dependencies = function created once
return (
<>
<ExpensiveChild onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</>
);
}
function SearchComponent() {
const [query, setQuery] = useState('');
const search = useCallback((searchQuery) => {
// Search logic
console.log('Searching for:', searchQuery);
}, []); // Function is not recreated
// useEffect won't trigger on every render
useEffect(() => {
if (query) {
search(query);
}
}, [query, search]);
return <input onChange={(e) => setQuery(e.target.value)} />;
}
Returns a memoized value, recalculating it only when dependencies change:
function ExpensiveComponent({ items }) {
// Without useMemo: recalculation on every render
const expensiveValue = items.reduce((sum, item) => sum + item.value, 0);
// With useMemo: recalculation only when items change
const memoizedValue = useMemo(
() => items.reduce((sum, item) => sum + item.value, 0),
[items]
);
return <div>Total: {memoizedValue}</div>;
}
function DataTable({ data, filter }) {
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [data, filter]);
return (
<table>
{filteredData.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</table>
);
}
function UserProfile({ userId }) {
// New object on every render can trigger unnecessary effects
const userConfig = useMemo(() => ({
id: userId,
theme: 'dark',
permissions: ['read', 'write']
}), [userId]);
// useEffect will only trigger when userId changes
useEffect(() => {
loadUserData(userConfig);
}, [userConfig]);
return <div>User: {userId}</div>;
}
// useCallback returns a function
const memoizedFn = useCallback(() => {
return a + b;
}, [a, b]);
// useMemo returns the result of function execution
const memoizedResult = useMemo(() => {
return a + b;
}, [a, b]);
console.log(typeof memoizedFn); // "function"
console.log(typeof memoizedResult); // "number"
// useCallback
const memoizedCallback = useCallback(
() => doSomething(a, b),
[a, b]
);
// Equivalent to useMemo returning a function
const memoizedCallback = useMemo(
() => () => doSomething(a, b),
[a, b]
);
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
// Handler memoization
const handleChange = useCallback((field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
}, []); // Function created once
// Validation memoization
const isValid = useMemo(() => {
return formData.name.length > 0 &&
formData.email.includes('@') &&
formData.message.length > 10;
}, [formData]);
return (
<form>
<input
value={formData.name}
onChange={handleChange('name')}
/>
<input
value={formData.email}
onChange={handleChange('email')}
/>
<textarea
value={formData.message}
onChange={handleChange('message')}
/>
<button disabled={!isValid}>
Submit
</button>
</form>
);
}
function TodoList({ todos, filter }) {
// Filter memoization
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
// Handler memoization
const toggleTodo = useCallback((id) => {
// Todo toggle logic
}, []);
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
/>
))}
</ul>
);
}
const TodoItem = React.memo(({ todo, onToggle }) => {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
);
});
// ❌ Bad: simple calculations don't need memoization
const sum = useMemo(() => a + b, [a, b]);
// ✅ Good: just calculate
const sum = a + b;
// ❌ Bad: missing dependency
const calculate = useCallback(() => {
return data.length * multiplier; // multiplier not in dependencies
}, [data]);
// ✅ Good: all external variables specified
const calculate = useCallback(() => {
return data.length * multiplier;
}, [data, multiplier]);
// ❌ Pointless: strings and numbers are already compared by value
const name = useMemo(() => 'John', []);
// ✅ Makes sense: objects are compared by reference
const config = useMemo(() => ({ name: 'John' }), []);
✅ useCallback is good for:
✅ useMemo is good for:
❌ Don’t use when:
Remember: premature optimization — is the root of all evil. Use these hooks only when they actually solve performance problems.
Want more interview prep articles? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪