Что делает useCallback/useMemo в React?

Как сказано в документации, useCallback Возвращает запомненный обратный вызов.

Передайте встроенный обратный вызов и массив входных данных. useCallback вернет запомненную версию обратного вызова, которая изменяется только в случае изменения одного из входных данных. Это полезно при передаче обратных вызовов оптимизированным дочерним компонентам, которые полагаются на равенство ссылок для предотвращения ненужных визуализаций (например, shouldComponentUpdate).

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Но как это работает и где лучше всего использовать его в React?

PS Думаю, визуализация на примере codepen поможет каждому лучше понять это.

Ответ 1

Это лучше всего использовать, когда вы хотите предотвратить ненужные повторные рендеры для лучшей производительности.

Сравните эти два способа передачи обратных вызовов дочерним компонентам, взятым из React Docs:

1. Функция стрелки в рендере

class Foo extends Component {
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <Button onClick={() => this.handleClick()}>Click Me</Button>;
  }
}

2. Привязка в конструкторе (ES2015)

class Foo extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <Button onClick={this.handleClick}>Click Me</Button>;
  }
}

Предполагая, что <Button> реализован как PureComponent, первый способ заставит <Button> перерисовывать каждый раз, когда <Foo> перерисовывает, потому что новая функция создается в каждом вызове render(). Во втором случае метод handleClick только один раз в конструкторе <Foo> и повторно используется при рендеринге.

Если мы переведем оба подхода на функциональные компоненты с помощью хуков, это эквиваленты (вроде):

1. Функция стрелки в рендере → незапамятный обратный вызов

function Foo() {
  const handleClick = () => {
    console.log('Click happened');
  }
  return <Button onClick={handleClick}>Click Me</Button>;
}

2. Bind in Constructor (ES2015) → Мемозированные обратные вызовы

function Foo() {
  const memoizedHandleClick = useCallback(
    () => console.log('Click happened'), [],
  ); // Tells React to memoize regardless of arguments.
  return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}

Первый способ создает обратные вызовы при каждом вызове функционального компонента, но во втором случае React запоминает функцию обратного вызова, и обратный вызов не создается несколько раз.

В большинстве случаев это нормально делать первым способом. Как говорится в документе React:

Можно ли использовать функции стрелок в методах рендеринга? В общем, да, это нормально, и часто это самый простой способ передать параметры в функции обратного вызова.

Если у вас есть проблемы с производительностью, обязательно оптимизируйте!

Ответ 2

Я сделал небольшой пример, чтобы помочь другим лучше понять, как он себя ведет. Вы можете запустить демо здесь или прочитать код ниже:

import React, { useState, useCallback, useMemo } from 'react';
import { render } from 'react-dom';

const App = () => {
    const [state, changeState] = useState({});
    const memoizedValue = useMemo(() => Math.random(), []);
    const memoizedCallback = useCallback(() => console.log(memoizedValue), []);
    const unMemoizedCallback = () => console.log(memoizedValue);
    const {prevMemoizedCallback, prevUnMemoizedCallback} = state;
    return (
      <>
        <p>Memoized value: {memoizedValue}</p>
        <p>New update {Math.random()}</p>
        <p>is prevMemoizedCallback === to memoizedCallback: { String(prevMemoizedCallback === memoizedCallback)}</p>
        <p>is prevUnMemoizedCallback === to unMemoizedCallback: { String(prevUnMemoizedCallback === unMemoizedCallback) }</p>
        <p><button onClick={memoizedCallback}>memoizedCallback</button></p>
        <p><button onClick={unMemoizedCallback}>unMemoizedCallback</button></p>
        <p><button onClick={() => changeState({ prevMemoizedCallback: memoizedCallback, prevUnMemoizedCallback: unMemoizedCallback })}>update State</button></p>
      </>
    );
};

render(<App />, document.getElementById('root'));