Spec-Zone.ru › React 18

useCallback

useCallback — это хук React, позволяющий кэшировать определение функции между перерисовками.

const cachedFn = useCallback(fn, dependencies)
  • Ссылка
    • useCallback(fn, dependencies)
  • Использование
    • Пропуск перерисовки компонентов
    • Обновление состояния из кэшированной обратной функции
    • Предотвращение слишком частого срабатывания эффекта
    • Оптимизация пользовательского хука
  • Отладка
    • Каждый раз при перерисовке компонента, useCallback возвращает другую функцию
    • Мне нужно вызвать useCallback для каждого элемента списка в цикле, но это запрещено

Ссылка

useCallback(fn, dependencies)

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

import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

См. дополнительные примеры ниже.

Параметры

  • fn: Значение функции, которое вы хотите кэшировать. Оно может принимать любые аргументы и возвращать любые значения. React вернёт (но не вызовет!) вашу функцию во время первоначальной отрисовки. При последующих отрисовках React вернёт вам ту же функцию, если значения dependencies не изменились с последней отрисовки. В противном случае он вернёт функцию, переданную вами в текущей отрисовке, и сохранит её, чтобы использовать позже. React не будет вызывать вашу функцию. Функция возвращается вам, чтобы вы могли решить, когда и вызывать её.

  • dependencies: Список всех реактивных значений, используемых внутри кода fn. Реактивные значения включают свойства, состояние и все переменные и функции, объявленные непосредственно внутри тела вашего компонента. Если ваш линтер настроен для React, он проверит, что каждое реактивное значение правильно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть записан в строке, например, [dep1, dep2, dep3]. React будет сравнивать каждую зависимость со своим предыдущим значением, используя алгоритм сравнения Object.is.

Возвращаемое значение

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

Во время последующих отрисовок он либо вернёт уже сохранённую fn функцию из предыдущей отрисовки (если зависимости не изменились), либо вернёт fn функцию, которую вы передали в этой отрисовке.

Примечания

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

Использование

Пропуск перерисовки компонентов

При оптимизации производительности отрисовки вам иногда нужно кэшировать функции, которые вы передаёте дочерним компонентам. Давайте сначала рассмотрим синтаксис, а затем случаи, когда это полезно.

Чтобы кэшировать функцию между перерисовками вашего компонента, оберните её определение в хук useCallback:

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
  // ...

Вам нужно передать две вещи в useCallback:

  1. Определение функции, которую вы хотите кэшировать между перерисовками.
  2. Список зависимостей, включая все значения внутри вашего компонента, используемые внутри вашей функции.

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

При последующих отрисовках React сравнит зависимости с зависимостями, переданными во время предыдущей отрисовки. Если ни одна из зависимостей не изменилась (сравнение выполняется с помощью Object.is), useCallback вернёт ту же функцию, что и раньше. В противном случае useCallback вернёт функцию, переданную в этой отрисовке.

Другими словами, useCallback кэширует функцию между перерисовками до тех пор, пока не изменятся её зависимости.

Давайте разберём пример, чтобы понять, когда это полезно.

Предположим, вы передаёте функцию handleSubmit из компонента ProductPage в компонент ShippingForm:

function ProductPage({ productId, referrer, theme }) {
  // ...
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );

Вы заметили, что переключение свойства theme на мгновение замораживает приложение, но если вы удалите <ShippingForm /> из JSX, оно работает быстро. Это говорит о том, что стоит попробовать оптимизировать компонент ShippingForm.

По умолчанию, при перерисовке компонента React перерисовывает все его дочерние элементы рекурсивно. Именно поэтому, когда ProductPage перерисовывается с другим значением theme, компонент ShippingForm также перерисовывается. Это нормально для компонентов, которые не требуют значительных вычислений для перерисовки. Но если вы обнаружили, что перерисовка медленная, вы можете сказать ShippingForm пропустить перерисовку, если его свойства такие же, как при последней перерисовке, обернув его в memo:

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

С этим изменением ShippingForm не будет перерисовываться, если все его свойства одинаковы, как и при последней перерисовке. Именно здесь кэширование функции становится важным! Предположим, вы определили handleSubmit без useCallback:

function ProductPage({ productId, referrer, theme }) {
  // Every time the theme changes, this will be a different function...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

В JavaScript, function () {} или () => {} всегда создают другую функцию, аналогично тому, как объект-литерал {} всегда создаёт новый объект. Обычно это не проблема, но это означает, что свойства ShippingForm никогда не будут одинаковыми, и ваша оптимизация memo не сработает. Именно здесь useCallback становится полезным:

function ProductPage({ productId, referrer, theme }) {
  // Tell React to cache your function between re-renders...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...so as long as these dependencies don't change...

  return (
    <div className={theme}>
      {/* ...ShippingForm will receive the same props and can skip re-rendering */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

Оборачивая handleSubmit в useCallback, вы гарантируете, что это одна и та же функция между перерисовками (до тех пор, пока не изменятся зависимости). Вам не обязательно оборачивать функцию в useCallback , если вы это не делаете по какой-то причине. В этом примере причина в том, что вы передаёте её в компонент, обернутый в memo, и это позволяет ему пропускать перерисовку. Есть и другие причины, по которым вам может понадобиться useCallback, которые описаны далее на этой странице.

Примечание

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

Подробное рассмотрение

Как useCallback связано с useMemo?

Вы часто видите useMemo вместе с useCallback. Они оба полезны, когда вы пытаетесь оптимизировать дочерний компонент. Они позволяют вам кэшировать (или, другими словами, сохранять в кэше) то, что вы передаёте:

import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
  const product = useData('/product/' + productId);

  const requirements = useMemo(() => { // Calls your function and caches its result
    return computeRequirements(product);
  }, [product]);

  const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
    </div>
  );
}

Разница в том, что они кэшируют:

  • useMemo кэширует результат вызова вашей функции. В этом примере он кэширует результат вызова computeRequirements(product), чтобы он не менялся, если не изменилось product. Это позволяет вам передавать объект requirements без ненужной перерисовки ShippingForm. При необходимости React вызовет функцию, которую вы передали во время отрисовки, чтобы вычислить результат.
  • useCallback кэширует саму функцию. В отличие от useMemo, он не вычисляет функцию, которую вы передали. Вместо этого он кэширует предоставленную вами функцию, так что handleSubmit само по себе не изменяется, если не изменились productId или referrer. Это позволяет вам передавать функцию handleSubmit без ненужной перерисовки ShippingForm. Ваш код не будет выполняться, пока пользователь не отправит форму.

Если вы уже знакомы с useMemo, вам может помочь понимать useCallback как:

// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}

Узнайте больше о разнице между useMemo и useCallback.

Глубокое погружение

Нужно ли добавлять useCallback везде?

Если ваше приложение похоже на этот сайт, и большинство взаимодействий являются крупными (например, замена страницы или всего раздела), кеширование обычно не требуется. С другой стороны, если ваше приложение больше похоже на редактор чертежей, и большинство взаимодействий являются мелкими (например, перемещение фигур), тогда вы можете обнаружить, что кеширование очень полезно.

Кэширование функции с помощью useCallback полезно только в нескольких случаях:

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

В других случаях нет никакой выгоды от обертывания функции в useCallback. Также нет существенного вреда, поэтому некоторые команды предпочитают не думать об отдельных случаях и кешировать как можно больше. Недостатком является то, что код становится менее читаемым. Кроме того, не всё кеширование эффективно: одно значение, которое «всегда новое», достаточно, чтобы нарушить кеширование для всего компонента.

Обратите внимание, что useCallback не препятствует созданию функции. Вы всегда создаёте функцию (и это нормально!), но React игнорирует её и возвращает вам кэшированную функцию, если ничего не изменилось.

На практике вы можете сделать много кеширования неважным, следуя нескольким принципам:

  1. Когда компонент визуально охватывает другие компоненты, пусть он принимает JSX в качестве дочерних элементов. Затем, если компонент-обёртка обновляет собственное состояние, React знает, что его дочерним элементам не нужно перерисовываться.
  2. Предпочитайте локальное состояние и не поднимайте состояние вверх дальше, чем необходимо. Не храните переходное состояние, такое как формы и то, наведён ли курсор на элемент, в верхней части дерева или в глобальной библиотеке состояния.
  3. Держите логику отрисовки чистой. Если повторное отображение компонента вызывает проблему или производит заметные визуальные артефакты, это ошибка в вашем компоненте! Исправьте ошибку, а не добавляйте кеширование.
  4. Избегайте необходимых эффектов обновления состояния. Большинство проблем с производительностью в приложениях React вызваны цепочками обновлений, исходящими от эффектов, которые заставляют ваши компоненты перерисовываться снова и снова.
  5. Постарайтесь убрать ненужные зависимости из ваших эффектов. Например, вместо кеширования часто проще переместить какой-либо объект или функцию внутрь эффекта или за пределы компонента.

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


Обновление состояния из кэшированного обратного вызова

Иногда вам может потребоваться обновить состояние, основываясь на предыдущем состоянии из кэшированного обратного вызова.

Эта handleAddTodo функция указывает todos в качестве зависимости, потому что она вычисляет новые задачи из неё:

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...

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

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, newTodo]);
  }, []); // ✅ No need for the todos dependency
  // ...

Здесь, вместо того, чтобы сделать todos зависимостью и прочитать её внутри, вы передаёте инструкции о том, как обновить состояние (todos => [...todos, newTodo]) React. Дополнительная информация о функциях обновления.


Предотвращение запуска эффекта слишком часто

Иногда вы можете вызвать функцию изнутри эффекта:

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    // ...

Это создаёт проблему. Каждое реактивное значение должно быть объявлено в качестве зависимости от вашего эффекта. Однако, если вы объявляете createOptions в качестве зависимости, это приведёт к постоянному подключению вашего эффекта к чату:

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🔴 Problem: This dependency changes on every render
  // ...

Чтобы решить эту проблему, вы можете обернуть функцию, которую нужно вызвать из эффекта, в useCallback:

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // ✅ Only changes when roomId changes

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // ✅ Only changes when createOptions changes
  // ...

Это гарантирует, что функция createOptions будет одинаковой между повторными отображениями, если roomId одинакова. Однако ещё лучше избавиться от необходимости зависимости от функции. Переместите свою функцию внутрь эффекта:

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() { // ✅ No need for useCallback or function dependencies!
      return {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ Only changes when roomId changes
  // ...

Теперь ваш код проще и не требует useCallback. Дополнительная информация об удалении зависимостей эффекта.


Оптимизация пользовательского хука

Если вы пишете пользовательский хук, рекомендуется обернуть любые функции, которые он возвращает, в useCallback:

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}

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


Отладка

Каждый раз, когда мой компонент перерисовывается, useCallback возвращает другую функцию

Убедитесь, что вы указали массив зависимостей в качестве второго аргумента!

Если вы забываете массив зависимостей, useCallback будет возвращать новую функцию каждый раз:

function ProductPage({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }); // 🔴 Returns a new function every time: no dependency array
  // ...

Вот исправленная версия, передающая массив зависимостей в качестве второго аргумента:

function ProductPage({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ✅ Does not return a new function unnecessarily
  // ...

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

  const handleSubmit = useCallback((orderDetails) => {
    // ..
  }, [productId, referrer]);

  console.log([productId, referrer]);

Затем вы можете щелкнуть правой кнопкой мыши по массивам из различных повторных отображений в консоли и выбрать «Сохранить как глобальную переменную» для обоих из них. Предположим, что первый был сохранён как temp1 , а второй как temp2, вы можете использовать консоль браузера, чтобы проверить, одинакова ли каждая зависимость в обоих массивах:

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

Когда вы найдёте зависимость, нарушающую кеширование, найдите способ её удалить или сделайте кеширование и её тоже.


Мне нужно вызвать useCallback для каждого элемента списка в цикле, но это запрещено

Предположим, что компонент Chart обернут в memo. Вы хотите пропустить повторное отображение каждого Chart в списке, когда компонент ReportList перерисовывается. Однако вы не можете вызвать useCallback в цикле:

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 You can't call useCallback in a loop like this:
        const handleClick = useCallback(() => {
          sendReport(item)
        }, [item]);

        return (
          <figure key={item.id}>
            <Chart onClick={handleClick} />
          </figure>
        );
      })}
    </article>
  );
}

Вместо этого извлеките компонент для отдельного элемента и поместите useCallback туда:

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ Call useCallback at the top level:
  const handleClick = useCallback(() => {
    sendReport(item)
  }, [item]);

  return (
    <figure>
      <Chart onClick={handleClick} />
    </figure>
  );
}

В качестве альтернативы, вы можете удалить useCallback в последнем фрагменте и вместо этого обернуть Report в memo. Если свойство item не меняется, Report пропустит повторное отображение, поэтому Chart тоже пропустит повторное отображение:

function ReportList({ items }) {
  // ...
}

const Report = memo(function Report({ item }) {
  function handleClick() {
    sendReport(item);
  }

  return (
    <figure>
      <Chart onClick={handleClick} />
    </figure>
  );
});

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react/useCallback

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API