useCallback¶
useCallback
- это хук React, который позволяет кэшировать определение функции между повторными рендерингами.
1 |
|
Описание¶
useCallback(fn, dependencies)
¶
Вызовите useCallback
на верхнем уровне вашего компонента, чтобы кэшировать определение функции между рендерингами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Параметры¶
fn
-
Значение функции, которое вы хотите кэшировать. Она может принимать любые аргументы и возвращать любые значения. React вернет (не вызовет!) вашу функцию обратно во время первоначального рендера. При последующих рендерах React вернет вам ту же функцию, если
dependencies
не изменились с момента последнего рендера. В противном случае, он отдаст вам функцию, которую вы передали во время текущего рендеринга, и сохранит ее на случай, если она может быть использована позже. React не будет вызывать вашу функцию. Функция возвращается вам, чтобы вы могли решить, когда и стоит ли ее вызывать. dependencies
-
Список всех реактивных значений, на которые ссылается код
fn
. Реактивные значения включают пропсы, состояние, а также все переменные и функции, объявленные непосредственно в теле вашего компонента. Если ваш линтер настроен на React, он проверит, что каждое реактивное значение правильно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть написан inline по типу[dep1, dep2, dep3]
. React будет сравнивать каждую зависимость с предыдущим значением, используя алгоритм сравненияObject.is
.
Возвращаемое значение¶
При первоначальном рендере useCallback
возвращает переданную вами функцию fn
.
При последующих рендерах она либо вернет уже сохраненную функцию fn
из последнего рендера (если зависимости не изменились), либо вернет функцию fn
, которую вы передали во время этого рендера.
Ограничения¶
useCallback
- это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, создайте новый компонент и перенесите состояние в него.- React не будет выбрасывать кэшированную функцию, если для этого нет особой причины. Например, в разработке React выбрасывает кэш, когда вы редактируете файл вашего компонента. Как в разработке, так и в продакшене, React отбрасывает кэш, если ваш компонент приостанавливается во время начального монтирования. В будущем React может добавить больше функций, которые будут использовать преимущества отбрасывания кэша - например, если React в будущем добавит встроенную поддержку виртуализированных списков, то будет иметь смысл отбрасывать кэш для элементов, которые прокручиваются из области просмотра виртуализированной таблицы. Это должно соответствовать вашим ожиданиям, если вы полагаетесь на
useCallback
в качестве оптимизации производительности. В противном случае, более подходящими могут быть переменные состояния или ref.
Использование¶
Пропуск повторного рендеринга компонентов¶
При оптимизации производительности рендеринга иногда требуется кэшировать функции, передаваемые дочерним компонентам. Давайте сначала рассмотрим синтаксис, как это сделать, а затем посмотрим, в каких случаях это полезно.
Чтобы кэшировать функцию между повторными рендерингами компонента, оберните ее определение в хук useCallback
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Вам нужно передать две вещи в useCallback
:
- Определение функции, которую вы хотите кэшировать между повторными рендерами.
- Список зависимостей, включающий каждое значение в вашем компоненте, которое используется в вашей функции.
При первом рендере возвращаемая функция, которую вы получите от useCallback
, будет функцией, которую вы передали.
При последующих рендерах React будет сравнивать зависимости с зависимостями, которые вы передали во время предыдущего рендера. Если ни одна из зависимостей не изменилась (по сравнению с Object.is
), useCallback
вернет ту же функцию, что и раньше. В противном случае, useCallback
вернет функцию, которую вы передали на этом рендере.
Другими словами, useCallback
кэширует функцию между повторными рендерами, пока ее зависимости не изменятся.
Давайте рассмотрим пример, чтобы увидеть, когда это полезно.
Скажем, вы передаете функцию handleSubmit
от ProductPage
компоненту ShippingForm
:
1 2 3 4 5 6 7 8 9 |
|
Вы заметили, что при переключении параметра theme
приложение на мгновение замирает, но если убрать <ShippingForm />
из JSX, то все работает быстро. Это говорит о том, что стоит попробовать оптимизировать компонент ShippingForm
.
По умолчанию, когда компонент рендерится, React рекурсивно рендерит все его дочерние компоненты. Вот почему, когда ProductPage
рендерится с другой theme
, компонент ShippingForm
также рендерится. Это хорошо для компонентов, которые не требуют больших вычислений для повторного рендеринга. Но если вы убедились, что повторный рендеринг медленный, вы можете сказать ShippingForm
пропустить повторный рендеринг, когда его пропсы такие же, как и при последнем рендере, обернув его в memo
:
1 2 3 4 5 6 7 |
|
После этого изменения ShippingForm
будет пропускать повторный рендеринг, если все его пропсы те же, что и при последнем рендеринге. Вот когда кэширование функции становится важным! Допустим, вы определили handleSubmit
без useCallback
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
В JavaScript function () {}
или () => {}
всегда создает разную функцию, подобно тому, как объектный литерал {}
всегда создает новый объект. Обычно это не является проблемой, но это означает, что пропс ShippingForm
никогда не будет одинаковым, и ваша оптимизация memo
не будет работать. Вот здесь-то и пригодится useCallback
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Вернув handleSubmit
в useCallback
, вы гарантируете, что это будет одна и та же функция между повторными рендерами (пока не изменятся зависимости). Вы не обязаны обертывать функцию в useCallback
, если только вы не делаете это по какой-то конкретной причине. В данном примере причина в том, что вы передаете ее компоненту, обернутому в memo
, и это позволяет ему пропустить повторный рендеринг. Есть и другие причины, по которым вам может понадобиться useCallback
, которые описаны далее на этой странице.
useCallback только для оптимизации
Вы должны полагаться на useCallback
только в качестве оптимизации производительности. Если ваш код не работает без него, найдите основную проблему и сначала устраните ее. Затем вы можете добавить useCallback
обратно.
Как useCallback связан с useMemo?
Вы часто будете видеть useMemo
вместе с useCallback
. Они оба полезны, когда вы пытаетесь оптимизировать дочерний компонент. Они позволяют вам мемоизировать (или, другими словами, кэшировать) то, что вы передаете вниз:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Разница в том, что именно они позволяют вам кэшировать:
useMemo
кэширует результат вызова вашей функции. В этом примере он кэширует результат вызоваcomputeRequirements(product)
, чтобы он не изменился, еслиproduct
не изменился. Это позволяет передавать объектrequirements
вниз без ненужного повторного рендерингаShippingForm
. При необходимости React будет вызывать переданную вами функцию во время рендеринга для вычисления результата.useCallback
кэширует саму функцию. В отличие отuseMemo
, он не вызывает предоставленную вами функцию. Вместо этого она кэширует предоставленную вами функцию, так чтоhandleSubmit
сама по себе не изменяется, если толькоproductId
илиreferrer
не изменились. Это позволяет вам передавать функциюhandleSubmit
вниз без ненужного повторного рендерингаShippingForm
. Ваш код не будет выполняться до тех пор, пока пользователь не отправит форму.
Если вы уже знакомы с useMemo
, вам будет полезно представить useCallback
следующим образом:
1 2 3 4 |
|
Подробнее о разнице между useMemo
и useCallback
.
Должны ли вы везде добавлять useCallback?
Если ваше приложение похоже на этот сайт, и большинство взаимодействий являются грубыми (например, замена страницы или целого раздела), мемоизация обычно не нужна. С другой стороны, если ваше приложение больше похоже на редактор рисунков, и большинство взаимодействий являются гранулированными (например, перемещение фигур), то мемоизация может оказаться очень полезной.
Кэширование функции с useCallback
полезно только в нескольких случаях:
- Вы передаете ее в качестве параметра компоненту, обернутому в
memo
. Вы хотите пропустить повторный рендеринг, если значение не изменилось. Мемоизация позволяет вашему компоненту повторно отображаться только в том случае, если изменились зависимости. - Функция, которую вы передаете, позже будет использоваться как зависимость какого-то Hook. Например, другая функция, обернутая в
useCallback
, зависит от нее, или вы зависите от этой функции изuseEffect
.
В других случаях нет никакой пользы от обертывания функции в useCallback
. Вреда от этого тоже нет, поэтому некоторые команды предпочитают не думать об отдельных случаях и мемоизировать как можно больше. Недостатком является то, что код становится менее читабельным. Кроме того, не вся мемоизация эффективна: одного значения, которое "всегда новое", достаточно, чтобы сломать мемоизацию для всего компонента.
Обратите внимание, что useCallback
не предотвращает создание функции. Вы всегда создаете функцию (и это хорошо!), но React игнорирует это и возвращает вам кэшированную функцию, если ничего не изменилось.
На практике вы можете сделать ненужной мемоизацию, следуя нескольким принципам:.
- Когда компонент визуально оборачивает другие компоненты, пусть он принимает JSX в качестве дочерних компонентов Тогда, если компонент-обертка обновляет свое состояние, React знает, что его дочерние компоненты не нужно перерисовывать.
- Предпочитайте локальное состояние и не поднимайте состояние вверх дальше, чем это необходимо. Не храните переходные состояния, такие как формы и то, наведен ли элемент на вершину вашего дерева или в глобальной библиотеке состояний.
- Сохраняйте чистоту логики рендеринга Если повторный рендеринг компонента вызывает проблему или приводит к заметным визуальным артефактам, это ошибка в вашем компоненте! Исправьте ошибку вместо того, чтобы добавлять мемоизацию.
- Избегайте ненужных Эффектов, обновляющих состояние Большинство проблем с производительностью в приложениях React вызваны цепочками обновлений, исходящих от Эффектов, которые заставляют ваши компоненты рендериться снова и снова.
- Попробуйте удалить ненужные зависимости из ваших Эффектов. Например, вместо мемоизации часто проще переместить какой-то объект или функцию внутрь Эффекта или за пределы компонента.
Если конкретное взаимодействие все еще кажется нестабильным, используйте профилировщик React Developer Tools, чтобы увидеть, какие компоненты больше всего выигрывают от мемоизации, и добавьте мемоизацию там, где это необходимо. Эти принципы облегчают отладку и понимание ваших компонентов, поэтому следовать им полезно в любом случае. В перспективе мы исследуем возможность автоматической мемоизации, чтобы решить эту проблему раз и навсегда.
Разница между useCallback и объявлением функции напрямую¶
1. Пропуск повторного рендеринга с useCallback
и memo
¶
В этом примере компонент ShippingForm
искусственно замедлен, чтобы вы могли увидеть, что происходит, когда React-компонент, который вы рендерите, действительно медленный. Попробуйте увеличить счетчик и переключить тему.
Увеличение счетчика кажется медленным, потому что это заставляет замедленный ShippingForm
перерисовываться. Это ожидаемо, потому что счетчик изменился, и вам нужно отразить новый выбор пользователя на экране.
Далее попробуйте переключить тему. Благодаря useCallback
вместе с memo
, это происходит быстро, несмотря на искусственное замедление! ShippingForm
пропускает повторное отображение, потому что функция handleSubmit
не изменилась. Функция handleSubmit
не изменилась, потому что productId
и referrer
(ваши зависимости useCallback
) не изменились с момента последнего рендеринга.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
|
2. Всегда перерендеринг компонента¶
В этом примере реализация ShippingForm
также искусственно замедлена, чтобы вы могли увидеть, что происходит, когда какой-либо компонент React, который вы рендерите, действительно медленный. Попробуйте увеличить счетчик и переключить тему.
В отличие от предыдущего примера, переключение темы теперь также происходит медленно! Это происходит потому, что в этой версии нет вызова useCallback
, поэтому handleSubmit
всегда является новой функцией, и замедленный компонент ShippingForm
не может пропустить повторный рендеринг.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
|
Однако, вот тот же код с искусственным замедлением. Отсутствие useCallback
ощущается заметно или нет?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Довольно часто код без мемоизации работает нормально. Если ваши взаимодействия достаточно быстрые, мемоизация не нужна.
Помните, что вам нужно запустить React в производственном режиме, отключить React Developer Tools и использовать устройства, похожие на те, которые есть у пользователей вашего приложения, чтобы получить реальное представление о том, что на самом деле замедляет работу вашего приложения.
Обновление состояния из мемоизированного обратного вызова¶
Иногда вам может потребоваться обновить состояние на основе предыдущего состояния из мемоизированного обратного вызова.
Эта функция handleAddTodo
указывает todos
как зависимость, потому что она вычисляет следующий todos
из него:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Обычно вы хотите, чтобы мемоизированные функции имели как можно меньше зависимостей. Когда вы читаете некоторое состояние только для вычисления следующего состояния, вы можете устранить эту зависимость, передав вместо него функцию обновления состояния:
1 2 3 4 5 6 7 8 9 |
|
Здесь вместо того, чтобы сделать todos
зависимостью и читать ее внутри, вы передаете в React инструкцию о том, как обновить состояние (todos => [...todos, newTodo]
). Подробнее о функциях обновления .
Предотвращение слишком частого срабатывания эффекта¶
Иногда вы можете захотеть вызвать функцию внутри Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Это создает проблему. Каждое реактивное значение должно быть объявлено зависимостью вашего Эффекта. Однако, если вы объявите createOptions
как зависимость, это заставит ваш Эффект постоянно переподключаться к чату:
1 2 3 4 5 6 7 |
|
Чтобы решить эту проблему, вы можете обернуть функцию, которую нужно вызвать из Effect, в useCallback
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Это гарантирует, что функция createOptions
будет одинаковой между повторными рендерингами, если roomId
одинаков. Однако, еще лучше устранить необходимость в зависимости от функции. Переместите вашу функцию внутрь Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Теперь ваш код стал проще и не нуждается в useCallback
. Подробнее об удалении зависимостей от эффектов .
Оптимизация пользовательского хука¶
Если вы пишете пользовательский хук, рекомендуется обернуть все функции, которые он возвращает, в useCallback
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Это гарантирует, что потребители вашего хука смогут при необходимости оптимизировать свой собственный код.
Устранение неполадок¶
Каждый раз, когда мой компонент рендерится, useCallback
возвращает другую функцию¶
Убедитесь, что вы указали массив зависимостей в качестве второго аргумента!
Если вы забудете про массив зависимостей, useCallback
будет возвращать каждый раз новую функцию:
1 2 3 4 5 6 7 8 9 |
|
Это исправленная версия, передающая массив зависимостей в качестве второго аргумента:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Если это не помогло, то проблема в том, что по крайней мере одна из ваших зависимостей отличается от предыдущего рендера. Вы можете отладить эту проблему, вручную записав логи зависимостей в консоль:
1 2 3 4 5 6 7 8 |
|
Затем вы можете щелкнуть правой кнопкой мыши на массивах из разных рендеров в консоли и выбрать "Store as a global variable" для обоих. Предположив, что первый массив был сохранен как temp1
, а второй - как temp2
, вы можете использовать консоль браузера, чтобы проверить, является ли каждая зависимость в обоих массивах одинаковой:
1 2 3 4 5 6 7 8 |
|
Когда вы обнаружите, какая зависимость нарушает мемоизацию, либо найдите способ удалить ее, либо мемоизируйте и ее.
Мне нужно вызвать useCallback
для каждого элемента списка в цикле, но это не разрешено¶
Предположим, что компонент Chart
обернут в memo
. Вы хотите пропустить повторное отображение каждого Chart
в списке при повторном отображении компонента ReportList
. Однако вы не можете вызвать useCallback
в цикле:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Вместо этого извлеките компонент для отдельного элемента и поместите туда useCallback
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
В качестве альтернативы можно убрать useCallback
в последнем фрагменте и вместо этого обернуть сам Report
в memo
. Если параметр item
не меняется, Report
пропускает повторный рендеринг, поэтому Chart
тоже пропускает повторный рендеринг:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Источник — https://react.dev/reference/react/useCallback