Reputation: 4439
On my journey to try and understand React Hooks better I came across some behaviour I did not expect. I was attempting to create an array of refs and pushing to said array via an onRef function I would pass to my <div>'s
. The array kept getting bigger everytime the component re-rendered presumably just because it was a simple arrow function and not memoized.
So then I added the useCallback
hook to make sure that I wouldn't get the same ref multiple times, but to my surprise it still called the function every re-render. After adding an empty array as second parameter the refs only fired once per component as expected.
This behaviour is demonstrated in the snippet below.
const Example = () => {
const _refs = React.useRef([]);
// Var to force a re-render.
const [ forceCount, forceUpdate ] = React.useState(0);
const onRef = (ref) => {
if (ref && ref !== null) {
console.log("Adding Ref -> Just an arrow function");
_refs.current.push(ref);
}
}
const onRefCallbackWithoutInputs = React.useCallback((ref) => {
if (ref && ref !== null) {
console.log("Adding Ref -> Callback without inputs.");
_refs.current.push(ref);
}
});
const onRefCallbackEmptyArray = React.useCallback((ref) => {
if (ref && ref !== null) {
console.log("Adding Ref -> Callback with empty array");
_refs.current.push(ref);
}
}, []);
React.useEffect(() => {
console.log("Refs size: ", _refs.current.length);
});
return (
<div>
<div ref={onRef}/>
<div ref={onRefCallbackWithoutInputs}/>
<div ref={onRefCallbackEmptyArray}/>
<div onClick={() => forceUpdate(forceCount + 1)}
style = {
{
width: '100px',
height: '100px',
marginTop: '12px',
backgroundColor: 'orange'
}
}>
{'Click me to update'}
</div>
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
I assumed useCallback
would have an empty array as a default for the second parameter. So what exactly does not giving a second parameter do? Why does it behave differently?
Upvotes: 66
Views: 57510
Reputation: 190
useCallback
with an empty dependency array is a memoized function, which does not compute updated states (if you have states inside it, it'll use the initial value passed in useState
.
When you want your useCallback
function to consume a state, for example, you should pass it inside its dependency array, just like useEffect
works! Doing this, your whole function will be remounted everytime that state changes.
Upvotes: 19
Reputation: 887
I think it's the same logic behind all the hooks, useEffect
, useLayoutEffect
, useCallback
, useMemo
, for dependency array,
if no dependencies passed means we passed the null value for dependencies, hence comparison would always result false and inline function will execute every time.
If empty dependencies are passed means there is nothing to compare further hence inline function will only execute once. (it is just like we are instructing React for no further comparison).
If the array are passed with some variable then it will compute the inline function based on the changes in the variable.
Though instance of the inline function will always created.
Upvotes: 11
Reputation: 33439
For both useMemo
and useCallback
(which is essentially just a special case of useMemo
), if the second argument is an empty array, the value will be memoized once and always returned.
If the second argument is omitted, the value will never be memoized, and the useCallback
and the useMemo
doesn't do anything.
Perhaps there's some edge case where you might conditionally memoize:
useMemo(someValue, shouldMemoize ? [] : null)
But in the vast majority of cases, the second argument to both useMemo
and useCallback
should be considered mandatory. And in fact, the Typescript definitions treat them this way.
// Require a second argument, and it must be an array
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
// Second argument can be undefined, but must be explicitly passed as undefined, not omitted.
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
There's an open pull request that's enhancing the exhaustive-deps
hooks eslint rule so that it will raise a lint error if the second argument is omitted, so pretty soon this will likely be a linter error.
Upvotes: 92