Prior CoderTech Studio
By priorcoder
Student

What is useCallback in React? And Why Do You Need It?

When building React applications, components re-render frequently. In many cases, re-rendering is perfectly fine. But sometimes unnecessary re-renders can slow down your application, especially when passing functions to child components.


This is where React’s useCallback hook becomes useful.


The useCallback hook helps you memoize functions, meaning React can reuse the same function instance between renders instead of creating a new one every time.


Understanding useCallback properly is important for writing optimized and scalable React applications.


Understanding the Problem First

Before learning useCallback, you need to understand an important concept:


Functions Are Recreated on Every Render

In JavaScript, every time a component renders, functions inside it are recreated.


Example:


function App() {
  const handleClick = () => {
    console.log("Clicked");
  };

  return <button onClick={handleClick}>Click</button>;
}

Whenever App re-renders:

  • A new handleClick function is created

  • Previous function reference is discarded

  • React treats it as a completely new function

Normally this is okay.

But problems start when:

  • Functions are passed to child components

  • Child components are wrapped with React.memo

  • Expensive rendering exists

  • Dependencies in hooks rely on function references

What is useCallback?

useCallback is a React Hook that memoizes a function.

Instead of recreating the function on every render, React stores the function and reuses it unless dependencies change.


Syntax of useCallback

const memoizedFunction = useCallback(() => {
  // function logic
}, [dependencies]);

Simple Example Without useCallback

import React, { useState } from "react";

function Child({ onClick }) {
  console.log("Child Rendered");

  return <button onClick={onClick}>Click Me</button>;
}

export default function App() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("Button clicked");
  };

  return (
    <div>
      <h1>{count}</h1>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      <Child onClick={handleClick} />
    </div>
  );
}

What Happens Here?

Whenever count changes:

  1. App re-renders

  2. handleClick is recreated

  3. Child receives a new function reference

  4. Child re-renders unnecessarily

Even though the logic inside handleClick never changed.


Optimizing with useCallback

import React, { useState, useCallback } from "react";

function Child({ onClick }) {
  console.log("Child Rendered");

  return <button onClick={onClick}>Click Me</button>;
}

export default function App() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("Button clicked");
  }, []);

  return (
    <div>
      <h1>{count}</h1>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      <Child onClick={handleClick} />
    </div>
  );
}

Now React keeps the same handleClick reference between renders.


But There’s Still a Problem

Even with useCallback, the child component will still re-render unless it is memoized.

That is why useCallback is commonly used together with React.memo.


Using useCallback with React.memo

import React, {
  useState,
  useCallback,
  memo
} from "react";

const Child = memo(({ onClick }) => {
  console.log("Child Rendered");

  return (
    <button onClick={onClick}>
      Click Me
    </button>
  );
});

export default function App() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("Clicked");
  }, []);

  return (
    <div>
      <h1>{count}</h1>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      <Child onClick={handleClick} />
    </div>
  );
}

Now:

  • Parent re-renders

  • handleClick keeps same reference

  • Child sees same props

  • React.memo prevents unnecessary re-render

This is the real power of useCallback.


Why Do You Need useCallback?

1. Prevent Unnecessary Re-renders

This is the most common reason.

Without useCallback, child components receive new function references every render.

With useCallback, React reuses the same function reference.


2. Improve Performance

In large applications:

  • Heavy UI components

  • Lists with many items

  • Complex rendering logic

Unnecessary renders can affect performance.

useCallback helps reduce rendering overhead.


3. Useful with React.memo

React.memo only works properly if props remain the same.

Functions normally get recreated every render, so memoization breaks.

useCallback fixes this.


4. Stable References in Dependencies

Sometimes hooks depend on functions.

Example:

useEffect(() => {
  fetchData();
}, [fetchData]);

If fetchData changes every render, the effect runs again unnecessarily.

Using useCallback stabilizes the function reference.


Example with useEffect

Problem

function App() {
  const fetchData = () => {
    console.log("Fetching...");
  };

  useEffect(() => {
    fetchData();
  }, [fetchData]);
}

Problem:

  • fetchData changes every render

  • useEffect runs repeatedly

Solution with useCallback

import React, {
  useEffect,
  useCallback
} from "react";

function App() {
  const fetchData = useCallback(() => {
    console.log("Fetching...");
  }, []);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return <div>App</div>;
}

Now the effect runs only once.


Understanding Dependencies in useCallback

Dependencies decide when the function should be recreated.

Example:

const handleClick = useCallback(() => {
  console.log(count);
}, [count]);

Here:

  • Function remains same until count changes

  • When count changes, React creates a new function

Empty Dependency Array

useCallback(() => {
  console.log("Hello");
}, []);

This means:

  • Function created once

  • Never recreated

Be careful because it may capture old state values.


Stale Closure Problem

One common mistake with useCallback is stale closures.

Example:

const [count, setCount] = useState(0);

const showCount = useCallback(() => {
  console.log(count);
}, []);

Problem:

  • Dependency array is empty

  • Function captures initial count

  • It always logs 0

Correct version:

const showCount = useCallback(() => {
  console.log(count);
}, [count]);

Difference Between useCallback and useMemo

Many developers confuse them.

useCallback

Memoizes a function.

const memoizedFn = useCallback(() => {
  return "Hello";
}, []);

useMemo

Memoizes a value.

const computedValue = useMemo(() => {
  return expensiveCalculation();
}, []);

Internally useCallback Uses useMemo

You can think of it like this:

const memoizedFn = useMemo(() => fn, deps);

That is essentially what useCallback does internally.


Real World Example

Imagine an e-commerce app.

Parent component:

  • Updates cart count

  • Passes event handlers to product cards

Without useCallback:

  • All product cards re-render

  • Performance drops

With useCallback + React.memo:

  • Only necessary components render

  • UI becomes smoother

When Should You Use useCallback?

Use it when:

  • Passing functions to memoized child components

  • Function references trigger unnecessary renders

  • Hooks depend on stable function references

  • Working with expensive rendering logic

When Should You NOT Use useCallback?

Do NOT overuse it.

Many beginners use it everywhere unnecessarily.

useCallback itself has overhead because React must:

  • Store memoized function

  • Compare dependencies

Sometimes creating a new function is actually cheaper.


Bad Example of Overusing useCallback

const sayHello = useCallback(() => {
  console.log("Hello");
}, []);

If the function is small and not causing re-renders, memoization is unnecessary.

Answers & discussion

Sign in to comment.

No comments yet.