Follow Mr. Petty thief for more front-end articles

Hook is a new feature in React 16.8. It lets you use state and other React features without having to write a class.

UseCallback and useMemo are two of these hooks. This article aims to provide an in-depth understanding of the usage and usage scenarios of useCallback and useMemo by addressing a requirement in combination with higher-order functions. These hooks are included together because they are primarily used for performance tuning, and useCallback is implemented using useMemo.

specifications

To understand why you need to use these hooks, write down useCallback and useMemo.

The requirement is: when the mouse moves over a DOM tag, record the number of normal mouse movements and the number of anti-shake movements. [pictured] :

Technical reserves

  • This paper mainly introducesuseCallbackanduseMemoSo come acrossuseStateI don’t want to make a special statement. If yesuseStateNot yet understood, please refer toThe official documentation.
  • This requirement requires an anti-shake function. To facilitate debugging, prepare a simple anti-shake function (a higher-order function) first:
function debounce(func, delay = 1000) {
  let timer;

  function debounced(. args) {
    debounced.cancel();
    timer = setTimeout((a)= > {
      func.apply(this, args);
    }, delay);
  }

  debounced.cancel = function () {
    if(timer ! = =undefined) {
      clearTimeout(timer);
      timer = undefined; }}return debounced
}
Copy the code

Unqualified solutions

Depending on the requirements, the written component might look something like this:

function Example() {
  const [count, setCount] = useState(0);
  const [bounceCount, setBounceCount] = useState(0);
  const debounceSetCount = debounce(setBounceCount);

  const handleMouseMove = (a)= > {
    setCount(count + 1);
    debounceSetCount(bounceCount + 1);
  };

  return (
    <div onMouseMove={handleMouseMove}>
      <p>Normal moves: {count}</p>
      <p>Number of moves after shaking treatment: {bounceCount}</p>
    </div>)}Copy the code

The effect seems to be right, print the log in debmentioning for a look:

function debounce(func, delay = 1000) {
    / /... Omit other code
    timer = setTimeout((a)= > {
      // Add a line of print code here
      console.log('run-do');
      func.apply(this, args);
    }, delay);
    / /... Omit other code
}
Copy the code

When the mouse moves over the div tag, the result is printed [as shown] :

We found that when the mouse stopped moving, run-do was printed for the same number of times as the mouse movement, indicating that the anti-shake function did not take effect. What went wrong?

First we will make it clear that the purpose of using debounce is to return a debounce function through debounce (note: Next, when executing the debounce, by using the timer in the closure of the writing unit, the setTimeout will be eliminated and the task will be executed after a period of inactivity.

Taking a look at our Example component, each updated render of the Example component generates a new debounceSetCount via debounce(setBounceCount). DebounceSetCount will point to different debounce, and different debounce will use different timer, then the closure of the debounce function will have no significance, so the situation in the screenshot will appear.

But why does bounceCount’s value look like it’s been shaken off? Because debounceSetCount(bounceCount + 1) is a setTimeout in debounce when executed multiple times. SetBounceCount is executed in setTimeout, which is asynchronous, and the wait time is about 1000ms. HandleMouseMove is an event callback, but the mouse movement interval is much shorter than 1000ms. The result is that the bounceCount parameter always has the same value, so the effect looks like it has been buffered.

useCallback

Let’s modify our component using useCallback:

function Example() {
  / /... Omit other code
  // We just added the useCallback hook to the Example component
  const debounceSetCount = React.useCallback(debounce(setBounceCount), []);
  / /... Omit other code
}
Copy the code

When we move the cursor over the div tag, it looks exactly like what we want:

With useCallback, we sort of solved the problem (there were problems, but we’ll get to that later).

So how does useCallback solve the problem? Take a look at the useCallback call signature:

function useCallback<T extends (. args:any[]) = >any> (callback: T, deps: ReadonlyArray<any>) :T;

/ / sample:
const memoizedCallback = useCallback(
  (a)= > {
    doSomething(a, b);
  },
  [a, b],
);
Copy the code

As you can see from the signature of useCallback, the first argument to useCallback is a function that returns a memoizedCallback function, such as memoizedCallback in the code above. The second parameter to useCallback is the dependency (DEPS), and memoizedCallback is updated when the dependency changes, that is, when the dependency is unchanged (or when the empty array has no dependencies), memoizedCallback always points to the same function, that is, to the same memory region. When memoizedCallbac is passed to a child component as props, the child component can avoid unnecessary updates by means such as shouldComponentUpdate.

When the Example component is first rendered, the value of debounceSetCount is the result of an execution of debounce(setBounceCount), because when the debounceSetCount is generated through useCallback, the dependency passed in is an empty array, So the next time the Example component renders, debounceSetCount ignores the result of the debounce(setBounceCount) execution and always returns the result cached by useCallback when Example was first rendered, That is to say, the result of the debounce(setBounceCount) will be cached by useCallback, which will solve the problem that debounceSetCount will always point to a different Debounce for Example every time it will render.

As mentioned above, the other problem is that every time the Example component is updated, the debounce function is executed once. As we can see from the above analysis, this is a useless execution and can affect performance if there is a large amount of calculation in the Debounce function.

useMemo

Take a look at how useMemo solves this problem:

function Example() {
  const [count, setCount] = useState(0);
  const [bounceCount, setBounceCount] = useState(0);
  const debounceSetCount = React.useMemo((a)= > debounce(setBounceCount), []);

  const handleMouseMove = (a)= > {
    setCount(count + 1);
    debounceSetCount(bounceCount + 1);
  };

  return (
    <div onMouseMove={handleMouseMove} >
      <p>Normal moves: {count}</p>
      <p>Number of moves after shaking treatment: {bounceCount}</p>
    </div>)}Copy the code

Now, debounceSetCount points to the same memory block every time Example updates its rendering, and debounce only executes once. Our requirements are done and our problems are solved.

Mr. Thief. – The original address of the article

How does useMemo do it? Take a look at the useMemo call signature:

function useMemo<T>(factory: () => T, deps: ReadonlyArray<any> | undefined): T; // Example: Const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);Copy the code

As you can see from useMemo’s signature, the first argument to useMemo is a factory function. The result of this function is cached by useMemo, and the factory function is re-executed only when the useMemo dependency (DEPS) changes. MemoizedValue will be recalculated. That is, when the dependency is unchanged (or when an empty array has no dependency) memoizedValue always returns the value cached through the useMemo.

UseCallback (fn, deps) is the same as useMemo(() => fn, deps).

Pay special attention to

React: You can use useMemo as a performance optimization tool, but don’t use it as a semantic guarantee. In the future, React might choose to “forget” some of the previous Memoized values and recalculate them at the next rendering, such as freeing memory for off-screen components. Write code that can run without useMemo first — then add useMemo to your code to optimize performance. Look at the original

Obviously, removing useMemo from our code would be a problem, so one might think to modify the Debounce function instead, for example:

function debounce(func, ... args) {
  if(func.timeId ! = =undefined) {
    clearTimeout(func.timeId);
    func.timeId = undefined;
  }

  func.timeId = setTimeout((a)= >{ func(... args); },200);
}

/ / use useCallback
function Example() {
  / /... Omit other code
  const debounceSetCount = React.useCallback((. args) = > {
    debounce(setBounceCount, ...args);
  }, []);
  / /... Omit other code
}

// Do not use useCallback
function Example() {
  / /... Omit other code
  const debounceSetCount = changeCount= > debounce(setBounceCount, changeCount);
  / /... Omit other code
}
Copy the code

It seemed that we could do without useMemo, but obviously this was a pretty decent solution that would not work with higher-order functions like the pre-modified Debounce. So, if you don’t use useMemo, what are your best solutions? Please leave a comment.

Follow Mr. Petty thief for more front-end articles

Reference documentation

  • The official documentation