React Hooks useState+useEffect+event 给出过时的状态

Posted

技术标签:

【中文标题】React Hooks useState+useEffect+event 给出过时的状态【英文标题】:React Hooks useState+useEffect+event gives stale state 【发布时间】:2019-08-04 20:32:36 【问题描述】:

我正在尝试将事件发射器与 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 值匹配。单击任何按钮时,应显示当前的 codemirror 值。而是显示初始值。

【问题讨论】:

它看起来与钩子无关。 useEffect 以异步方式执行。并且 handleEvent 方法直接改变 DOM 中的某些内容。因此,DOM 突变发生在您从服务器获取值之前。如果你用 React 的基于 state 值的渲染方式,就不会发生这种情况。 @Dinesh 在我的真实代码中,我没有在事件处理程序中修改 DOM。这只是为了演示问题。 【参考方案1】:

value 在事件处理程序中是陈旧的,因为它从定义它的闭包中获取它的值。除非我们每次value 更改时都重新订阅一个新的事件处理程序,否则它不会获得新的值。

解决方案 1:将第二个参数设置为发布效果 [value]。这使得事件处理程序获得正确的值,但也使效果在每次击键时再次运行。

解决方案 2:使用 ref 将最新的 value 存储在组件实例变量中。然后,制作一个除了在每次value 状态更改时更新此变量之外什么都不做的效果。在事件处理程序中,使用ref,而不是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

看起来该页面上还有一些其他解决方案也可能有效。非常感谢@Dinesh 的帮助。

【讨论】:

刚刚注意到这个用例在 React Hooks RFC 的缺点部分中有明确描述:github.com/reactjs/rfcs/blob/master/text/… 其实这里的useState应该有[]的第二个参数,否则在不需要的时候每次渲染都会调用。【参考方案2】:

更新答案。

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

【讨论】:

感谢您的帮助。我将您的代码放在一个新的沙箱中:codesandbox.io/s/oo159k4w5y (App3)。它可以加载服​​务器值,但目标是让 Codemirror 也发生变化,因此如果您更改 codemirror 输入,您仍然会获得原始服务器值。我通过将事件处理程序置于以下状态使其以另一种方式工作:codesandbox.io/s/6jq3r6vnjw (App)。想法? @TonyR 我不确定我是否正确理解所需的功能。 - 你所说的“作品”是什么意思? - 你所说的“不工作”是什么意思? 单击按钮时,事件处理程序中的值应与代码镜像的当前值相同。 @TonyR 我发现了这个问题。它与钩子或 React 无关。 handleEvent 方法以初始状态值关闭并传递给 EventEmitter 并且一直使用相同的值。关闭。我已经更新了解决方案。 我认为关键是,无论您从哪里触发事件,发射器都应该包含有效负载,而不是依赖外部来源的数据。也许,像 redux 这样的数据存储会有所帮助,因为它允许您在 React 组件之外获取存储数据。但正确的方法是设计一个数据不依赖于外部来源并作为有效负载传递的实现。【参考方案3】:

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

【讨论】:

以上是关于React Hooks useState+useEffect+event 给出过时的状态的主要内容,如果未能解决你的问题,请参考以下文章

React hooks的useState原理

React Hooks - 使用 useState 与仅使用变量

React Hooks --- useState 和 useEffect

react hooks useState 赋值优化解决方案

Firebase 和 React Hooks(useState 和 useEffect)

react源码debugger-各个hooks的逻辑实现(useState和useEffect)