I'm trying to use an event emitter with React useEffect
and useState
, but it always gets the initial state instead of the updated state. It works if I call the event handler directly, even with a setTimeout
.
If I pass the value to the useEffect()
2nd argument, it makes it work, however this causes a resubscription to the event emitter every time the value changes (which is triggered off of keystrokes).
What am I doing wrong? I've tried useState
, useRef
, useReducer
, and useCallback
, and couldn't get any working.
Here's a reproduction:
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;
Here's a code sandbox with the same in App2
:
https://codesandbox.io/s/ww2v80ww4l
App
component has 3 different implementations - EventEmitter, pubsub-js, and setTimeout. Only setTimeout works.
Edit
To clarify my goal, I simply want the value in handleEvent
to match the Codemirror value in all cases. When any button is clicked, the current codemirror value should be displayed. Instead, the initial value is displayed.
useEffect
executes in async way. And handleEvent method mutates something in the DOM directly. So DOM mutation happens before you get the value from the server. If you do it the React way of rendering based on state value, this won't happen. – Eulau