A brief discussion on React hook functions useMemo and useCallback

Foreword

There are many officially provided hooks in React, such as useEffect, useState, useMemo, and useCallback. Some beginners can’t tell what scenarios useMemo and useCallback are suitable for. Today we will talk about these two hook functions.

useMemo

It is used to optimize rendering performance. useMemo will receive a callback function wrapped by an arrow function and a dependency array, and then return the calculation result of the callback function. useMemo recalculates the callback function when a value in the dependency array changes. If the dependency has not changed, useMemo will return the result of the last calculation, thus avoiding unnecessary calculations. As follows, the value of value will be recalculated only when a or b changes.


 const value = useMemo(() => {
    return caculateFunction (a, b)
 }, [a, b])

Usage scenario: useMemo should be used when there is an expensive calculation operation and the input value of the operation will not change between multiple renderings.

Implementation principle: In the react hooks system, the hook function has its own execution logic for each stage, and is stored in Dispatcher. Take a look at the scheduler when mounting as follows:


const HooksDispatcherOnMount: Dispatcher = {
  ...
  useMemo: mountMemo,
  ...
};

The scheduler when updating is as follows:


const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useMemo: updateMemo,
  ...
}

Obviously, the key point lies in the updateMemo method. Let’s take a look at his implementation principle


function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

A key function here is areHookInputsEqual, which is used to compare whether the two dependencies have changed.


function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (__DEV__) {
    if (ignorePreviousDependencies) {
      // Only true when this component is being hot reloaded.
      return false;
    }
  }

  if (prevDeps === null) {
    if (__DEV__) {
      console.error(
        '%s received a final argument during this render, but not during ' +
          'the previous render. Even though the final argument is optional, ' +
          'its type cannot change between renders.',
        currentHookNameInDev,
      );
    }
    return false;
  }

  if (__DEV__) {
    // Don't bother comparing lengths in prod because these arrays should be
    // passed inline.
    if (nextDeps.length !== prevDeps.length) {
      console.error(
        'The final argument passed to %s changed size between renders. The ' +
          'order and size of this array must remain constant.\\
\\
' +
          'Previous: %s\\
' +
          'Incoming: %s',
        currentHookNameInDev,
        `[${prevDeps.join(', ')}]`,
        `[${nextDeps.join(', ')}]`,
      );
    }
  }
  for (let i = 0; i < prevDeps.length & amp; & amp; i < nextDeps.length; i + + ) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

As can be seen from the above code, the areHookInputsEqual function accepts two dependency arrays, nextDeps and prevDeps. It will first check if the lengths of the two arrays are equal, and if not, will issue a warning in development mode. It then iterates through the array and compares elements one by one using the is function (similar to Object.is). If any unequal elements are found, the function returns false. Otherwise, return true

This way react knows whether a recalculation operation is needed.

useCallback

useCallback is a React Hook that allows you to cache functions across multiple renders.

It also accepts two parameters, callback and dependencies. When the value in the dependency array changes, useCallback will return a new function instance. Otherwise, it returns the last created function instance. useCallback is used in combination with React.Memo.


 const childFucntion = useCallback(() => {
    action()
 }, [a, b])

The usage scenario is: there is a parent component that contains child components, and the child component receives a function as props; generally speaking, if the parent component is updated, the child component will also perform an update; but in most scenarios, updates are not necessary , we can use useCallback to return the function, and then pass this function to the subcomponent as props; in this way, the subcomponent can avoid unnecessary updates. For example: The subcomponent is as follows


const child = memo(() => {
  return <div>
    I am a child component
  </div>
})

The parent structure is as follows:


const parent = props => {
  const [num, setNum] = useState(0);

  const getValue = value => {
    console.log(value);
  };

  const changeState = () => {
    setNum(pre => {
      return pre + 1;
    });
  };

  return (
    <div>
      I am the parent component
      <Button onClick={changeState}>Click me to change state</Button>
      {num}
      <child getValue={getValue} />
    </div>
  );
};

When you click to change the value of number, although num has nothing to do with the child, the child will still be re-rendered, which obviously causes a waste of performance. The reason for the update is that React.memo detects whether the stack address of the data in props has changed. When you change the state in the parent component, it will cause the parent component to be rebuilt, and when the parent component is rebuilt, all functions in the parent component will be rebuilt (old functions are destroyed and new functions are created , equal to updating the function address), the new function address is passed into the subcomponent and the stack address update is detected by props. This also triggers the re-rendering of the child component.

The solution is to use useCallback to wrap the function to be passed into the child component.


const parent = props => {
  const [num, setNum] = useState(0);

  const getValue = useCallback(value => {
    console.log(value);
  }, []);

  const changeState = () => {
    setNum(val => val + 1);
  };
  
  return (
    <div>
      I am the parent component
      <Button onClick={changeState}>Click me to change state</Button>
      {num}
      <child getValue={getValue} />
    </div>
  );
};

useCallback source code


function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

Among them areHookInputsEqual is the same as useMemo.

Summary

Do not use useCallBack arbitrarily. Adding useCallBack to each method without thinking. Do not use it arbitrarily, which will cause unnecessary waste of performance. useCallBack itself requires a certain amount of performance useCallBack does not prevent function re-creation. You can only decide whether to return a new function or an old function through dependencies, thereby ensuring that the function address remains unchanged while the dependencies remain unchanged useCallBack needs to be used with React.memo `useMemo will execute the callback function and returns the result, but useCallback does not execute the callback function.