useEffect всегда устанавливает мой addEventListener - PullRequest
1 голос
/ 05 августа 2020

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

Проблема, с которой я столкнулся, или что я не делаю ' Я понимаю, если и почему всегда нужно вызывать useEffect для повторной установки eventListener?

Мне нужно создать прослушиватель событий keydown только один раз, но каждое изменение состояния воссоздает прослушиватель событий, иначе это просто не сработает:

function App() {
  const [pictureNumber, setPictureNumber] = useState(1);
  
  const setCurrentSlideData = e => setPictureNumber(e)

  const handleUserKeyPress = useCallback((e) => {
    if (e.code === "ArrowLeft") {
      setCurrentSlideData(pictureNumber - 1);
    }
    if (e.code === "ArrowRight") {
      setCurrentSlideData(pictureNumber + 1);
    }
  }, [pictureNumber]);

  useEffect(() => {
    console.log("use effect called")
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [handleUserKeyPress]);

  return (
    <div>
      <h1>Hello, world {pictureNumber}</h1>
    </div>
  );
}

См. перо: https://codepen.io/moshem/pen/NWNWzod

use effect called регистрируется при каждом нажатии клавиши, даже если handleUserKeyPress не должно изменяться.

Если я удалю зависимость pictureNumber от useCallback, счетчик изменится только один раз для каждой клавиши.

Правильно ли я делаю?

Ответы [ 2 ]

2 голосов
/ 05 августа 2020

Простое решение с использованием стиля обратного вызова установщика useState

Как указал @devserkan. Мы можем использовать предыдущее состояние, передав обратный вызов в setState. Благодаря этому трюку мы избегаем зависимости от состояния.

const [pictureNumber, setPictureNumber] = useState(1);

  useEffect(() => {
    console.log("use effect called");
    
    const handleUserKeyPress = (e) => {
      if (e.code === "ArrowLeft") {
        setPictureNumber(prev => prev - 1);
      }
      if (e.code === "ArrowRight") {
        setPictureNumber(prev => prev + 1);
      }
    };

    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [setPictureNumber]);

Вот официальная документация: https://reactjs.org/docs/hooks-reference.html#functional -updates

Вот ручка, показывающая это решение: https://codepen.io/abumalick/pen/BaKaPgr

Решение с использованием ссылки

Это решение немного сложнее и будет полезно, когда вам нужно изменить функцию обработчика. Мы используем ссылку, useEffect сохраняет ссылку на ссылку, которая не изменяется, и мы можем изменить свойство current, которое изменяется.

useEffect будет вызываться только один раз.

  const [pictureNumber, setPictureNumber] = useState(1);

  const callbackRef = useRef(null);

  callbackRef.current = useCallback(
    (e) => {
      if (e.code === "ArrowLeft") {
        setPictureNumber(pictureNumber - 1);
      }
      if (e.code === "ArrowRight") {
        setPictureNumber(pictureNumber + 1);
      }
    },
    [pictureNumber, setPictureNumber]
  );

  useEffect(() => {
    const handleUserKeyPress = (e) => {
      callbackRef.current(e);
    };
    console.log("use effect called");
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [callbackRef]);

Вот кодовая ссылка: https://codepen.io/abumalick/pen/jOqOpLz

2 голосов
/ 05 августа 2020

Вам не нужно добавлять pictureNumber в массив зависимостей для useCallback, вместо этого вы можете использовать функциональное обновление для установки своего состояния.

const handleUserKeyPress = useCallback((e) => {
  const setCurrentSlideData = e => setPictureNumber(e);
  
  if (e.code === "ArrowLeft") {
    setCurrentSlideData(prev => prev - 1);
  }
  if (e.code === "ArrowRight") {
    setCurrentSlideData(prev => prev + 1);
  }
}, []);

Итак, pictureNumber изменяется не влияют на воссоздание этой функции.

В дополнение к этому, на самом деле вам не нужно определять handleUserKeyPress вне вашего эффекта, поскольку вы не используете его где-либо еще. Если вы переместите его в свой эффект, вам больше не придется использовать useCallback.

Кроме того, вам не нужна отдельная функция для установки pictureNumber, вы можете использовать функцию установки напрямую. Вот рабочий:

function App() {
  const [pictureNumber, setPictureNumber] = React.useState(1);

  React.useEffect(() => {
    console.log("use effect called");
    
    function handleUserKeyPress(e) {
      if (e.code === "ArrowLeft") {
        setPictureNumber(prev => prev - 1);
      }
      if (e.code === "ArrowRight") {
        setPictureNumber(prev => prev + 1);
      }
    }

    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Hello, world {pictureNumber}</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root" />
...