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