ApplePearPerson
ApplePearPerson

Reputation: 4439

What's the difference between `useCallback` with an empty array as inputs and `useCallback` without a second parameter?

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

Answers (3)

antoniopataro
antoniopataro

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

Krishna Agarwal
Krishna Agarwal

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

Retsam
Retsam

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

Related Questions