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

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

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

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

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

P.S. Думаю, визуализация с помощью пример кода поможет каждому лучше понять это. Объяснено в документации.

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
76
0
34 815
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

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

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

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

class Foo extends Component {
  handleClick() {
    console.info('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.info('Click happened');
  }
  render() {
    return <Button onClick = {this.handleClick}>Click Me</Button>;
  }
}

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

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

1. Стрелка в Render -> Un-memoized callback

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

2. Bind in Constructor (ES2015) -> Memoized callbacks.

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

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

Следовательно, в первом случае, если Button реализован с использованием React.memo, он всегда будет повторно визуализироваться (если у вас нет какой-либо пользовательской функции сравнения), потому что опора onClick каждый раз отличается, во втором случае - не будет.

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

Is it OK to use arrow functions in render methods? Generally speaking, yes, it is OK, and it is often the easiest way to pass parameters to callback functions.

If you do have performance issues, by all means, optimize!

Тогда, если моя функция handleClick имеет некоторые изменения данных или параметров, тогда я НЕ должен использовать useCallback, да?

ZiiMakc 06.11.2018 21:13

Да, если ваше возвращаемое значение может измениться даже с теми же параметрами, вам не следует использовать useCallback, потому что возвращаемое значение запомнено.

Yangshun Tay 06.11.2018 21:30

Как вы используете его с функцией двойной стрелки? Как это: const tagLike = useCallback ((tagId, tagIndex) => async () => {})?

ZiiMakc 11.03.2019 00:06

@YangshunTay имейте в виду, что если вы передадите аргументы в функцию useCallback и добавите [] в конце (чтобы она не отслеживала изменения), она всегда будет возвращать мемоизированную функцию, а не обновлять аргументы.

Jony-Y 23.03.2019 09:12

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

user239558 12.04.2019 09:08

Для людей, которые не замечают второй аргумент и может быть любопытно, почему он не работает, ему нужен [] в качестве второго аргумента useCallback, чтобы сообщить React о необходимости мемоизации независимо от аргументов.

Supasate 26.05.2019 09:04

@YangshunTay, почему вы сказали, что если handleClick содержит асинхронный вызов, то мы не должны использовать useCallback? Судя по вашему полному описанию, которое даже лучше, чем в Документах, мы используем useCallback, когда хотим передать его оптимизированному дочернему компоненту в качестве свойств. Правильно? Так почему бы нам не использовать useCallback?

AmerllicA 04.05.2020 22:54

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

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.info(memoizedValue), []);
    const unMemoizedCallback = () => console.info(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'));

Я не уверен, что это полезно, memoizedCallback недействителен, поскольку он обращается к memoizedValue и имеет пустой список deps, не так ли?

user239558 12.04.2019 09:10

@ user239558, когда вы обновляете состояние, нажимая кнопку «обновить состояние», вы должны увидеть, что prevMemoizedCallback совпадает со следующим memoizedCallback, который делает то, что должен делать, чтобы запоминать обратный вызов, который можно использовать во всех обновлениях. Тот факт, что в этом примере я использовал memoizedValue, он просто демонстрирует, что вы можете использовать первое случайное значение, сгенерированное при первоначальном рендеринге, а для других обновлений каждый раз генерируются новые случайные значения.

stackoverflow 13.04.2019 11:15

useCallback и useMemo - это попытка обойти слабые места, которые связаны с подходом функционального программирования, выбранным с помощью перехватчиков React. В Javascript каждая сущность, независимо от того, является ли она функцией, переменной или чем-то еще, создается в памяти, когда выполнение входит в блок кода функции. Это большая проблема для React, который попытается определить, нужно ли отрендерить компонент. Необходимость повторной визуализации вычитается на основе входных свойств и контекстов. Давайте посмотрим на простой пример без useCallback.

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = () => {
    setCounter(counter + 1);
  }

  return <div>
    Counter:{counter}<br/>
    <button onClick = {handleClick}>+1</button>
  </div>
}

Обратите внимание, что экземпляр функции handleClick будет создаваться при каждом вызове функции внутри блока, поэтому адрес обработчика событий при каждом вызове будет другим. Фреймворк React всегда будет видеть обработчик событий как измененный из-за этого. В приведенном выше примере React будет рассматривать handleClick как новое значение при каждом вызове. У него просто нет инструментов, чтобы отличить его от одного и того же вызова.

Что делает useCallback, он внутренне сохраняет первую введенную версию функции и возвращает ее вызывающей стороне, если перечисленные переменные не изменились.

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
  }, [])

  return <div>
    Counter:{counter}<br/>
    <button onClick = {handleClick}>+1</button>
  </div>
}

Теперь, с помощью приведенного выше кода, React идентифицирует обработчик события handleClick как тот же самый, благодаря вызову функции useCallback. Он всегда будет возвращать один и тот же экземпляр функции, и механизм рендеринга компонентов React будет доволен.

Сохранение функции внутри useCallback приведет к новой проблеме. Сохраненный экземпляр вызова функции не будет иметь прямого доступа к переменным текущего вызова функции. Вместо этого он увидит переменные, введенные в вызове первоначального закрытия, в котором была создана сохраненная функция. Таким образом, вызов не будет работать для обновленных переменных. Вот почему вам нужно знать, изменились ли некоторые используемые переменные. Таким образом, useCallback сохранит текущий экземпляр вызова функции как новый сохраненный экземпляр. Список переменных в качестве второго аргумента useCallback - это список переменных для этой функции. В нашем примере нам нужно сообщить функции useCallback, что нам нужна свежая версия counter -variable при каждом вызове. Если мы этого не сделаем, значение счетчика после вызова всегда будет равно 1, что соответствует исходному значению 0 плюс 1.

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
  }, [counter])

  return <div>
    Counter:{counter}<br/>
    <button onClick = {handleClick}>+1</button>
  </div>
}

Теперь у нас есть рабочая версия кода, которая не будет перерисовываться при каждом вызове.

Приятно отметить, что вызов useState находится здесь по той же причине. Функциональный блок не имеет внутреннего состояния, поэтому хуки используют useState, useCallback и useMemo для имитации базовой функциональности классов. В этом смысле функциональное программирование - это большой шаг назад в истории, ближе к процедурному программированию.

useMemo - это тот же механизм, что и useCallback, но для других объектов и переменных. С его помощью вы можете ограничить потребность в повторном рендеринге компонентов, поскольку функция useMemo будет возвращать одни и те же значения при каждом вызове функции, если перечисленные поля не изменились.

Эта часть нового подхода к перехватам React определенно является самым слабым местом системы. useCallback в значительной степени нелогичен и действительно подвержен ошибкам. С вызовами и зависимостями useCallback слишком легко в конечном итоге изменить внутренние циклы. Этого предостережения у нас не было с подходом React Class.

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

Это отличный ответ!

Arvand 09.07.2021 19:31

Обработчик событий воссоздается, и ему по умолчанию назначается другой адрес при каждом рендеринге, что приводит к изменению объекта «props». Ниже кнопка 2 не отображается повторно, поскольку объект «props» не изменился. Обратите внимание, как вся функция Example () выполняется до завершения при каждом рендеринге.

const MyButton = React.memo(props=>{
   console.info('firing from '+props.id);
   return (<button onClick = {props.eh}>{props.id}</button>);
});

function Example(){
   const [a,setA] = React.useState(0);
   const unmemoizedCallback = () => {};
   const memoizedCallback = React.useCallback(()=>{},[]);   // don’t forget []!
   setTimeout(()=>{setA(a=>(a+1));},3000);
   return (<React.Fragment>
                 <MyButton id = "1" eh = {unmemoizedCallback}/>
                 <MyButton id = "2" eh = {memoizedCallback}/>
                 <MyButton id = "3" eh = {()=>memoizedCallback}/>
           </React.Fragment>);
} 
ReactDOM.render(<Example/>,document.querySelector("div"));

Другие вопросы по теме