React Hooki useState+useEffect + event daje stan nieświeży

Próbuję użyć emitera zdarzeń z Reactem useEffect i useState, ale zawsze dostaje stan początkowy zamiast stanu zaktualizowanego. Działa, jeśli wywołam obsługę zdarzenia bezpośrednio, nawet za pomocą setTimeout.

Jeśli przekażę wartość do drugiego argumentu useEffect(), spowoduje to, że będzie działać, jednak spowoduje to ponowne zapisanie do emitera zdarzeń za każdym razem, gdy zmieni się wartość (co jest wyzwalane po naciśnięciu klawisza).

Co robię źle? Próbowałem useState, useRef, useReducer, i useCallback, i nie mógł pracować.

Oto reprodukcja:

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;

Oto piaskownica kodu z tym samym w App2:

Https://codesandbox.io/s/ww2v80ww4l

App komponent ma 3 różne implementacje-EventEmitter, pubsub-js i setTimeout. Działa tylko setTimeout.

Edit

Aby wyjaśnić mój cel, chcę po prostu, aby wartość w handleEvent była zgodna z wartością Codemirror we wszystkich przypadkach. Gdy jakiekolwiek kliknięcie przycisku spowoduje wyświetlenie aktualnej wartości codemirror. Zamiast tego wyświetlana jest wartość początkowa.

Author: halfer, 2019-03-14

3 answers

value jest przestarzały w obsłudze zdarzenia, ponieważ pobiera swoją wartość z zamknięcia, w którym została zdefiniowana. Jeśli nie zapiszemy ponownie nowego programu obsługi zdarzeń za każdym razem, gdy value się zmieni, nie otrzyma on nowej wartości.

Rozwiązanie 1: dodać drugi argument do efektu publish [value]. Powoduje to, że obsługa zdarzeń otrzymuje poprawną wartość, ale także powoduje, że efekt będzie uruchamiany ponownie przy każdym naciśnięciu klawisza.

Rozwiązanie 2: Użyj ref, Aby zapisać najnowszą value W Zmiennej instancji komponentu. Następnie wykonaj efekt, który nie robi nic poza aktualizacją tej zmiennej za każdym razem, gdy zmieni się stan value. W programie obsługi zdarzenia użyj ref, a nie value.

const [value, setValue] = useState(initialValue);
const 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

Wygląda na to, że są inne rozwiązania na tej stronie, które mogą również działać. Wielkie podziękowania dla @ Dinesh za pomoc.

 54
Author: Tony R,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-01-20 04:21:30

Zaktualizowana Odpowiedź.

Problem nie dotyczy hooków. Wartość stanu początkowego została zamknięta i przekazana do EventEmitter i była używana wielokrotnie.

Nie jest dobrym pomysłem używanie wartości stanu bezpośrednio w handleEvent. Zamiast tego musimy przekazać je jako parametry podczas emitowania zdarzenia.

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;

Oto działa codesandbox

 3
Author: Dinesh Pandiyan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-03-14 05:23:17

UseCallback powinien działać tutaj.

import React, { useState, useEffect, useCallback } from "react";
import PubSub from "pubsub-js";
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
  const handler = (msg, data) => {
    console.info("Value in event handler: ", value);
    document.getElementById("result").innerHTML = value;
  };

  const handleEvent = useCallback(handler, [value]);

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

  // Subscribe to events on component creation
  useEffect(() => {
    PubSub.subscribe("some_event", handleEvent);
    return () => {
      PubSub.unsubscribe(handleEvent);
    };
  }, [handleEvent]);
  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);
        }}
      />
      <button
        onClick={() => {
          ee.emit("some_event");
        }}
      >
        EventEmitter (works)
      </button>
      <button
        onClick={() => {
          PubSub.publish("some_event");
        }}
      >
        PubSub (doesnt work)
      </button>
      <button
        onClick={() => {
          setTimeout(() => handleEvent(), 100);
        }}
      >
        setTimeout (works!)
      </button>
      <div id="result" />
    </React.Fragment>
  );
}

export default App;

Sprawdź codesandbox tutaj https://codesandbox.io/s/react-base-forked-i9ro7

 0
Author: Lokii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-11-18 05:11:07