React useCallback Hook


React's useCallback Hook is a powerful tool to optimize performance and prevent unnecessary re-renders. In this guide, you’ll learn:

  • What useCallback does
  • When and why to use it
  • Real-world example
  • Common pitfalls and tips

What is useCallback?

The useCallback Hook returns a memoized version of a function.

const memoizedCallback = useCallback(() => {
  // your logic here
}, [dependencies]);

This means the function reference will not change unless the dependencies change.


Why Use useCallback?

In React, every time a component re-renders, all its functions get recreated. This causes problems if:

  • You're passing functions as props to child components
  • Those child components rely on React.memo to avoid re-renders

Using useCallback ensures that the function keeps the same reference unless its dependencies actually change.


Problem: Unwanted Re-Renders

Let’s look at an example without useCallback.

index.js

import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";
 
const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
 
  const increment = () => setCount((c) => c + 1);
 
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };
 
  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Todos.js

import { memo } from "react";
 
const Todos = ({ todos, addTodo }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2> 
      {todos.map((todo, index) => (
        <p key={index}>{todo}</p>
      ))}
      <button onClick={addTodo}>Add Todo</button>
    </>
  );
};
 
export default memo(Todos);

🔄 Try it out Click the Count + button. Notice how Todos re-renders even though todos didn’t change?

Why Is This Happening?

This is due to referential equality.

Every re-render of App creates a new version of the addTodo function, even if its logic is identical. So React.memo sees a new function and re-renders the Todos component.


Solution: useCallback

Wrap the function with useCallback to preserve the reference:

index.js (fixed)

import { useState, useCallback } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";
 
const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
 
  const increment = () => setCount((c) => c + 1);
 
  const addTodo = useCallback(() => {
    setTodos((t) => [...t, "New Todo"]);
  }, [todos]); // 👈 dependencies
 
  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
 

Now what happens?

Now when you click the Count + button:

✅ The Todos component does not re-render ✅ addTodo has the same reference ✅ Performance is improved


Gotcha: Dependency Array

Be careful with what you include in the dependency array.

const addTodo = useCallback(() => {
  setTodos((t) => [...t, "New Todo"]);
}, [todos]); // This will recreate the function when todos change

💡 If you use the updater function (setTodos(prev => ...)), you can often pass an empty array:

const addTodo = useCallback(() => {
  setTodos(t => [...t, "New Todo"]);
}, []); // now truly stable unless setTodos changes

Summary

  • useCallback helps prevent unnecessary re-renders
  • It memorizes the function so its reference doesn’t change
  • Ideal when passing functions to React.memo components
  • Use with care—always manage the dependency array properly