React Hooks: useMemo and useCallback
When to use them?
Refresher
In simple terms, React maintains the UI in sync with the application's state by performing re-renders. While React is optimized to handle re-renders efficiently, there are scenarios where these re-renders can become performance bottlenecks, causing delays in UI updates after user interactions.
To address such performance issues, React provides the useMemo and useCallback hooks.
useMemo allows you to cache the result of a computation between re-renders. useCallback, on the other hand, enables you to cache a function definition between re-renders.
A Soda Dispenser
const SodaDispenser = () => {
const initialSodas = ['Coke', 'Sprite', 'Fanta', 'Pepsi'];
const [sodas, setSodas] = useState(initialSodas);
const dispense = soda => {
setSodas(allSodas => allSodas.filter(c => c !== soda));
};
return (
Soda Dispenser
{sodas.length === 0 ? (
<Button onClick={() => setSodas(initialSodas)}>Refill</Button>
) : (
{sodas.map(soda => (
<ButtonContainer key={soda}>
{soda}
<Button onClick={() => dispense(soda)}>Grab</Button>
</ButtonContainer>
))}
)}
);
};
Now, consider this: I'm going to make a minor change to this component and I want you to think about which version will perform better.
The change is to wrap the dispense function inside React.useCallback:
Original:
const dispense = soda => {
setSodas(allSodas => allSodas.filter(s => s !== soda));
};
With useCallback:
const dispense = React.useCallback(soda => {
setSodas(allSodas => allSodas.filter(s => s !== soda));
}, []);
In this specific case, which version has better performance?
If you answered the original, you're correct!
Why is useCallback worse?
We've often heard that using React.useCallback can improve performance and that inline functions can be problematic for performance. So, how could it be better not to use useCallback?
Let's refactor the useCallback example a bit to illustrate more clearly:
Original:
const dispense = soda => {
setSodas(allSodas => allSodas.filter(c => c !== soda));
};
With useCallback (Updated):
const dispense = soda => {
setSodas(allSodas => allSodas.filter(c => c !== soda));
};
const dispense = useCallback(dispense, []);
As you can see, they are essentially the same except that the useCallback version is doing additional work. We need to define the function, create an array, and call React.useCallback, which involves setting properties and evaluating logical expressions. In both cases, JavaScript must allocate memory for the function definition on every render.
So when should I useMemo and useCallback?
Referential Equality in JavaScript:
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
(() => {}) === (() => {}) // false
When you define an object inside your React function component, it won't be referentially equal to the last time that same object was defined -- even if it has the same properties and values.
Now, let's review this example:
const Foo = ({bar, baz}) => {
useEffect(() => {
const options = {bar, baz};
buzz(options);
}, [bar, baz]); // we want this to re-run if bar or baz change
return foobar;
};
const Blub = () => {
const bar = () => {};
const baz = [1, 2, 3];
return <Foo bar={bar} baz={baz} />;
};
The issue here is that useEffect will perform a referential equality check on bar and baz between every render. Due to JavaScript's behavior, both will be new objects/functions every time, so React will always evaluate the dependencies as changed, causing the useEffect callback to run after every render.
This is precisely why useCallback and useMemo exist. Here's how you'd fix it:
const Foo = ({bar, baz}) => {
useEffect(() => {
const options = {bar, baz};
buzz(options);
}, [bar, baz]);
return foobar;
};
const Blub = () => {
const bar = useCallback(() => {}, []);
const baz = useMemo(() => [1, 2, 3], []);
return <Foo bar={bar} baz={baz} />;
};
Now, bar and baz will only be defined once at the initial render. Therefore, when React checks the dependencies between renders, it will evaluate them as unchanged.
Optimise Responsibly
Performance optimisations come at a cost and do not always provide a significant benefit. It's essential to identify performance bottlenecks through profiling and apply optimisations where they will have a tangible impact. Overusing useMemo and useCallback can lead to more complex code without substantial performance gains. Always weigh the trade-offs and optimise only when necessary to ensure your application remains maintainable and performant.
Ps. if you have any questions
Ask here