useCallback

useCallback takes a callback function and an array of dependencies as arguments, and returns the cached callback function if each item in the dependency array remains the same.

Since useCallback can cache functions, wouldn’t all functions in the Function component be wrapped in useCallback for the purpose of React application optimization? Don’t worry, let’s take a look at two examples and decide which one is better, App1 or App2?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const App1 = () => {
  const [value, setValue] = useState('');
  const onValueChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) =>
      setValue(event.target.value),
    [],
  );

  return <input type="text" value={value} onChange={onValueChange} />;
};
1
2
3
4
5
6
7
const App2 = () => {
  const [value, setValue] = useState('');
  const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) =>
    setValue(event.target.value);

  return <input type="text" value={value} onChange={onValueChange} />;
};

The difference between App1 and App2 is that onValueChange uses useCallback in the former and not in the latter. We know from the definition that useCallback can cache functions, and that App1 will not generate a new onValueChange when value changes, so the first impression is that App1 is definitely better.

Now, rewrite App1 and determine which is better, App1 or App2?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const App1 = () => {
  const [value, setValue] = useState('');
  const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) =>
    setValue(event.target.value);
  const dependenies = [];
  const onValueChangeInUseCallback = useCallback(onValueChange, dependenies);

  return (
    <input type="text" value={value} onChange={onValueChangeInUseCallback} />
  );
};

The above App1 is actually the same as the original App1, but written in a different way, compared to App2, which has one more array definition and one more useCallback call than App2, so contrary to the initial intuition, App2 is better than App1 which uses useCallback. performs better thanApp1withuseCallback`.

So you shouldn’t use useCallback, because useCallback will lead to worse performance ?

Of course not, useCallback can improve React’s performance, but only if it’s a prerequisite. Modify the above example to add a clear function to the input box:

In the above example, ClearButton accepts props from onClear and clears the input when the button is clicked. The problem with this example, however, is that when the value changes, the ClearButton is also re-rendered as seen in the Console. From a performance point of view, a change in value should not cause ClearButton to re-render, because ClearButton only relies on onClear, but a change in value causes onClear to re-generate, resulting in prevProps.onClear ! == nextProps.onClear, which causes ClearButton to be re-rendered. This is where useCallback comes in handy.

In the example above, onClear is wrapped in useCallback, so that a change in value does not cause onClear to be re-generated. But from the Console, even with useCallback, a change in value still causes ClearButton to be re-rendered.

This brings us back to the rendering of React, where its own rendering must cause a re-rendering of the child component for the React component, which can be verified by removing onClear from the ClearButton.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const ClearButton = () => {
  console.count('clear button render');

  return <button type="button">clear</button>;
};

const App = () => {
  console.count('app render');

  const [value, setValue] = useState('');
  const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) =>
    setValue(event.target.value);

  return (
    <>
      <input value={value} onChange={onValueChange} />
      <ClearButton />
    </>
  );
};

In the above example, even though ClearButton does not have any props, the value change still causes ClearButton to be re-rendered. Why does this happen? Rewrite the App above and it becomes clear.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const App = () => {
  console.count('app render');

  const [value, setValue] = useState('');
  const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) =>
    setValue(event.target.value);

  return React.createElement(React.Fragment, {}, [
    React.createElement('input', { value, onChange: onValueChange }, null),
    React.createElement(ClearButton, {}, null),
  ]);
};

The second argument to React.createElement is the component’s props, and you can see that even though ClearButton doesn’t have any props, each render of App still passes a new empty object as its props, which is why ClearButton re-renders This is why ClearButton re-renders.

So how can we avoid this unnecessary rendering? The answer is the shouldComponentUpdate lifecycle for Class components and React.memo for Function components, shouldComponentUpdate and React.memo allow the developer to manually determine if the component needs to be re-rendered.

For the above example, simply wrapping React.memo around ClearButton would prevent ClearButton from being re-rendered.

So, useCallback is not a silver bullet for React performance optimisation, rather, the wrong use of it can lead to negative optimisation.

useMemo

In fact, useCallback is syntactic sugar for useMemo, and useCallback(fn, dependencies) is equivalent to useMemo(() => fn, dependencies). As with useCallback, useMemo is not used as much as it should be, so when should you use useMemo?

In the first case, as with useCallback, when a child component uses shouldComponentUpdate or React.memo, the value calculated by useMemo ensures that the props remain the same, thus avoiding repeated rendering by the child component.

The second case is where the performance consumption of the value calculation exceeds the performance consumption of the call to useMemo. Similar to the decomposition of useCallback above, using useMemo adds a computed function fn generation, a dependency array dependencies generation and a single call to useMemo, if the value calculation requires more performance than the call to useMemo, then using useMemo is can be optimized.

Reference