React Mastery / Advanced React Hooks

useCallback Hook

The useCallback hook is a staple of React performance optimization. At its core, it’s about memoization—specifically, ensuring that a function instance remains the same between renders unless its dependencies change.


❓What is useCallback?

In React, every time a component re-renders, all functions defined inside it are "re-created." While JavaScript handles this quickly, it causes issues when those functions are passed as props to optimized child components.

useCallback returns a memoized version of the callback function that only changes if one of the dependencies has changed.

The Syntax

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // Dependency array
);

🧐 Why do we need it?

To understand useCallback, you have to understand Referential Equality.

In JavaScript:

  • {} === {} is false.

  • () => {} === () => {} is false.

Even if two functions do the exact same thing, they occupy different spots in memory. When you pass a function to a child component, the child sees a "new" prop every time the parent renders, which triggers a re-render of the child—even if the child is wrapped in React.memo.


🧠 The Core Use Cases

A. Preventing Unnecessary Child Re-renders

This is the most common use case. When a child component is wrapped in React.memo, it only re-renders if its props change. Without useCallback, the function prop "changes" every time.

B. Functions as Dependencies

If you have a function that is used inside a useEffect hook, React will warn you to include that function in the dependency array. If you don't wrap the function in useCallback, the useEffect will run on every single render.


🔴 Common Pitfalls & Best Practices

  • Don't Overuse It: Memoization has an overhead cost. If you're not passing the function to a memoized child or using it in a dependency array, useCallback actually makes your code slightly slower and more complex.

  • Dependency Array Honesty: Always include every reactive value (state, props) used inside the function. If you skip one, your function will use "stale" variables from a previous render.

  • Pairs with React.memo: Using useCallback on a prop is useless unless the child component is actually optimized with React.memo.


🚀 Practical Example

Here is how it looks in a real-world scenario where a button component shouldn't re-render needlessly:

1. The "Problem" Component: ShippingButton

const ShippingButton = React.memo(({ onClick }) => {
  console.log("Button rendered");
  return <button onClick={onClick}>Update Shipping</button>;
});
  • React.memo: This is a "Higher Order Component." It tells React: "Only re-render this button if its props (like onClick) actually change."

  • The Console Log: This is our diagnostic tool. If you see "Button rendered" in your console when you haven't touched the shipping settings, it means your optimization failed.


2. The State Trigger: count

const [count, setCount] = useState(0);
// ...
<button onClick={() => setCount(count + 1)}>Add to Cart</button>
  • When you click "Add to Cart," the count state updates.

  • Crucial Concept: When state updates, the entire ProductPage function runs again from top to bottom.


3. The Solution: useCallback

Without useCallback, the handleShipping function would be "new" (a different spot in memory) on every single click of the "Add to Cart" button. This would trick React.memo into thinking the props changed, causing the ShippingButton to re-render needlessly.

const handleShipping = useCallback(() => {
  console.log("Processing product:", productId);
}, [productId]);
  • The Memoization: React stores this function definition in a special cache.

  • The Dependency Array [productId]:

    • If productId stays the same (e.g., "iPhone-15"), React keeps returning the exact same function instance from the cache.

    • Because the instance is identical, React.memo looks at the onClick prop, sees it hasn't changed, and skips re-rendering the button.

    • If you switch to a different product page (changing productId), React throws away the old function and creates a new one to ensure the logic stays fresh.


Summary of the "Flow"

  1. User clicks "Add to Cart": State (count) changes.

  2. ProductPage re-renders: React encounters useCallback.

  3. Stability Check: React sees productId hasn't changed $\rightarrow$ it returns the old handleShipping reference.

  4. Prop Comparison: ShippingButton (wrapped in memo) compares its new onClick prop to its old one.

  5. The Result: Since they are the same instance, the button does not re-render.

This saves CPU cycles and prevents the UI from flickering or lagging in complex applications.