useCallback
In React, whenever a component re-renders, all functions defined inside it are recreated from scratch. Usually, this is fine and incredibly fast. However, sometimes recreating a function can cause performance issues or logic bugs. This is where useCallback comes in.
useCallback is a React Hook that lets you cache a function definition between re-renders.
Understanding Referential Equality
In JavaScript, two objects or functions are only strictly equal (===) if they share the exact same reference in memory.
const func1 = () => console.log('hello');
const func2 = () => console.log('hello');
console.log(func1 === func2); // false
When React re-renders a component, it creates a new function reference. If you pass this function as a prop to a child component, the child sees a "new" prop and might re-render unnecessarily.
Syntax
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b], // Dependency array
);
React will return the same function instance as long as the dependencies (a and b) haven't changed.
When to use useCallback
1. Passing callbacks to highly optimized child components
If you wrap a child component in React.memo(), it will only re-render if its props change. If you pass an unmemoized function to it, the React.memo optimization is broken because the function prop changes every render.
import { useState, useCallback, memo } from 'react';
const ExpensiveChart = memo(({ onDataClick }) => {
// Expensive rendering logic...
return <div onClick={onDataClick}>Chart</div>;
});
function Dashboard() {
const [count, setCount] = useState(0);
// Without useCallback, ExpensiveChart would re-render every time `count` changes!
const handleDataClick = useCallback(() => {
console.log('Chart clicked');
}, []); // Empty array means this function never changes
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<ExpensiveChart onDataClick={handleDataClick} />
</div>
);
}
2. When a function is a dependency in useEffect
If you call a function inside useEffect, you must include it in the dependency array. If that function is recreated every render, it will trigger the effect continuously, potentially causing infinite loops.
import { useState, useEffect, useCallback } from 'react';
function Search({ query }) {
const [results, setResults] = useState([]);
// Memoize the fetch function so it only changes when `query` changes
const fetchData = useCallback(async () => {
const data = await api.search(query);
setResults(data);
}, [query]);
useEffect(() => {
fetchData();
}, [fetchData]); // Safe to include as dependency now
return <div>...</div>;
}
When NOT to use useCallback
Do not blindly wrap every function in useCallback.
- Creating the
useCallbackhook itself has a small performance cost (React has to run the hook and check dependencies). - It makes your code harder to read.
- If a child component is not wrapped in
React.memo, passing auseCallbackfunction to it does nothing to prevent re-renders, as the child will re-render anyway when the parent re-renders.
Use it only when you have a measured performance problem or are managing specific referential equality dependencies!