12 min read
React useCallback: A Quick Guide
Discover how React’s useCallback hook can help you optimize performance and reduce unnecessary re-renders in your app.
Introduction
I find that a decent amount of performance issues in React apps often come down to unnecessary re-renders.
Let's say you're working on an app with a performance issue. Sometime after debugging you come across a component that is re-rendering multiple times for no reason, wrap it in a React.memo and move on, only to find that it's still re-rendering.
The component is only taking two props:
label(typestring)onButtonPressforonClickevents (typefunction)
The culprit is the prop onButtonPress, whose reference in memory is changing when the component's parent re-renders. Implementing the useCallback hook will help achieve the goal of preventing this component from re-rendering and further optimize your app.
In this tutorial, I’ll explain how useCallback works and when it’s useful in your React projects. By the end, you’ll have a solid understanding of when and why to use this hook to optimize performance and make your app more efficient.
What is useCallback
In plain terms, React's useCallback hook allows functions to retain the same reference in memory (memoize/cache) between renders. When comparing props to evaluate a re-render, or dependencies in a hook's dependency array, React uses the Object static method Object.is to determine if the variables have changed.
In JavaScript, functions are Objects , which means that natively, no two functions can be equal each other. This
makes useCallback a great tool because it allows a function written in a React component to be equal to itself on re-render, given that the values in its dependency array don't change.
Proper usage can prevent unnecessary renders, which can optimize performance within an application.
Improper usage can add to code complexity, possibly worse performance, and odd bugs when functions are executed with outdated parameters.
Key features of useCallback:
- Memoization: React stores the function and only updates it when necessary.
- Dependency array: The function is re-created only if one or more of the values in the dependency array change.
- Better Performance: Helps reduce unnecessary re-renders, especially in large or complex components.
Requirements for useCallback:
- Functional components only:
useCallbackis a hook, so it must be used within functional components. - Top-level hook: Always place hooks at the top level of your component or custom hook, not inside loops or conditionals.
- Memoizing passed functions: Useful when passing functions as props, particularly to
React.memowrapped components. - Accurate dependencies: Ensure all variables the function relies on are included in the dependency array to avoid stale data.
- Avoid overuse: Not all functions need to be memoized. Overuse can add complexity without meaningful performance gains.
Syntax for useCallback
The useCallback hook takes two arguments:
- Callback function: Contains the logic that will execute when called.
- Dependency array: Controls when the function is re-created. If the array is empty, the function reference will remain the same throughout the component's lifecycle.
Simple Usage:
import { useCallback } from "react";
// Dependent on variables a and b
const memoizedCallback = useCallback(() => {
// Logic
}, [a, b]);
// No dependent variables, function will not be recreated
const memoizedCallback = useCallback(() => {
// Logic
}, []);
Inline Function:
import { useCallback } from "react";
const FunctionalComponent = ({ dependency1 = 1, dependency2 = 2 }) => {
const memoizedCalculation = useCallback(() => {
return dependency1 + dependency2;
}, [dependency1, dependency2]);
return <div>useCallback Example: {memoizedCalculation()}</div>
};
Passing a Function:
import { useCallback } from "react";
const FunctionalComponent = ({ dependency1 = 1, dependency2 = 2 }) => {
const handleCalculation = (dependency1, dependency2) => {
return dependency1 + dependency2;
}
const memoizedCalculation = useCallback(
() => handleCalculation(dependency1, dependency2),
[dependency1, dependency2]
);
return (<div>useCallback Example: {memoizedCalculation()}</div>)
};
Custom Hook:
import { useCallback } from "react";
const useCustomHook = ({ dependency1 = 1, dependency2 = 2 }) => {
const memoizedHandleCalculationOnClick = useCallback(() => {
return dependency1 + dependency2;}, [dependency1, dependency2]
);
return memoizedHandleCalculationOnClick;
}
When to Use useCallback
In my opinion, useCallback is a performance optimization tool that should only be used when you discover performance problems in your app, or if you're 100% sure you writing a function that will need to use useCallback.
If you pre-optimize and wrap your functions in useCallback before you've finished working on a feature, you might forgot to update the dependency array if and when the logic changes and you're developing. I've seen this lead to bugs that can take a while to discover because technically, everything is working correctly, but the functions are executing with outdated variables.
Consider using useCallback in these cases:
- Passing Functions as Props: If you’re passing a function as a prop to a child component, especially one wrapped in
React.memo, memoizing the function helps prevent the child from re-rendering unnecessarily. - Event Handlers in Complex Components: In large components with multiple event handlers, memoizing functions can prevent excessive re-creation and ensure smoother performance.
Expensive Operations Argument
In the above list of considerations on when to use useCallback, I did not mention avoiding expensive calculations or function calls as a possible scenario.
I see a lot of guides and tutorials on useCallback that mention if the the function logic "performs costly calculations" or "makes API calls", wrapping it the function is useCallback will have performance benefits.
This argument makes zero sense to me:
- If the function is only called on mount or as the side-effect of an event handler, then why does it matter if the logic that executes is expensive and costly? When React recreates a non-memoized function on re-render, it does not execute the function and run the logic, because the function is not being called on re-render.
- If the function is called on every render, once again, why does it matter if the logic is costly? This function has to run on every render, and by wrapping it in
useCallback, you're making React compare the dependency array in addition to executing the function - when it was going to execute anyway.
Consider the following:
import { useCallback, useEffect, useState, useRef } from "react";
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const CountExample = () => {
const [count, setCount] = useState(0);
const memoizedCalculation = useCallback(() => {
console.log("memoizedCalculation");
return 1 + 2;
}, [1, 2]);
const prevMemoizedCalculation = usePrevious(memoizedCalculation);
const functionsAreEqual = prevMemoizedCalculation === memoizedCalculation;
console.log("functionsAreEqual", functionsAreEqual);
return (
<div>
<p>count: {count}</p>
<p>{memoizedCalculation()}</p>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
Increment Count
</button>
</div>
);
};
In the above example:
usePrevioushook is declared, which stores the previous value of a variable for use on the next rendercountandsetCountuseStateis declared with a simple increment counter function on button click, as a way to force the component to re-rendermemoizedCalculationuseCallbackhook is declared, with its dependencies being both primitive values and constant (ensuring React will not return a new function on re-render)
Notice that the usePrevious is storing the previous value of the memoizedCalculation calculation function, which essentially is () => {}
After the component mounts and the Increment Count button is clicked:
- The state increases by its values by 1
- The
logforfunctionsAreEqualwill writetrueto the console showing that React properly return the same function instance between renders - Most importantly,
memoizedCalculationgets called and the logic runs returning the same value. ThelogformemoizedCalculationwill write to the console on every re-render.
The memoizedCalculation logging on every render shows the difference between useCallback and useMemo. useCallback will not optimize performance if the function is being called on every render.
How to Use useCallback
I typically use useCallback in two scenarios:
- Prop Functions: When passing function props to child components that I don't want to re-render unnecessarily.
- Custom Hooks: When returning functions from hooks that require stable function references.
1. Using useCallback in a Functional Component
You will most likely find yourself using useCallback to prevent unnecessary re-renders of child components when passing functions as props. By memoizing the function, it ensures that the child component doesn’t re-render unless necessary.
import { useState, useCallback, memo} from 'react';
const ChildComponent = memo(({ onClick, label }) => {
console.log('Child re-rendered');
return <button onClick={onClick}>{label}</button>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
// Memoizing the click handler to prevent unnecessary re-renders
const onButtonPress = useCallback(() => {
console.log(`Button clicked, count is ${count}`);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<ChildComponent onClick={onButtonPress} label="Click Me" />
</div>
); In this example, onButtonPress is memoized to ensure that the child component (ChildComponent) doesn't re-render every time the parent component re-renders. The function is only re-created when the count changes.
2. Using useCallback in a Custom Hook
Custom hooks can also benefit from useCallback, especially when a function inside the hook depends on changing data. By memoizing the function, it ensures consistent behavior without unnecessary re-creation.
import { useState, useCallback } from 'react';
const useToggle = (initialValue) => {
const [value, setValue] = useState(initialValue);
// Memoizing the toggle function to prevent unnecessary re-renders
const toggle = useCallback(() => {
setValue((prevValue) => !prevValue);
}, []);
return [value, toggle];
};
This custom hook memoizes the toggle function, ensuring that it remains stable across re-renders unless dependencies change.
Incorrect Usage of useCallback
Using useCallback improperly can lead to unnecessary complexity without meaningful performance gains. Below are some common examples of incorrect usage.
1. Over-Memoizing Simple Functions
Memoizing simple functions adds overhead without any real benefit. This can lead to unnecessary complexity, and maintainability issues in your codebase.
import { useCallback } from 'react';
const FunctionalComponent = () => {
const logMessageOnButtonPress = useCallback(() =>
{ console.log('Look mah no hands!'); }, []);
return <button onClick={logMessageOnButtonPress}>Log Message</button>
}
In this case, logMessageOnButtonPress is a simple function that doesn’t need memoization. Since it doesn't cause a performance bottleneck, wrapping it in useCallback adds unneeded complexity.
2. Forgetting Dependencies
A frequent mistake is forgetting to include all necessary dependencies in the array. This can result in bugs where the memoized function uses outdated values.
Quick Note on this:You do not need to always add all dependencies to the dependency array.
On several occasions I have used
useCallbackand the function logic did not care if some of the dependencies changed (meaning those dependencies could have the same value they did on mount every time the function is executed).
import { useCallback } from 'react';
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log(`Count is: ${count}`); // Depends on count
}, []); // Missing count in the dependencies
return (
<div>
<button onClick={handleClick}>Log Count</button>
</div>
); }; 3. Functions That Never Change
If a function does not depend on any external variables or props, it should not be memoized using useCallback. Memoizing such functions adds unnecessary overhead without offering performance improvements.
import { useCallback } from 'react';
const staticFunction = useCallback(() => {
console.log('This function never changes');
}, []); // No dependencies, no need for memoization
const handleChange = useCallback((e) => {
setValue(e.target.value); }, []); // No real need to memoize simple function
Performance Considerations
While useCallback can optimize your React app by reducing unnecessary function re-creation, it’s important to remember that it introduces its own performance overhead. Every time a component renders, React must compare the dependencies to decide whether to return a new function or reuse the existing one. This comparison itself has a cost.
Here are some key points for you to keep in mind:
- Minimal Gains for Simple Functions: If a function is lightweight or doesn’t trigger re-renders, the performance gains from using
useCallbackare minimal. In such cases, it might be better to skip memoization altogether. - Avoid Overusing: Overusing
useCallbackcan lead to more complex code without meaningful performance improvements. Only memoize functions when necessary, such as when passing them as props toReact.memowrapped child components. - Careful Dependency Management: Ensure the dependency array is accurate. If dependencies are mismanaged,
useCallbackcan introduce bugs or create stale closures, which could degrade performance rather than improve it. - Profiling Before Optimizing: Always measure and profile your app’s performance before deciding to use
useCallback. React’s built-in DevTools profiler can you help identify re-renders and performance bottlenecks.
Closing Thoughts
useCallback is a powerful tool for optimizing performance in React applications, but it’s not a silver bullet. Like most optimizations, it should be used thoughtfully and only when necessary. Overusing it can result in harder-to-read code with minimal performance gains.
When used correctly, useCallback can prevent unnecessary re-renders and boost your app's performance. However, always test whether it truly benefits your specific use case before relying on it.
React’s optimization hooks like useCallback, useMemo, and useRef are best used in moderation, striking the right balance between simplicity and performance.