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.

Tutorials dojo strip

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

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.

// 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);

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);
Tutorials dojo strip
Scroll to Top