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