目錄1. useCallback refresher2. Why does this matter?3. When is this useful in React?4. When not to use useCallback?5. useCallback doesn’t memoize the function resultI 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?
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.
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:
const firstFunction = function() { return 1 + 2; // same as secondFunction}
const secondFunction = function() { return 1 + 2; // same as firstFunction}
// Same implementation, different objectsfirstFunction === secondFunction; // false
// Same objects!firstFunction === firstFunction; // true3. 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.
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:
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:
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 resultIt’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:
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():
const memoizedPi = useMemo( calculatePii() );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:
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