Почему мой обработчик onClick не выходит из последней версии состояния и как я могу устранить эту проблему?

Песочница

import React, { useEffect, useState } from "react";

export default function App() {
  let [button, setButton] = useState(null);
  let [num, setNum] = useState(5);

  function revealState() {
    console.info(num);
  }
  function changeState() {
    setNum(Math.random());
  }

  useEffect(() => {
    const el = (
      <button id = "logStateButton" onClick = {revealState}>
        Log state
      </button>
    );
    setButton(el);
  }, []);

  return (
    <>
      {button}
      <button onClick = {changeState}>Change state</button>
    </>
  );
}

Нажатие на кнопку «Журнал состояния» успешно регистрирует состояние num. Нажатие на кнопку «Изменить состояние» успешно изменяет состояние num. Повторное нажатие кнопки «Журнал состояния» не регистрирует обновленное значение состояния — оно регистрирует старое.

  1. Почему это? Я предполагаю, что, поскольку useEffect запускается только один раз, он ссылается только на первую revealState функцию, которая ссылается только на первую num переменную. Поскольку его нет в операторе return компонента, он не «обновляется».

  2. Какова бы ни была причина проблемы, какие есть обходные пути? Вот некоторые из требований:

  • тег не может отображаться непосредственно в операторе return.
  • у нас должен быть useEffect, который есть, и он должен иметь какой-то массив dep (нежелательно, чтобы он срабатывал каждый раз, когда компонент функции повторно выполняется).
  • В реальном проекте могут быть внесены некоторые важные изменения в рендеринг обратного вызова тегов useEffect, поэтому нецелесообразно повторно запускать useEffect, помещая что-то вроде num в его массив dep.

🤔 А знаете ли вы, что...
JavaScript поддерживает работу с куки и хранилищем веб-браузера для сохранения данных на клиентской стороне.


94
4

Ответы:

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

export default function App() {
  let [button, setButton] = useState(null);
  let [num, setNum] = useState(5);

  const revealState = useCallback(() => {
    console.info(num);
  }, [num])
  function changeState() {
    setNum(Math.random());
  }

  useEffect(() => {
    const el = (
      <button id = "logStateButton" onClick = {revealState}>
        Log state
      </button>
    );
    setButton(el);
  }, [revealState]);

  return (
    <>
      {button}
      <button onClick = {changeState}>Change state</button>
    </>
  );
}

вы можете прослушать состояние раскрытия в useEffect. который инициализируется только при изменении num с помощью useCallback. поэтому всякий раз, когда вы нажимаете кнопку, число изменяется, что инициализирует функцию раскрыть состояние и не инициализируется при других повторных рендерингах.


вы должны добавить num в качестве зависимости к useEffect:

  useEffect(() => {
      const el = (
          <button id = "logStateButton" onClick = {revealState}>
              Log state
          </button>
      );
      setButton(el);
  }, [num]);

После дополнительных разъяснений по вашей проблеме кажется, что вам нужно следить как за числом, так и за состоянием HTML. Решением является объединение кода Алана и Кишора. Поскольку useEffect только наблюдает за числом в ответе Алана, поэтому любые изменения в теге не приведут к его повторному запуску. kishore также упомянул другое исправление, которое заключается в использовании useCallback, но нужно следить за button, а не num. Так:

const updateButton = useCallback(function (newButton) {
    setButton(newButton);
  }, [button])

useEffect(() => {
    const el = (
      <button id = "logStateButton" onClick = {revealState}>
        Log state
      </button>
    );
    updateButton(el)
  }, [num]);

Это скажет useEffect наблюдать num и вернет новый button только при изменении состояния button.


Решено

IMO, самое изящное решение - просто добавить обновленный прослушиватель событий каждый раз, когда страница отображается:

useEffect(() => {
  el.onclick = onClickHandler
});

Слушатель событий всегда имеет доступ к последнему состоянию (и свойствам). IMO, это решение более масштабируемо, чем ранее упомянутые решения - если моему прослушивателю событий необходимо отслеживать последние версии нескольких состояний и реквизитов, это может стать грязным. При этом все, что мне нужно сделать, это добавить дополнительных слушателей в этот обратный вызов useEffect. Мысли?