Почему мои обработчики событий срабатывают с неправильным состоянием при использовании React Hooks?

Я пытаюсь создать копию этого вращающегося div примера, используя перехватчики реакции. https://codesandbox.io/s/XDjY28XoV

Вот мой код на данный момент

import React, { useState, useEffect, useCallback } from 'react';

const App = () => {
  const [box, setBox] = useState(null);

  const [isActive, setIsActive] = useState(false);
  const [angle, setAngle] = useState(0);
  const [startAngle, setStartAngle] = useState(0);
  const [currentAngle, setCurrentAngle] = useState(0);
  const [boxCenterPoint, setBoxCenterPoint] = useState({});

  const setBoxCallback = useCallback(node => {
    if (node !== null) {
      setBox(node)
    }
  }, [])

  // to avoid unwanted behaviour, deselect all text
  const deselectAll = () => {
    if (document.selection) {
      document.selection.empty();
    } else if (window.getSelection) {
      window.getSelection().removeAllRanges();
    }
  }

  // method to get the positionof the pointer event relative to the center of the box
  const getPositionFromCenter = e => {
    const fromBoxCenter = {
      x: e.clientX - boxCenterPoint.x,
      y: -(e.clientY - boxCenterPoint.y)
    };
    return fromBoxCenter;
  }

  const mouseDownHandler = e => {
    e.stopPropagation();
    const fromBoxCenter = getPositionFromCenter(e);
    const newStartAngle =
      90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
    setStartAngle(newStartAngle);
    setIsActive(true);
  }

  const mouseUpHandler = e => {
    deselectAll();
    e.stopPropagation();
    if (isActive) {
      const newCurrentAngle = currentAngle + (angle - startAngle);
      setIsActive(false);
      setCurrentAngle(newCurrentAngle);
    }
  }

  const mouseMoveHandler = e => {
    if (isActive) {
      const fromBoxCenter = getPositionFromCenter(e);
      const newAngle =
        90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
      box.style.transform =
        "rotate(" +
        (currentAngle + (newAngle - (startAngle ? startAngle : 0))) +
        "deg)";
      setAngle(newAngle)
    }
  }

  useEffect(() => {
    if (box) {
      const boxPosition = box.getBoundingClientRect();
      // get the current center point
      const boxCenterX = boxPosition.left + boxPosition.width / 2;
      const boxCenterY = boxPosition.top + boxPosition.height / 2;

      // update the state
      setBoxCenterPoint({ x: boxCenterX, y: boxCenterY });
    }

    // in case the event ends outside the box
    window.onmouseup = mouseUpHandler;
    window.onmousemove = mouseMoveHandler;
  }, [ box ])

  return (
    <div className="box-container">
      <div
        className="box"
        onMouseDown={mouseDownHandler}
        onMouseUp={mouseUpHandler}
        ref={setBoxCallback}
      >
        Rotate
      </div>
    </div>
  );
}

export default App;

В настоящее время mouseMoveHandler вызывается с состоянием isActive = false, хотя на самом деле состояние истинно. Как я могу заставить этот обработчик событий срабатывать с правильным состоянием?

Также консоль регистрирует предупреждение:

React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array  react-hooks/exhaustive-deps

Почему мне нужно включать методы компонентов в массив зависимостей useEffect? Мне никогда не приходилось делать это для других более простых компонентов, использующих React Hooks.

Спасибо


person Jon_B    schedule 26.04.2019    source источник


Ответы (3)


Эта проблема

Почему isActive неверно?

const mouseMoveHandler = e => {
   if(isActive) {
       // ...
   }
};

(Обратите внимание, для удобства я говорю только о mouseMoveHandler, но все здесь применимо и к mouseUpHandler)

При выполнении приведенного выше кода создается экземпляр функции, который извлекает переменную isActive через закрытие функции. Эта переменная является константой, поэтому, если isActive имеет значение false, когда функция определена, то всегда будет false, пока существует этот экземпляр функции.

useEffect также принимает функцию, и эта функция имеет постоянную ссылку на ваш moveMouseHandler экземпляр функции - пока существует этот обратный вызов useEffect, он ссылается на копию moveMouseHandler, где isActive ложно.

Когда isActive изменяется, компонент перерисовывается, и будет создан новый экземпляр moveMouseHandler, в котором isActive равно true. Однако useEffect повторно запускает свою функцию только в том случае, если зависимости изменились - в этом случае зависимости ([box]) не изменились, поэтому useEffect не запускается повторно, а версия moveMouseHandler, где isActive ложно, все еще прикреплена к окну, независимо от текущего состояния.

Вот почему ловушка "excustive-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, подробно описывает подробности в своем Полном руководстве по useEffect. сообщение в блоге.

person Retsam    schedule 29.04.2019
comment
Это потрясающе. Спасибо. - person Jon_B; 29.04.2019

React Hooks событие useState + useEffect + возвращает устаревшее состояние. похоже, у вас похожие проблемы. основная проблема заключается в том, что «он получает свое значение от закрытия, в котором он был определен»

попробуйте это Решение 2 «Использовать ссылку». в вашем сценарии

Добавьте ниже useRef и useEffect.

let refIsActive = useRef(isActive);
useEffect(() => {
    refIsActive.current = isActive;
});

затем внутри mouseMoveHandler используйте эту ссылку

 const mouseMoveHandler = (e) => {    
  console.log('isActive',refIsActive.current);
    if (refIsActive.current) {
person imk    schedule 26.04.2019

Я создал модуль NPM, чтобы решить эту проблему https://www.npmjs.com/package/react-usestateref, и кажется, что он может помочь и ответить на ваш вопрос, как запустить текущее состояние.

Это комбинация useState и useRef, позволяющая получить последнее значение, например ref

Пример использования:

    const [isActive, setIsActive,isActiveRef] = useStateRef(false);
    console.log(isActiveRef.current)

Больше информации:

person Aminadav Glickshtein    schedule 28.12.2020