React Hooks useState+useEffect+event 给出陈旧状态
React Hooks useState+useEffect+event gives stale state
我正在尝试将事件发射器与 React useEffect
和 useState
一起使用,但它总是获取初始状态而不是更新后的状态。如果我直接调用事件处理程序,它会起作用,即使使用 setTimeout
。
如果我将值传递给 useEffect()
第二个参数,它会使其工作,但是这会导致每次值更改时重新订阅事件发射器(由击键触发)。
我做错了什么?我试过 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 值匹配。单击任何按钮时,应显示当前代码镜像值。而是显示初始值。
更新答案。
问题不在于挂钩。初始状态值已关闭并传递给 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
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 的帮助。
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;
检查这里的codesandbox https://codesandbox.io/s/react-base-forked-i9ro7
我正在尝试将事件发射器与 React useEffect
和 useState
一起使用,但它总是获取初始状态而不是更新后的状态。如果我直接调用事件处理程序,它会起作用,即使使用 setTimeout
。
如果我将值传递给 useEffect()
第二个参数,它会使其工作,但是这会导致每次值更改时重新订阅事件发射器(由击键触发)。
我做错了什么?我试过 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 值匹配。单击任何按钮时,应显示当前代码镜像值。而是显示初始值。
更新答案。
问题不在于挂钩。初始状态值已关闭并传递给 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
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 的帮助。
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;
检查这里的codesandbox https://codesandbox.io/s/react-base-forked-i9ro7