React useCallback Hook

The useCallback Hook in React provides a memoized callback function. Think of memoization as a way to cache a value to avoid recalculating it. This helps isolate resource-intensive functions so they don’t run on every render.

The useCallback Hook only re-executes when one of its dependencies changes, which can boost performance.

The useCallback and useMemo Hooks are similar. The key difference is that useMemo returns a memoized value, while useCallback returns a memoized function.




Problem

A common reason to use useCallback is to prevent a component from re-rendering unless its props change.

In this example, you might assume the TaskList component won’t re-render unless the tasks change.


Example

index.js

// index.js
import { useState } from "react";
import ReactDOM from "react-dom/client";
import TaskList from "./TaskList";

const App = () => {
  const [count, setCount] = useState(0);
  const [tasks, setTasks] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTask = () => {
    setTasks((t) => [...t, "New Task"]);
  };

  return (
    <>
      <TaskList tasks={tasks} addTask={addTask} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

TaskList.js

// TaskList.js
import { memo } from "react";

const TaskList = ({ tasks, addTask }) => {
  console.log("child render");
  return (
    <>
      <h2>My Tasks</h2>
      {tasks.map((task, index) => {
        return <p key={index}>{task}</p>;
      })}
      <button onClick={addTask}>Add Task</button>
    </>
  );
};

export default memo(TaskList);

Run this and click the count increment button. You’ll notice the TaskList component re-renders even when the tasks don’t change.

Why doesn’t this work? Even with memo, the TaskList component re-renders because the tasks state and the addTask function remain unchanged when the count is incremented. This happens due to “referential equality”. Every time a component re-renders, its functions get recreated, meaning the addTask function changes.




Solution

To fix this, use the useCallback Hook to prevent the function from being recreated unless necessary.

Use useCallback to stop the TaskList component from re-rendering needlessly


Example

index.js

// index.js
import { useState, useCallback } from "react";
import ReactDOM from "react-dom/client";
import TaskList from "./TaskList";

const App = () => {
  const [count, setCount] = useState(0);
  const [tasks, setTasks] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTask = useCallback(() => {
    setTasks((t) => [...t, "New Task"]);
  }, [tasks]);

  return (
    <>
      <TaskList tasks={tasks} addTask={addTask} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

TaskList.js

Now the TaskList component will only re-render when the tasks prop changes.

// TaskList.js
import { memo } from "react";

const TaskList = ({ tasks, addTask }) => {
  console.log("child render");
  return (
    <>
      <h2>My Tasks</h2>
      {tasks.map((task, index) => {
        return <p key={index}>{task}</p>;
      })}
      <button onClick={addTask}>Add Task</button>
    </>
  );
};

export default memo(TaskList);
Scroll to Top