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