Hey there! đ
So, youâve mastered useState and useEffect. Now youâre looking at useCallback and wondering, âDo I wrap everything in this?â
Short answer: No. Long answer: Read on. â
I remember when I first started with Hooks, I used useCallback everywhere. I thought I was making my app âfasterâ. Spoiler: I wasnât. I was just adding complexity.
Letâs demystify this hook together.
The Problem: Functions are different every time
In JavaScript, functions are objects. And every time a React component re-renders, all the functions inside it are re-created.
const MyComponent = () => {
// This function is BRAND NEW on every render
const handleClick = () => {
console.log("Clicked!");
};
return <ChildComponent onClick={handleClick} />;
};
Usually, this is fine. Browsers are fast.
But, if ChildComponent is wrapped in React.memo (to prevent unnecessary re-renders), it checks if its props have changed.
Since handleClick is technically a new function every time, React.memo thinks the props changed, and re-renders the child anyway. All that optimization work? Wasted. đ
The Solution: useCallback
useCallback tells React: âHey, keep this function exactly the same between renders, unless something specific changes.â
import { useCallback, useState } from "react";
const MyComponent = () => {
const [count, setCount] = useState(0);
// Now, this function stays the same!
const handleClick = useCallback(() => {
console.log("Clicked!");
}, []); // <--- Dependency array
return <ChildComponent onClick={handleClick} />;
};
Now, ChildComponent sees the exact same function reference. If it uses React.memo, it wonât re-render unnecessary.
When should you actually use it?
Donât use it prematurely. Here is my rule of thumb:
- Passing props to memoized components: If you are passing a function to a big list or a heavy component wrapped in
React.memo, use it. - useEffect dependencies: If a function is in a
useEffectdependency array, wrap it inuseCallbackso it doesnât trigger the effect on every render.
Example: The âToo Many Rendersâ Trap
Imagine you have a useEffect that fetches data when a fetchData function is called.
// WITHOUT useCallback
const fetchData = () => { ... };
useEffect(() => {
fetchData();
}, [fetchData]); // đš Infinite Loop Alert!
Because fetchData is new every render, the effect sees it as âchangedâ and runs again⊠which triggers a re-render⊠which creates a new fetchData⊠and boom. Infinite loop. đ„
Fix it with useCallback:
// WITH useCallback
const fetchData = useCallback(() => { ... }, []);
useEffect(() => {
fetchData();
}, [fetchData]); // â
Safe and sound
Closing
Optimization is a double-edged sword. useCallback has a cost (React has to store the function in memory). Use it when you have a specific performance problem or referential equality issue, not just because it looks cool.
Keep it simple! đ