βοΈ React Hook: useCallback
β
π Quick Summary β
useCallback
memoizes a function definition so that it keeps the same reference across renders unless dependencies change.- Useful when passing callbacks to child components that depend on reference equality (e.g., wrapped in
React.memo
). - Syntax:tsx
const memoizedFn = useCallback(() => { /* logic */ }, [deps]);
π§ Mental Model β
- Think of
useCallback
as a function factory with a memory. - Without it, every render creates a new function reference β may trigger unnecessary child re-renders.
- With it, React reuses the same function reference if dependencies havenβt changed.
π Key Concepts β
Function Identity
- Functions are objects in JS β new render = new function reference.
useCallback
preserves identity until deps change.
When to Use
- Passing callbacks to memoized children (
React.memo
). - Event handlers that donβt need to change every render.
- Passing callbacks to memoized children (
Dependencies
- If dependencies change, new function is created.
- If
[]
, function is stable forever (likecomponentDidMount
).
Relation to
useMemo
useCallback(fn, deps)
is equivalent touseMemo(() => fn, deps)
.- Difference is semantic intent:
useMemo
β values,useCallback
β functions.
π» Code Examples β
Example 1: Stable Callback for Child Component β
import { useState, useCallback, memo } from "react";
const Child = memo(({ onClick }: { onClick: () => void }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click Me</button>;
});
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<Child onClick={handleClick} />
</div>
);
}
How it works (stepβbyβstep):
Parent
re-renders whencount
changes.- Without
useCallback
,handleClick
would be a new function each time βChild
would re-render unnecessarily. - With
useCallback
,handleClick
keeps the same reference, soChild
doesnβt re-render unless needed.
Example 2: Callback with Dependencies β
import { useState, useCallback } from "react";
export default function SearchBox() {
const [query, setQuery] = useState("");
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
}, []);
return (
<input value={query} onChange={handleChange} placeholder="Search..." />
);
}
How it works (stepβbyβstep):
handleChange
is created once withuseCallback
.- Each keystroke calls the same function reference.
- If dependencies were added, the function would be recreated when they change.
Example 3: Passing Stable Callback with Dependency β
import { useState, useCallback, memo } from "react";
const ExpensiveList = memo(({ filter }: { filter: (item: string) => boolean }) => {
console.log("ExpensiveList rendered");
const items = ["apple", "banana", "cherry"];
return (
<ul>{items.filter(filter).map((i, idx) => <li key={idx}>{i}</li>)}</ul>
);
});
export default function Parent() {
const [letter, setLetter] = useState("a");
const filterFn = useCallback((item: string) => item.startsWith(letter), [letter]);
return (
<div>
<ExpensiveList filter={filterFn} />
<button onClick={() => setLetter("b")}>Show B</button>
</div>
);
}
How it works (stepβbyβstep):
- Parent renders with
letter = "a"
. filterFn
created with dependencyletter
.ExpensiveList
only re-renders whenletter
changes β stable function identity otherwise.
Example 4: useCallback vs useMemo β
const fn1 = useCallback(() => compute(), [deps]);
const fn2 = useMemo(() => () => compute(), [deps]);
- Both are equivalent β
fn1
andfn2
are memoized functions. - Use
useCallback
for functions,useMemo
for computed values.
β οΈ Common Pitfalls & Gotchas β
- β Using
useCallback
everywhere β unnecessary complexity. - β Forgetting dependencies β stale closures (function uses outdated variables).
- β Passing inline functions to memoized children β defeats memoization.
β Best Practices β
- Use it with
React.memo
or heavy child components. - Keep dependency arrays accurate.
- Avoid premature optimization: measure before adding.
- Donβt wrap every function in
useCallback
blindly.
β Interview Q&A β
Q1. Why do we need useCallback
?
A: To memoize function references so they donβt change on every render, preventing unnecessary re-renders in memoized children.
Q2. Difference between useCallback
and useMemo
?
A:
useCallback
β memoizes a function.useMemo
β memoizes a value.
Implementation-wise,useCallback(fn, deps)
isuseMemo(() => fn, deps)
.
Q3. Does useCallback
guarantee no re-render of child?
A: No. It only guarantees the function reference is stable. Child still re-renders if its other props or state change.
Q4. When should you not use useCallback
?
A: For simple inline event handlers in non-memoized children. Wrapping them adds unnecessary complexity.
Q5. Whatβs a stale closure issue in useCallback
?
A: When a callback captures outdated state/props due to missing dependencies in its dependency array.
Q6. Is useCallback
just performance optimization?
A: Yes. It doesnβt add new functionality, only helps optimize renders and reference stability.