preface

As React users, when trying to optimize existing code, we may try to use useMemo and useCallback to cache data or functions. In the next component update, if the corresponding dependencies do not change, the cache value can be obtained without recalculation.

Next, I will analyze useMemo and useCallback in terms of usage and principle

Use useMemo and useCallback

import React, { useState, useMemo, useCallback } from 'react'

function Demo () {
  const [count, setCount] = useState(0)
  const [isChanged, setIsChanged] = useState(false)
  
  // // useCallback takes two parameters, a function that needs to be executed when a dependency changes and a dependency
  const calDoubleCount = useCallback(() = > {
    setCount(count= > count + 1)}, [])// useMemo takes two arguments, one is the function that needs to be executed when the dependency changes, and the other is the dependency
  const doubleCount = useMemo(() = > {
    return count * 2
  }, [count])
  
  return (
    <>
      <p onClick={()= >setIsChanged(isChanged => ! isChanged)}>change me</p>
      <p onClick={()= > setCount(count => count + 1)}>{doubleCount}</p>
      <button onClick={calDoubleCount}>click me</button>
    </>)}Copy the code

The above code describes how useMemo is used in real development. We change the count value by clicking on the second P tag, and the doubleCount dependency has count, so when count changes, DoubleCount recalculates and returns the latest value.

When the first P tag is clicked, because there is no change in count, doubleCount uses the last calculated value instead of recalculating

UseMemo source code analysis

Before talking about useMemo source code, there is a premise that we need to understand clearly, is that each hook execution we can be divided into two parts

  1. mount
  2. update

The first time a component is mounted, it goes mount, and every subsequent component rendering goes Update. Let’s look at a hook related source code.

// mount
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useMemo: mountMemo,
  // omit other hooks
};

// update
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useMemo: updateMemo,
  // omit other hooks
};
Copy the code

HooksDispatcherOnMount; HooksDispatcherOnUpdate; HooksDispatcherOnUpdate; With this premise in mind, let’s look at the following code.

// mount
HooksDispatcherOnMountInDEV = {
  useMemo<T>(create: () = > T, deps: Array<mixed> | void | null): T {
    try {
      return mountMemo(create, deps);
    } finally {
      / / to omit}}}// update
HooksDispatcherOnUpdateInDEV = {
  useMemo<T>(create: () = > T, deps: Array<mixed> | void | null): T {
    try {
      return updateMemo(create, deps);
    } finally {
      / / to omit}}}Copy the code

That is, the useMemo called when the component is first mounted is actually called mountMemo. Every time I do that, I call updateMemo.

mountMemo

function mountMemo<T> (
  nextCreate: () => T,                                // The function that needs to be executed when a dependency changes
  deps: Array<mixed> | void | null./ / rely on
) :T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
Copy the code

NextCreate is actually the first argument we pass to useMemo, which is a callback function that gets executed when a dependency is updated, and dep is the second argument we pass, which is an array of dependencies.

As you can see, for the first time a component is mounted, useMemo executes nextCreat directly, returning the calculated value. While Hook. memoizedState stores the calculated values and corresponding dependencies, the next time useMemo is executed, the purpose is to determine whether the dependency has changed to return the recalculated value or the result of the last calculation.

updateMemo

function updateMemo<T> (
  nextCreate: () => T,
  deps: Array<mixed> | void | null.) :T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // memoizedState is [value, deps]
  const prevState = hook.memoizedState;
  if(prevState ! = =null) {
    // If the dependency is not null
    if(nextDeps ! = =null) {
      // Dependencies since last update
      const prevDeps: Array<mixed> | null = prevState[1];
      // Compare the current and last dependencies
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // If the two dependencies are equal, return the result of the previous calculation directly
        return prevState[0]; }}}// Otherwise recalculate the dependency value
  const nextValue = nextCreate();
  // Assign the recalculated values and dependencies to hook.memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
Copy the code

useCallback

Now that we know how useMemo works, useCallback is pretty simple.

UseCallback is almost identical to useMemo, except that useMemo returns the value calculated by the function, whereas useCallback returns the function itself

mountCallback

function mountCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  // Return the function itself directly
  return callback;
}
Copy the code

updateCallback

function updateCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState; // [callback, deps]
  if(prevState ! = =null) {
    // Whether there are dependencies
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // Compare the current dependency to the dependency after the last calculation
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        If so, this returns the last cached value
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  // Return the function itself directly
  return callback;
}
Copy the code

The only difference between updateCallback and updateMemo is that updateMemo internally executes a callback and returns the value. UseCallback returns the function directly without evaluating it.

conclusion

From the analysis of useMemo and useCallback above, we can see that the difference between useMemo and useCallback is simple. I’ve written all the rewrites up here, but I want to make one last point here. Although both useMemo and useCallback can cache data, it should not be abused. We should consider which data is worth caching, because for useMemo and useCallback, in addition to calculating values, it takes time to compare dependency changes. We should measure this so that we can use hooks better, rather than optimizing for optimization’s sake.