Проблема
Почему isActive
ложно?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(обратите внимание, для удобства я говорю только о mouseMoveHandler
, но все здесь относится и к mouseUpHandler
)
Когда запускается приведенный выше код, создается экземпляр функции, который извлекает переменную isActive
через закрытие функции . Эта переменная является константой, поэтому, если isActive
является ложным, когда функция определена, то всегда будет false
, пока существует экземпляр функции.
useEffect
также принимает функцию, и эта функция имеет постоянную ссылку на ваш экземпляр функции moveMouseHandler
- поэтому, пока существует этот обратный вызов useEffect, она ссылается на копию moveMouseHandler
, где isActive
равно false.
Когда изменяется isActive
, компонент перезапускается, и создается новый экземпляр moveMouseHandler
, в котором isActive
равен true
. Однако useEffect
перезапускает свою функцию только в случае изменения зависимостей - в этом случае зависимости ([box]
) не изменились, поэтому useEffect
не запускается повторно и версия moveMouseHandler
, где isActive
false - все еще прикреплено к окну, независимо от текущего состояния.
Вот почему ловушка "исчерпывающего deps" предупреждает вас о useEffect
- некоторые из его зависимостей могут измениться, не вызывая повторный запуск ловушки и обновление этих зависимостей.
Исправление
Поскольку перехватчик косвенно зависит от isActive
, вы могли бы исправить это, добавив isActive
в массив deps
для useEffect
:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
Однако, это не очень чисто: если вы измените mouseMoveHandler
так, чтобы оно зависело от большего количества состояний, у вас будет та же ошибка, если вы не забудете прийти и добавить ее также в массив deps
, (Также линтеру это не понравится)
Функция useEffect
косвенно зависит от isActive
, поскольку она напрямую зависит от mouseMoveHandler
; так что вместо этого вы можете добавить это к зависимостям:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
С этим изменением useEffect будет перезапущен с новыми версиями mouseMoveHandler
, что означает, что оно будет уважать isActive
. Однако он будет запускаться слишком часто - он будет запускаться каждый раз, когда mouseMoveHandler
становится новым экземпляром функции ... который представляет собой каждый отдельный рендер, поскольку новая функция создается при каждом рендеринге.
Нам не нужно создавать новую функцию при каждом рендеринге, только когда isActive
изменился: React предоставляет хук useCallback
для этого варианта использования. Вы можете определить свой mouseMoveHandler
как
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
и теперь новый экземпляр функции создается только при изменении isActive
, после чего useEffect
запускается в соответствующий момент, и вы можете изменить определение mouseMoveHandler
(например, добавив больше состояний) без прерывания ваш useEffect
крючок.
Скорее всего, это все еще создает проблему с вашим хуком useEffect
: он будет перезапускаться каждый раз при isActive
изменениях, что означает, что он будет устанавливать центральную точку окна при каждом изменении isActive
, что, вероятно, нежелательно. Вы должны разделить свой эффект на два отдельных эффекта, чтобы избежать этой проблемы:
useEffect(() => {
// update box center
}, [box])
useEffect(() => {
// expose window methods
}, [mouseMoveHandler, mouseUpHandler]);
Конечный результат
В конечном итоге ваш код должен выглядеть следующим образом:
const mouseMoveHandler = useCallback(e => {
/* ... */
}, [isActive]);
const mouseUpHandler = useCallback(e => {
/* ... */
}, [isActive]);
useEffect(() => {
/* update box center */
}, [box]);
useEffect(() => {
/* expose callback methods */
}, [mouseUpHandler, mouseMoveHandler])
Подробнее:
Дэн Абрамов, один из авторов React, подробно описывает свое Полное руководство по использованию эффекта blogpost.