useCallback is a React Hook that lets you cache a function definition between re-renders.
constcachedFn =useCallback(fn,dependencies)Note
React Compiler automatically memoizes values and functions, reducing the need for manualuseCallback calls. You can use the compiler to handle memoization automatically.
Reference
useCallback(fn, dependencies)
CalluseCallback at the top level of your component to cache a function definition between re-renders:
import{useCallback}from'react';
exportdefaultfunctionProductPage({productId,referrer,theme}){
consthandleSubmit =useCallback((orderDetails)=>{
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
},[productId,referrer]);Parameters
fn: The function value that you want to cache. It can take any arguments and return any values. React will return (not call!) your function back to you during the initial render. On next renders, React will give you the same function again if thedependencieshave not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later. React will not call your function. The function is returned to you so you can decide when and whether to call it.dependencies: The list of all reactive values referenced inside of thefncode. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter isconfigured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like[dep1, dep2, dep3]. React will compare each dependency with its previous value using theObject.iscomparison algorithm.
Returns
On the initial render,useCallback returns thefn function you have passed.
During subsequent renders, it will either return an already storedfn function from the last render (if the dependencies haven’t changed), or return thefn function you have passed during this render.
Caveats
useCallbackis a Hook, so you can only call itat the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.- Reactwill not throw away the cached function unless there is a specific reason to do that. For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache—for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should match your expectations if you rely on
useCallbackas a performance optimization. Otherwise, astate variable or aref may be more appropriate.
Usage
Skipping re-rendering of components
When you optimize rendering performance, you will sometimes need to cache the functions that you pass to child components. Let’s first look at the syntax for how to do this, and then see in which cases it’s useful.
To cache a function between re-renders of your component, wrap its definition into theuseCallback Hook:
import{useCallback}from'react';
functionProductPage({productId,referrer,theme}){
consthandleSubmit =useCallback((orderDetails)=>{
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
},[productId, referrer]);
// ...You need to pass two things touseCallback:
- A function definition that you want to cache between re-renders.
- Alist of dependencies including every value within your component that’s used inside your function.
On the initial render, thereturned function you’ll get fromuseCallback will be the function you passed.
On the following renders, React will compare thedependencies with the dependencies you passed during the previous render. If none of the dependencies have changed (compared withObject.is),useCallback will return the same function as before. Otherwise,useCallback will return the function you passed onthis render.
In other words,useCallback caches a function between re-renders until its dependencies change.
Let’s walk through an example to see when this is useful.
Say you’re passing ahandleSubmit function down from theProductPage to theShippingForm component:
functionProductPage({productId,referrer,theme}){
// ...
return(
<divclassName={theme}>
<ShippingFormonSubmit={handleSubmit}/>
</div>
);You’ve noticed that toggling thetheme prop freezes the app for a moment, but if you remove<ShippingForm /> from your JSX, it feels fast. This tells you that it’s worth trying to optimize theShippingForm component.
By default, when a component re-renders, React re-renders all of its children recursively. This is why, whenProductPage re-renders with a differenttheme, theShippingForm componentalso re-renders. This is fine for components that don’t require much calculation to re-render. But if you verified a re-render is slow, you can tellShippingForm to skip re-rendering when its props are the same as on last render by wrapping it inmemo:
import{memo}from'react';
constShippingForm =memo(functionShippingForm({onSubmit}){
// ...
});With this change,ShippingForm will skip re-rendering if all of its props are thesame as on the last render. This is when caching a function becomes important! Let’s say you definedhandleSubmit withoutuseCallback:
functionProductPage({productId,referrer,theme}){
// Every time the theme changes, this will be a different function...
functionhandleSubmit(orderDetails){
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
}
return(
<divclassName={theme}>
{/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
<ShippingFormonSubmit={handleSubmit}/>
</div>
);
}In JavaScript, afunction () {} or() => {} always creates adifferent function, similar to how the{} object literal always creates a new object. Normally, this wouldn’t be a problem, but it means thatShippingForm props will never be the same, and yourmemo optimization won’t work. This is whereuseCallback comes in handy:
functionProductPage({productId,referrer,theme}){
// Tell React to cache your function between re-renders...
consthandleSubmit =useCallback((orderDetails)=>{
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
},[productId,referrer]);// ...so as long as these dependencies don't change...
return(
<divclassName={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingFormonSubmit={handleSubmit}/>
</div>
);
}By wrappinghandleSubmit inuseCallback, you ensure that it’s thesame function between the re-renders (until dependencies change). You don’thave to wrap a function inuseCallback unless you do it for some specific reason. In this example, the reason is that you pass it to a component wrapped inmemo, and this lets it skip re-rendering. There are other reasons you might needuseCallback which are described further on this page.
Note
You should only rely onuseCallback as a performance optimization. If your code doesn’t work without it, find the underlying problem and fix it first. Then you may adduseCallback back.
Deep Dive
You will often seeuseMemo alongsideuseCallback. They are both useful when you’re trying to optimize a child component. They let youmemoize (or, in other words, cache) something you’re passing down:
import{useMemo,useCallback}from'react';
functionProductPage({productId,referrer}){
constproduct =useData('/product/' +productId);
constrequirements =useMemo(()=>{// Calls your function and caches its result
returncomputeRequirements(product);
},[product]);
consthandleSubmit =useCallback((orderDetails)=>{// Caches your function itself
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
},[productId,referrer]);
return(
<divclassName={theme}>
<ShippingFormrequirements={requirements}onSubmit={handleSubmit}/>
</div>
);
}The difference is inwhat they’re letting you cache:
useMemocaches theresult of calling your function. In this example, it caches the result of callingcomputeRequirements(product)so that it doesn’t change unlessproducthas changed. This lets you pass therequirementsobject down without unnecessarily re-renderingShippingForm. When necessary, React will call the function you’ve passed during rendering to calculate the result.useCallbackcachesthe function itself. UnlikeuseMemo, it does not call the function you provide. Instead, it caches the function you provided so thathandleSubmititself doesn’t change unlessproductIdorreferrerhas changed. This lets you pass thehandleSubmitfunction down without unnecessarily re-renderingShippingForm. Your code won’t run until the user submits the form.
If you’re already familiar withuseMemo, you might find it helpful to think ofuseCallback as this:
// Simplified implementation (inside React)
functionuseCallback(fn,dependencies){
returnuseMemo(()=>fn,dependencies);
}Read more about the difference betweenuseMemo anduseCallback.
Deep Dive
If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.
Caching a function withuseCallback is only valuable in a few cases:
- You pass it as a prop to a component wrapped in
memo. You want to skip re-rendering if the value hasn’t changed. Memoization lets your component re-render only if dependencies changed. - The function you’re passing is later used as a dependency of some Hook. For example, another function wrapped in
useCallbackdepends on it, or you depend on this function fromuseEffect.
There is no benefit to wrapping a function inuseCallback in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.
Note thatuseCallback does not preventcreating the function. You’re always creating a function (and that’s fine!), but React ignores it and gives you back a cached function if nothing changed.
In practice, you can make a lot of memoization unnecessary by following a few principles:
- When a component visually wraps other components, let itaccept JSX as children. Then, if the wrapper component updates its own state, React knows that its children don’t need to re-render.
- Prefer local state and don’tlift state up any further than necessary. Don’t keep transient state like forms and whether an item is hovered at the top of your tree or in a global state library.
- Keep yourrendering logic pure. If re-rendering a component causes a problem or produces some noticeable visual artifact, it’s a bug in your component! Fix the bug instead of adding memoization.
- Avoidunnecessary Effects that update state. Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.
- Try toremove unnecessary dependencies from your Effects. For example, instead of memoization, it’s often simpler to move some object or a function inside an Effect or outside the component.
If a specific interaction still feels laggy,use the React Developer Tools profiler to see which components benefit the most from memoization, and add memoization where needed. These principles make your components easier to debug and understand, so it’s good to follow them in any case. In long term, we’re researchingdoing memoization automatically to solve this once and for all.
Example 1 of 2:Skipping re-rendering withuseCallback andmemo
In this example, theShippingForm component isartificially slowed down so that you can see what happens when a React component you’re rendering is genuinely slow. Try incrementing the counter and toggling the theme.
Incrementing the counter feels slow because it forces the slowed downShippingForm to re-render. That’s expected because the counter has changed, and so you need to reflect the user’s new choice on the screen.
Next, try toggling the theme.Thanks touseCallback together withmemo, it’s fast despite the artificial slowdown!ShippingForm skipped re-rendering because thehandleSubmit function has not changed. ThehandleSubmit function has not changed because bothproductId andreferrer (youruseCallback dependencies) haven’t changed since last render.
import{useCallback}from'react';importShippingFormfrom'./ShippingForm.js';exportdefaultfunctionProductPage({productId,referrer,theme}){consthandleSubmit =useCallback((orderDetails)=>{post('/product/' +productId +'/buy',{referrer,orderDetails,});},[productId,referrer]);return(<divclassName={theme}><ShippingFormonSubmit={handleSubmit}/></div>);}functionpost(url,data){// Imagine this sends a request...console.log('POST /' +url);console.log(data);}
Updating state from a memoized callback
Sometimes, you might need to update state based on previous state from a memoized callback.
ThishandleAddTodo function specifiestodos as a dependency because it computes the next todos from it:
functionTodoList(){
const[todos,setTodos] =useState([]);
consthandleAddTodo =useCallback((text)=>{
constnewTodo ={id:nextId++,text};
setTodos([...todos,newTodo]);
},[todos]);
// ...You’ll usually want memoized functions to have as few dependencies as possible. When you read some state only to calculate the next state, you can remove that dependency by passing anupdater function instead:
functionTodoList(){
const[todos,setTodos] =useState([]);
consthandleAddTodo =useCallback((text)=>{
constnewTodo ={id:nextId++,text};
setTodos(todos=>[...todos,newTodo]);
},[]);// ✅ No need for the todos dependency
// ...Here, instead of makingtodos a dependency and reading it inside, you pass an instruction abouthow to update the state (todos => [...todos, newTodo]) to React.Read more about updater functions.
Preventing an Effect from firing too often
Sometimes, you might want to call a function from inside anEffect:
functionChatRoom({roomId}){
const[message,setMessage] =useState('');
functioncreateOptions(){
return{
serverUrl:'https://localhost:1234',
roomId:roomId
};
}
useEffect(()=>{
constoptions =createOptions();
constconnection =createConnection(options);
connection.connect();
// ...This creates a problem.Every reactive value must be declared as a dependency of your Effect. However, if you declarecreateOptions as a dependency, it will cause your Effect to constantly reconnect to the chat room:
useEffect(()=>{
constoptions =createOptions();
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[createOptions]);// 🔴 Problem: This dependency changes on every render
// ...To solve this, you can wrap the function you need to call from an Effect intouseCallback:
functionChatRoom({roomId}){
const[message,setMessage] =useState('');
constcreateOptions =useCallback(()=>{
return{
serverUrl:'https://localhost:1234',
roomId:roomId
};
},[roomId]);// ✅ Only changes when roomId changes
useEffect(()=>{
constoptions =createOptions();
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[createOptions]);// ✅ Only changes when createOptions changes
// ...This ensures that thecreateOptions function is the same between re-renders if theroomId is the same.However, it’s even better to remove the need for a function dependency. Move your functioninside the Effect:
functionChatRoom({roomId}){
const[message,setMessage] =useState('');
useEffect(()=>{
functioncreateOptions(){// ✅ No need for useCallback or function dependencies!
return{
serverUrl:'https://localhost:1234',
roomId:roomId
};
}
constoptions =createOptions();
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[roomId]);// ✅ Only changes when roomId changes
// ...Now your code is simpler and doesn’t needuseCallback.Learn more about removing Effect dependencies.
Optimizing a custom Hook
If you’re writing acustom Hook, it’s recommended to wrap any functions that it returns intouseCallback:
functionuseRouter(){
const{dispatch} =useContext(RouterStateContext);
constnavigate =useCallback((url)=>{
dispatch({type:'navigate',url});
},[dispatch]);
constgoBack =useCallback(()=>{
dispatch({type:'back'});
},[dispatch]);
return{
navigate,
goBack,
};
}This ensures that the consumers of your Hook can optimize their own code when needed.
Troubleshooting
Every time my component renders,useCallback returns a different function
Make sure you’ve specified the dependency array as a second argument!
If you forget the dependency array,useCallback will return a new function every time:
functionProductPage({productId,referrer}){
consthandleSubmit =useCallback((orderDetails)=>{
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
});// 🔴 Returns a new function every time: no dependency array
// ...This is the corrected version passing the dependency array as a second argument:
functionProductPage({productId,referrer}){
consthandleSubmit =useCallback((orderDetails)=>{
post('/product/' +productId +'/buy',{
referrer,
orderDetails,
});
},[productId,referrer]);// ✅ Does not return a new function unnecessarily
// ...If this doesn’t help, then the problem is that at least one of your dependencies is different from the previous render. You can debug this problem by manually logging your dependencies to the console:
consthandleSubmit =useCallback((orderDetails)=>{
// ..
},[productId,referrer]);
console.log([productId,referrer]);You can then right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. Assuming the first one got saved astemp1 and the second one got saved astemp2, you can then use the browser console to check whether each dependency in both arrays is the same:
Object.is(temp1[0],temp2[0]);// Is the first dependency the same between the arrays?
Object.is(temp1[1],temp2[1]);// Is the second dependency the same between the arrays?
Object.is(temp1[2],temp2[2]);// ... and so on for every dependency ...When you find which dependency is breaking memoization, either find a way to remove it, ormemoize it as well.
I need to calluseCallback for each list item in a loop, but it’s not allowed
Suppose theChart component is wrapped inmemo. You want to skip re-rendering everyChart in the list when theReportList component re-renders. However, you can’t calluseCallback in a loop:
functionReportList({items}){
return(
<article>
{items.map(item=>{
// 🔴 You can't call useCallback in a loop like this:
consthandleClick =useCallback(()=>{
sendReport(item)
},[item]);
return(
<figurekey={item.id}>
<ChartonClick={handleClick}/>
</figure>
);
})}
</article>
);
}Instead, extract a component for an individual item, and putuseCallback there:
functionReportList({items}){
return(
<article>
{items.map(item=>
<Reportkey={item.id}item={item}/>
)}
</article>
);
}
functionReport({item}){
// ✅ Call useCallback at the top level:
consthandleClick =useCallback(()=>{
sendReport(item)
},[item]);
return(
<figure>
<ChartonClick={handleClick}/>
</figure>
);
}Alternatively, you could removeuseCallback in the last snippet and instead wrapReport itself inmemo. If theitem prop does not change,Report will skip re-rendering, soChart will skip re-rendering too:
functionReportList({items}){
// ...
}
constReport =memo(functionReport({item}){
functionhandleClick(){
sendReport(item);
}
return(
<figure>
<ChartonClick={handleClick}/>
</figure>
);
});