React Hooks useState + useEffect + event提供陈旧状态

时间:2019-03-14 02:44:59

标签: javascript reactjs codemirror eventemitter react-hooks

我正在尝试将事件发射器与React useEffectuseState一起使用,但是它总是获取初始状态而不是更新状态。如果我直接调用事件处理程序,即使使用setTimeout,也可以使用。

如果我将值传递给useEffect()第二个参数,它将使它起作用,但是,每次值更改时(这是击键触发的),这都会导致重新订阅事件发射器。

我在做什么错?我已经尝试过useStateuseRefuseReduceruseCallback,但无法正常工作。

这是复制品:

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值匹配。单击任何按钮时,应显示当前的代码镜像值。而是显示初始值。

3 个答案:

答案 0 :(得分:4)

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 :(得分:1)

更新的答案。

问题不在于钩子。初始状态值被关闭并传递给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;

这是工作中的codesandbox

答案 2 :(得分:0)

useCallback应该在这里起作用。

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;

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

处检查代码和框