
What is useCallback Hook in React?
Learn everything about the useCallback hook in React - memoization, syntax, use cases, examples, useCallback vs useMemo, interview questions - in one article.

Kaushal Joshi
Feb 13, 2025 • 13 min read
When I started learning React, I was quite confident at first. Everything I was learning— component rerendering, state and props, basic hooks, etc.— seemed simple at first. Until one day, I came across memoization. That day, I felt like I was stuck in my React journey.
Optimization becomes an important part of the application if you are building a large-scale application with complex logic, data flow, and user interaction. In terms of React, this means controlling what components get rerendered and whether you can prevent it. React provides a built-in hook, useCallback()
, that memoizes a function, preventing unnecessary recreation and reducing component re-renders, thereby improving performance.
In this blog, let's dive deep into one of the most important hooks in React - useCallback. We will start from the basics and learn about memoization, and different memoization methods in React, and then learn about the useCallback hook. We'd learn what it is, how to write it, when to use it, and how to use it, and end our discussion with some interview questions.
Prerequisites and Assumptions
The useCallback()
hook is an advanced hook in React.js. Hence, I assume you're well familiar with basic React concepts like functional components, states, props, etc. If you are just getting started with React, I'd highly recommend saving this article for now and coming back to it later.
That being said, let's start with knowing what is memoization.
What is Memoization?
Memoization is an optimization technique that improves performance by caching previously computed results. When the function is called with the same inputs, it returns the cached results instead of recomputing them.
This works because of closures in JavaScript. Closures allow a function to "remember" the variables from its outer scope even after the outer function has finished executing. In the context of memoization, this means the cached results persist across multiple function calls, ensuring we don’t recompute values unnecessarily.
Let's see a simple example of Memoization in JavaScript:
function memoizedAdd() {
let cache = {};
return function(a, b) {
const key = `${a}-${b}`;
if (cache[key]) return cache[key];
cache[key] = a + b;
return cache[key];
};
}
const add = memoizedAdd();
console.log(add(2, 3)); // Computes and stores result
console.log(add(2, 3)); // Returns cached result
This is how it works:
Check the cache: Check if the result for the given set of inputs exists in the cache.
Compute if needed: If the result is not cached, compute and store it.
Return cached result: When the function is called again with the same inputs, return the cached results instead of recomputing them.
Memoization is a general programming concept and is not limited to JavaScript, React, or front-end development.
Memoization in React
In React, memoization helps optimize performance by preventing unnecessary rerenders and recomputations. React provides built-in hooks like useMemo()
and useCallback()
and a memo()
function that memoizes different parts of the component.
memo()
: If you wrap a functional component withmemo()
, the component will rerender only if the props are changed. Otherwise, it'd skip the rerender.useMemo()
: This built-in hook caches the result of an expensive computation and only recalculates it when dependencies change.useCallback()
: This built-in hook memoizes a function so it doesn’t get recreated on every render.
Now as our basics are clear, let's see what the useCallback()
hook is all about.
What is the useCallback Hook in React?
The useCallback
hook in React memoizes a function, ensuring that its reference remains stable across renders unless its dependencies change. This helps optimize performance by preventing unnecessary recreations of the function, which results in the rendering of the components.
Parameters of useCallback
fn
: This is the function that you want to memoize. The function could be any valid JavaScript function.dependencies
: This is an array of all reactive values referenced inside of the function passed as the first parameter. Reactive values include props, states, context values, and all other variables and functions declared directly inside your component body. Similar touseEffect
, you should only add those values that are being used inside the function.
The return value of useCallback
On the initial render, the useCallback hook returns the function passed as the first parameter itself.
During the next render, it will first check if the dependencies have changed. If so, it will return a new function based on new values.
If dependencies are not changed, it will return the already stored function from the last render.
The
useCallback
hook does not call the function. It simply stores ("caches") the function definition and returns the same function reference unless dependencies change.
Caveats with useCallback
It's crucial to add a dependency array to the useCallback hook. The values inside the list decide when the function should be recreated. If you miss a value, the function won't be recreated. On the other hand, if you add additional values, the function will be recreated unnecessarily.
If you have passed an empty dependency array, the function will be frozen on the first render itself. On all future renders, the same function from the first render will be reused.
First useCallback example
Let's see a simple example to understand useCallback hook.
import { useState, useCallback, memo } from "react";
const Button = memo(({ onClick }) => {
return <button onClick={onClick}>Increment count</button>;
});
export default function Counter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Button onClick={increment} />
</div>
)
}
Let's see what the code does...
The
<Button />
component is wrapped inmemo()
. That means it'd not rerender if the props are unchanged.We have wrapped
increment()
insideuseCallback
with an empty dependency array. This ensures that the function reference remains the same across re-renders, preventing unnecessary recreation.We pass the
increment()
function to<Button />
component as a prop. Here, we ensure that the button component doesn't rerender when thecount
prop changes.
Real World Example of useCallback
Now, let's see a real-world example of the useCallback hook where memoizing a function makes sense.
Imagine you have a large list of users and a search input that filters the list. A separate component <UserList />
takes the list of filtered users and renders them on the screen.
import { useState } from "react";
const users = ["Alice", "Bob", "Charlie", "David", "Eve"];
function UserList({ filter }: { filter: (user: string) => boolean }) {
console.log("UserList rerendered"); // Rerenders every time
return (
<ul>
{users.filter(filter).map((user) => (
<li key={user}>{user}</li>
))}
</ul>
);
}
export default function App() {
const [searchQuery, setSearchQuery] = useState("");
// filterUsers is recreated on every render
const filteredUsers = (user: string) => user.toLowerCase().includes(searchQuery.toLowerCase());
return (
<div>
<input
type="text"
placeholder="Search users..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<UserList filter={filteredUsers} />
</div>
);
}
The filteredUsers()
should be optimized because it is passed down to a <UserList />
component. If we don't use useCallback
, the filtering function will be recreated on every render, causing <UserList />
to rerender unnecessarily.
Let's optimize this snippet by ensuring that filteredUsers
only changes when searchQuery
updates. Additionally, we'll wrap <UserList />
with memo()
so it re-renders only when its filter
prop changes.
import { useState, memo, useCallback } from "react";
const users = ["Alice", "Bob", "Charlie", "David", "Eve"];
// Memoized UserList to prevent unnecessary renders
const UserList = memo(({ filter }: { filter: (user: string) => boolean }) => {
console.log("UserList rerendered"); // Now only rerenders when filter changes
return (
<ul>
{users.filter(filter).map((user) => (
<li key={user}>{user}</li>
))}
</ul>
);
});
export default function App() {
const [searchQuery, setSearchQuery] = useState("");
// Memoized function, only changes when searchQuery changes
const filteredUsers = useCallback(
(user: string) => user.toLowerCase().includes(searchQuery.toLowerCase()),
[searchQuery]
);
return (
<div>
<input
type="text"
placeholder="Search users..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<UserList filter={filteredUsers} />
</div>
);
}
Let's understand this.
filteredUsers
is not recreated unlesssearchQuery
changes.As
<UserList />
is wrapped inmemo()
, it will only rerender iffilteredUsers
changes.If the user types quickly,
useCallback
prevents frequent rerenders of<UserList />
, improving efficiency.
Why Functions Are Recreated During Every Render?
By now, you might be wondering: why are functions recreated on every render? Can't React simply skip recreating them? The answer lies in how JavaScript handles functions within components.
All modern React components are functions. When a component renders, it is simply a function call. This means that every time React rerenders a component, it calls the function again from scratch.
Functions declared inside a component are recreated on every render because they exist within that specific function's execution context (scope). Since components in React are just JavaScript functions, they re-run entirely upon re-render, leading to function recreation unless memoized.
Therefore, even if you check prevFunction === newFunction
, it will return false
because each function instance has a different memory reference.
This is why React treats the new function as different from the previous one, even if their implementation is identical.
How does useCallback caches a function?
When a component renders, useCallback
creates, and stores a function reference in React's internal cache. This reference points to the function's memory address.
On subsequent renders, useCallback
checks the dependency array:
If the dependency array is empty (
[]
), or its values haven't changed, React returns the previously cached function reference, avoiding unnecessary recreation.If any dependency has changed,
useCallback
creates a new function and updates the cache, replacing the old reference.
This mechanism prevents redundant function recreations, improving performance, especially when passing functions to child components that rely on reference equality (useEffect
, useMemo
, React.memo
).
Use Cases of useCallback
Preventing unnecessary rerenders of child components
This is the most important use case of the useCallback hook. We saw why this is important in the previous section of this article. Let's see another example of the same.
import { memo, useState, useCallback } from "react";
const Button = memo(({ onClick }) => {
console.log("button rerendered...");
return <button onClick={onClick}>Click me</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Button onClick={handleClick} />
</div>
);
}
The
handleClick
function is wrapped inside useCallback with an empty dependency array. This makes sure that the function is only created during the first render.The
<Button />
component is wrapped withmemo()
, making sure it won't rerender unlessonChange
has changed.This would ensure that
<Button />
renders only once, and does not rerender unnecessarily.
An important thing to note here is that you should wrap the child component with memo()
. Otherwise, the component will rerender even if the prop hasn't changed.
Preventing an effect from firing too soon
Sometimes, you might want to call a function inside an effect. Hence you must add it in its dependency array. This creates a problem because the function will be created every time the component rerenders. And hence, the effect will be triggered on every render as well.
import { useState, useEffect } from "react";
function Counter() {
const [count, setCount] = useState(0);
const logCount = () => {
console.log(`Count: ${count}`);
};
useEffect(() => {
logCount();
}, [logCount]); // logCount changes on every render
return (
<button onClick={() => setCount(count + 1)}>
Increment {count}
</button>
);
}
To resolve this, you can wrap the function in useCallback. This ensures that the function only recreates when the dependency changes. Now, the effect doesn't run on every render. It'd only run when the function changes.
import { useState, useEffect, useCallback } from "react";
function Counter() {
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log(`Count: ${count}`);
}, [count]); // Only recreates when `count` changes
useEffect(() => {
logCount();
}, [logCount]);
return (
<button onClick={() => setCount(count + 1)}>
Increment {count}
</button>
);
}
Memoizing a custom hook
If you are writing a custom hook, you should always wrap all the functions returned by the hook inside useCallback. Consumer components can optimize their code whenever needed.
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
useCallback vs useMemo
We briefly saw another hook called useMemo in one of the first sections of this article. Now you must be wondering what's the difference between these two.
The main difference between useCallback and useMemo is that useMemo caches the result of calling a function whereas useCallback caches the function itself.
useMemo calls the function whenever values in the dependency list updates, and stores the result. useCallback does not call the function you provide. It caches the function you provide so that the function itself doesn't change unless the dependencies are changed. With useCallback, you still need to call the function to get a result.
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Calls your function and caches its result
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
Should You Use useCallback Everywhere?
The short answer is no, you should not.
It's unnecessary to use memorization if your app consists of navigating through different pages and replacing entire sections of a page often. If it's more like a single-page application where the user hardly goes to another page (or does it less often), then you should consider memoization.
Memozation is only useful in two cases:
You pass a function as a prop to a component wrapped in
memo()
.The function is being used as a dependency of some hook.
Apart from these, there are no fancy advantages of using useCallback. Wrapping all functions inside useCallback reduces the code readability. And even if any one dependency from the useCallback changes, it'd cause the entire component to rerender.
Therefore, you should focus on writing optimized code that requires little to no memoization.
When a component wraps other components / HTML elements, accept JSX as children.
Prefer local states and avoid lifting states to parent components unless necessary.
Avoid unnecessary effects that update the state.
Remove unnecessary dependencies and try to use as few dependencies in your effects as possible.
Interview Questions for useCallback
By now you must be very confident in memoization in React and the useCallback hook. If you're preparing for mid to senior-level frontend interviews, useCallback is a hot topic amongst interviewers to test your React knowledge. Here are some interview questions for you to test what you learned today:
Why does React recreate functions on every render? Is it normal?
What is useCallback hook? How does it improve performance?
Difference between useCallback, useMemo, and memo.
Can you use useMemo to memoize a function? Write code and explain.
What would happen if you don't provide a dependency array to useCallback?
Should you wrap all functions in useCallback?
How would you optimize a React app without using useCallback, useMemo, and memo?
Wrapping Up
Today we learned about the useCallback hook in React. It is one of the advanced hooks in React. So it's alright if you didn't fully understand it at first read. Take a break, come back, read again, write some code, and I am sure you'll understand it better.
I hope you found this article helpful. If you did, feel free to share it with your friends and peers. I have written deep technical articles on other React-focused topics that'd help you prepare better.
If you are looking for a new job or want to share a cool project you built with likeminded folks, checkout Peerlist!
All the best for your interviews. Until then, keep grinding.