目录
1. useCallback refresher
2. Why does this matter?
3. When is this useful in React?
4. When not to use useCallback?
5. useCallback doesn’t memoize the function result
I write a lot of React components which require code review. Many times during that process someone has dropped the classic line:
I think we should wrap this function in useCallback for performance reasons.
But what is useCallback and when does it make sense to use it?
1. useCallback refresher
To recap, useCallback is a React Hook which returns a memoized version of the callback function it is passed.
This means that the function object returned from useCallback will be the same between re-renders.
2. Why does this matter?
It’s worth recalling that in JavaScript, functions display referential equality. This means they are only considered equal if they point to the same object.
Therefore if you were two declare two functions with identical implementations they would not be equal to each other.
For example:
代码语言:javascript复制const firstFunction = function() {
return 1 2; // same as secondFunction
}
const secondFunction = function() {
return 1 2; // same as firstFunction
}
// Same implementation, different objects
firstFunction === secondFunction; // false
// Same objects!
firstFunction === firstFunction; // true
3. When is this useful in React?
Typically useCallback is helpful when passing callback props to highly optimised child components.
For example, if a child component that accepts a callback relies on a referential equality check (eg: React.memo() or shouldComponentUpdate) to prevent unnecessary re-renders when its props change, then it is important that any callback props do not change between renders.
To do this, the parent component can wrap the callback prop in useCallback and be guaranteed that it is passing the same function object down into the optimised child component.
代码语言:javascript复制function ParentComponent() {
const onHandleClick = useCallback(() => {
// this will return the same function
// instance between re-renders
});
return (
<MemoizedSubComponent
handleClick={onHandleClick}
/>
);
}
4. When not to use useCallback?
You should avoid seeing useCallback as a blanket performance optimisation.
In most cases, it’s simply better to accept that for functions declared inline with React components, each new re-render creates a new function object. This is typically absolutely fine and will not have a detrimental impact on performance.
You should always profile though – just in case!
Let’s look at another example:
代码语言:javascript复制import React, { useCallback } from 'react';
function MyComponent() {
const handleClick = useCallback(() => {
// handle the click event
}, []);
return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
return <button onClick={onClick}>I am a child</button>;
}
Does it make sense to apply useCallback()? Most likely not.
useCallback() hook is called every time MyComponent renders. Even useCallback() returning the same function object, still, the inline function is re-created on every re-rendering (useCallback() just skips it).
This doesn’t bring any benefits because the optimization costs more than not having the optimization.
Don’t forget about the increased code complexity. You have to keep the deps of useCallback(..., deps) in sync with what you’re using inside the memoized callback.
Simply accept that on each re-rendering new functions are created:
代码语言:javascript复制import React, { useCallback } from 'react';
function MyComponent() {
const handleClick = () => {
// handle the click event
};
return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
return <button onClick={onClick}>I am a child</button>;
}
5. useCallback doesn’t memoize the function result
It’s worth noting that applying useCallback doesn’t memoize the result of a function’s invocation. Rather it memoizes the function object itself.
Consider the following:
代码语言:javascript复制function ParentComponent() {
const onHandleClick = useCallback(() => {
const special = calculatePi();
});
return (
<SubComponent
handleClick={onHandleClick}
/>
);
}
In this example, each time the <SubComponent> triggers the onHandleClick callback via its handleClick prop the (presumably expensive!) calculatePi() will be triggered. The result of the arrow function isn’t memoized, only the reference.
If we wanted to avoid re-calculating Pi on each handleClick we’d be better off memorizing calculatePi directly via useMemo():
代码语言:javascript复制const memoizedPi = useMemo( calculatePii() );
6. Summary
When thinking about performance tweaks, recall the statement:
Profile before optimizing
Any optimization adds complexity. Any optimization added too early is a risk because the optimized code may change many times.
These considerations apply to useCallback() hook too. Its appropriate use case is to memoize the callback functions that are supplied to memoized heavy child components.
Either way:
- profile
- quantify the increased performance (e.g. 150ms vs 50ms render speed increase)
Then ask yourself: does the increased performance, compared to increased complexity, worth using useCallback()?
To enable the memoization of the entire component output I recommend checking my post Use React.memo() wisely.
参考:
When to use React.useCallback()? https://aheadcreative.co.uk/articles/when-to-use-react-usecallback/ Your Guide to React.useCallback(): https://dmitripavlutin.com/dont-overuse-react-usecallback/ When to useMemo and useCallback: https://kentcdodds.com/blog/usememo-and-usecallback