React Hooks useState + useEffect + событие дает устаревшее состояние - PullRequest
4 голосов
/ 14 марта 2019

Я пытаюсь использовать источник событий с React useEffect и useState, но он всегда получает начальное состояние вместо обновленного состояния.Это работает, если я вызываю обработчик событий напрямую, даже с setTimeout.

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

Что я делаю не так?Я пробовал useState, useRef, useReducer и useCallback, и не мог получить какую-либо работу.

Вот репродукция:

import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";

let ee = new EventEmitter();

const initialValue = "initial value";

function App(props) {
  const [value, setValue] = useState(initialValue);

  // Should get the latest value, both after the initial server load, and whenever the Codemirror input changes.
  const handleEvent = (msg, data) => {
    console.info("Value in event handler: ", value);
    // This line is only for demoing the problem. If we wanted to modify the DOM in this event, we would instead call some setState function and rerender in a React-friendly fashion.
    document.getElementById("result").innerHTML = value;
  };

  // Get value from server on component creation (mocked)
  useEffect(() => {
    setTimeout(() => {
      setValue("value from server");
    }, 1000);
  }, []);

  // Subscribe to events on component creation
  useEffect(() => {
    ee.on("some_event", handleEvent);
    return () => {
      ee.off(handleEvent);
    };
  }, []);

  return (
    <React.Fragment>
      <CodeMirror
        value={value}
        options={{ lineNumbers: true }}
        onBeforeChange={(editor, data, newValue) => {
          setValue(newValue);
        }}
      />
      {/* Everything below is only for demoing the problem. In reality the event would come from some other source external to this component. */}
      <button
        onClick={() => {
          ee.emit("some_event");
        }}
      >
        EventEmitter (doesnt work)
      </button>
      <div id="result" />
    </React.Fragment>
  );
}

export default App;

Вот кодовая песочница с таким же в App2:

https://codesandbox.io/s/ww2v80ww4l

App компонент имеет 3 различных реализации- EventEmitter, pubsub-js и setTimeout.Работает только setTimeout.

Спасибо!

РЕДАКТИРОВАТЬ: Чтобы уточнить мою цель, я просто хочу, чтобы значение в handleEvent соответствовало значению Codemirror во всех случаях.При нажатии любой кнопки должно отображаться текущее значение кода.Вместо этого отображается начальное значение.

Ответы [ 2 ]

7 голосов
/ 14 марта 2019

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

Решение 1. Создайте второй аргумент для эффекта публикации [value]. Это заставляет обработчик событий получить правильное значение, но также заставляет эффект запускаться снова при каждом нажатии клавиши.

Решение 2. Используйте ref для сохранения самого последнего value в переменной экземпляра компонента. Затем создайте эффект, который только обновляет эту переменную каждый раз, когда изменяется состояние value. В обработчике событий используйте ref, а не value.

const [value, setValue] = useState(initialValue);
let refValue = useRef(value);
useEffect(() => {
    refValue.current = value;
});
const handleEvent = (msg, data) => {
    console.info("Value in event handler: ", refValue.current);
};

https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often

Похоже, на этой странице есть и другие решения, которые тоже могут работать. Большое спасибо @Dinesh за помощь.

1 голос
/ 14 марта 2019

Обновленный ответ.

Проблема не с крючками. Начальное значение состояния было закрыто и передано в EventEmitter и использовалось снова и снова.

Не рекомендуется использовать значения состояний непосредственно в handleEvent. Вместо этого нам нужно передать их в качестве параметров при отправке события.

import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";

let ee = new EventEmitter();

const initialValue = "initial value";

function App(props) {
  const [value, setValue] = useState(initialValue);
  const [isReady, setReady] = useState(false);

  // Should get the latest value
  function handleEvent(value, msg, data) {
    // Do not use state values in this handler
    // the params are closed and are executed in the context of EventEmitter
    // pass values as parameters instead
    console.info("Value in event handler: ", value);
    document.getElementById("result").innerHTML = value;
  }

  // Get value from server on component creation (mocked)
  useEffect(() => {
    setTimeout(() => {
      setValue("value from server");
      setReady(true);
    }, 1000);
  }, []);

  // Subscribe to events on component creation
  useEffect(
    () => {
      if (isReady) {
        ee.on("some_event", handleEvent);
      }
      return () => {
        if (!ee.off) return;
        ee.off(handleEvent);
      };
    },
    [isReady]
  );

  function handleClick(e) {
    ee.emit("some_event", value);
  }

  return (
    <React.Fragment>
      <CodeMirror
        value={value}
        options={{ lineNumbers: true }}
        onBeforeChange={(editor, data, newValue) => {
          setValue(newValue);
        }}
      />
      <button onClick={handleClick}>EventEmitter (works now)</button>
      <div id="result" />
    </React.Fragment>
  );
}

export default App;

Вот рабочая коды и коробка

...