React hooks: When should you use useMemo/useCallback

background

UseMemo returns a memoized value.

You pass in the create function and the dependency array as arguments to useMemo, which recalculates memoized values only when a dependency changes. This optimization helps avoid costly calculations every time you render.

Remember that functions passed into useMemo are executed during rendering. Please do not perform non-rendering operations inside this function. Operations such as side effects are used by useEffect, not useMemo.

If the dependency array is not provided, useMemo evaluates the new value each time it renders.

⬆️ the words above are from the React documentation

UseMemo, useCallbck is an important tool used by react hooks to cache data. Using these hooks can optimize component performance to some extent.

Many students would prefer to apply useMemo/useCallback to every variable, but this is not the right approach, because there is a cost to this optimization approach.

Each useMemo/useCallback is a natural closure, where garbage data cannot be released in a timely manner, and improper use can result in data accumulation and negative optimization.

When should you use pinching

The following only represents personal opinions, if there are omissions/errors, please correct!

When it is a reference type and is intended to be a dependency on other hooks

Kangkang this demo

export const Component: React.FC = () = > {
  const [someDatas, setSomeDatas] = useState([1.2.3]);
  const [otherData, setOtherData] = useState<{ bool: boolean({} >bool: true });
  const datas100 = someDatas.map((item) = > {
    return item + 100;
  });
  const { bool } = otherData;
  // Effect 1
  useEffect(() = > {
    console.log('Effect1 : ', datas100);
  }, [datas100]);

  // Effect 2
  useEffect(() = > {
    console.log('Effect2 : ', bool);
  }, [bool]);

  return (
    <div>
      <button
        onClick={()= > {
          setSomeDatas((draft) => {
            return [...draft, 1];
          });
        }}
      >
        update someDatas
      </button>
      <button
        onClick={()= >{ setOtherData((draft) => { return { bool: ! draft.bool }; }); }} > update otherDatas</button>
    </div>
  );
};

Copy the code

He might look like this:

Update someDatas (someDatas); update someDatas (someDatas);

If you click Update otherDatas, the expectation is to update otherDatas, causing the bool data to change, causing Effect2 to be reexecuted, but…

After clicking Update otherDatas, Effect1 and Effect2 are executed simultaneously!

why

The hook component re-executes the entire function body on each render, so the datas100 and bool variables are recalculated.

React hooks (useEffect, useMemo, useCallback, etc.) will have a DEps array (second argument), Whether these hooks will be calculated again depends on a shallow diff between the data in the current DEPS array and the data in the last DEPS array. If the data is not equal, the hook will be run again. Otherwise, the hook will not be executed.

As we all know, when JS stores variables, simply speaking, the address of reference type will be stored in the data stack, while the value will be stored in the data heap. The principle of shallow diff is to compare whether the data stored in the stack is the same, regardless of the situation in the heap

Since the DATAS100 will be recalculated in each render, each data100 stored in the stack will be newly allocated, even if the value in the heap is the same, but in the view of THE DEPS array, the current state and the previous state of the DATAS100 is different. So each time Render reexecutes Effect1.

Since bool is a simple type, the value is stored directly in the stack. Although it is regenerated each time, as long as the values are the same, dePS array will consider them equal. So Effect2 is only executed if the value of bool itself has changed

So how to change this awkward situation? We can make an improvement to the above component:

To improve the

export const Component: React.FC = () = > {
  const [someDatas, setSomeDatas] = useState([1.2.3]);
  const [otherData, setOtherData] = useState<{ bool: boolean({} >bool: true });
  const datas100 = useMemo(
    () = >
      someDatas.map((item) = > {
        return item + 100;
      }),
    [someDatas],
  );
  const { bool } = otherData;
  // Effect 1
  useEffect(() = > {
    console.log('Effect1 : ', datas100);
  }, [datas100]);

  // Effect 2
  useEffect(() = > {
    console.log('Effect2 : ', bool);
  }, [bool]);

  return (
    <div>
      <button
        onClick={()= > {
          setSomeDatas((draft) => {
            return [...draft, 1];
          });
        }}
      >
        update someDatas
      </button>
      <button
        onClick={()= >{ setOtherData((draft) => { return { bool: ! draft.bool }; }); }} > update otherDatas</button>
    </div>
  );
};

Copy the code

Effect1 will not be executed every time someDatas is not updated. Effect1 will not be executed every time someDatas is not updated.

Success, only Effect2 is executed, let’s try update someDatas again:

I love it. It works fine. Effect1 is executed properly

Therefore, we can draw conclusion 1: useMemo needs to be wrapped when it is a reference type and is intended to be a dependency on other hooks

UseMemo wrapping is recommended when data is of reference type and is to be passed to child components as props

What if we put the side effects in the component above (whisper: fake side effects! I mean useEffect! Pull it out, get rid of the useMemo, and we get this structure down here

interface Props {
  datas100: number[];
  bool: boolean;
}

export const ChildComponent: React.FC<Props> = ({ datas100, bool }) = > {
  // Effect 1
  useEffect(() = > {
    console.log('Effect1 : ', datas100);
  }, [datas100]);

  // Effect 2
  useEffect(() = > {
    console.log('Effect2 : ', bool);
  }, [bool]);
  return <div>I'm a child component</div>;
};

export const Component: React.FC = () = > {
  const [someDatas, setSomeDatas] = useState([1.2.3]);
  const [otherData, setOtherData] = useState<{ bool: boolean({} >bool: true });
  const datas100 = someDatas.map((item) = > {
    return item + 100;
  });
  const { bool } = otherData;

  return (
    <div>
      <button
        onClick={()= > {
          setSomeDatas((draft) => {
            return [...draft, 1];
          });
        }}
      >
        update someDatas
      </button>
      <button
        onClick={()= >{ setOtherData((draft) => { return { bool: ! draft.bool }; }); }} > update otherDatas</button>
      <ChildComponent datas100={datas100} bool={bool} />
    </div>
  );
};

Copy the code

At this point we click on Update otherDatas and crack again:

Effect1 was executed incorrectly again, for a simple reason that you’ve already seen and won’t go into too much detail here.

This example is typical of passing a reference type to a child component but not caching it.

If a hook of a child component is dependent on this variable, then the hook is invalid, like a virus. If the hook is useMemo, and it is passed to a further component, then the consequences can be severe.

So I usually give will be passed to the child component reference types are combined with useMemo, this practice may make a lot of people dislike, but after all the business code is not only one maintenance, others may update this code, if he is in the child components using a didn’t do cache the reference type of props, that is not in situ crack?

Therefore, we can draw the conclusion that:

UseMemo wrapping is recommended when data is of reference type and is to be passed to child components as props

When the time complexity of processing data is high, it should be wrapped with useMemo

Let’s come to kangkang’s demo:

interface Props {
  datas: number[];
  anyProps: any;
}

const Component: React.FC<Props> = ({ datas }) => {
  const str = datas.sort((a, b) => a > b ? 1 : -1).join('->');
  return <div>{str}</div>;
};
Copy the code

In this demo, if the length of the datas is very large, then the datas.sort calculation will be horrible. Every Component update (not an update caused by the datas itself) will cause the STR to be recalculated, which is obviously not expected. And it can be very slow.

So we can also wrap STR with useMemo:

const Component: React.FC<Props> = ({ datas }) => {
  const str = useMemo(
    () => datas.sort((a, b) => (a > b ? 1 : -1)).join('->'),
    [datas],
  );
  return <div>{str}</div>;
};
Copy the code

With this change, STR will not be recalculated when the Component update is not caused by datAS, thus improving performance

When you don’t know if you should add useMemo and it happens to be a reference type

Warning: It’s best not to do this unless you’re unfamiliar with React, manual funny 🤪

As you can imagine, if all reference types were useMemo, there would be no dePS arr diff mess. This way the logic is safe, but the performance is not…

One last sneak bibi

The above content only represents my personal understanding, there are bound to be some wrong or incomplete places, so if you have anything to add, feel free to leave a comment in the comment section!!