Используя React Hooks, почему мои обработчики событий работают с неправильным состоянием? - PullRequest
1 голос
/ 26 апреля 2019

Я пытаюсь создать копию этого вращающегося 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.

Спасибо

Ответы [ 2 ]

1 голос
/ 29 апреля 2019

Проблема

Почему 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.

0 голосов
/ 27 апреля 2019

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

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

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

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

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

 const mouseMoveHandler = (e) => {    
  console.log('isActive',refIsActive.current);
    if (refIsActive.current) {
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...