【发布时间】:2021-06-23 06:02:44
【问题描述】:
我想从组件上的事件处理程序触发异步操作,并在该操作完成后更新该组件中的某些 UI 状态。但是由于用户导航到另一个页面,该组件可能随时从 DOM 中删除。如果在操作尚未完成时发生这种情况,React 会记录以下警告:
警告:无法对未安装的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
这是一个可重现的例子:
import { useState } from "react";
import ReactDOM from "react-dom";
// The router lib is a detail; just to simulate navigating away.
import { Link, Route, BrowserRouter } from "react-router-dom";
function ExampleButton() {
const [submitting, setSubmitting] = useState(false);
const handleClick = async () => {
setSubmitting(true);
await doStuff();
setSubmitting(false);
};
return (
<button onClick={handleClick} disabled={submitting}>
{submitting ? "Submitting" : "Submit"}
</button>
);
}
function doStuff() {
// Suppose this is a network request or some other async operation.
return new Promise((resolve) => setTimeout(resolve, 2000));
}
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link> | <Link to="/other">Other</Link>
</nav>
<Route path="/" exact>
Click the button and go to "Other" page
<br />
<ExampleButton />
</Route>
<Route path="/other">Nothing interesting here</Route>
</BrowserRouter>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
您可以查看并运行示例here。如果您在 2 秒后单击“提交”按钮,然后单击“其他”链接,您应该会在控制台上看到警告。
是否有惯用的方式或模式来处理异步操作后需要状态更新的这些场景?
我试过的
我第一次尝试修复此警告是使用可变 ref 和 useEffect() 挂钩来跟踪组件是否已卸载:
function ExampleButton() {
const [submitting, setSubmitting] = useState(false);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const handleClick = async () => {
setSubmitting(true);
await doStuff();
if (isMounted.current) setSubmitting(false);
};
return (
<button onClick={handleClick} disabled={submitting}>
{submitting ? "Submitting" : "Submit"}
</button>
);
}
注意在doStuff() 调用之后对setSubmitting() 的条件调用。
这个解决方案works,但我不太满意,因为:
- 这很像样板。所有手动
isMounted跟踪似乎都是低级细节,与该组件试图做什么无关,我不想在其他需要类似异步操作的地方重复。 - 即使样板被隐藏到自定义
useIsMounted()钩子中,似乎isMounted is an antipattern。是的,这篇文章讨论的是Component.prototype.isMounted方法,它不存在于像我在这里使用的那样的功能组件上,但我基本上用isMounted参考模拟相同的功能。
更新:我还看到了在 useEffect 函数中使用 a didCancel boolean variable 的模式,并使用它在异步函数之后有条件地执行操作(因为卸载或更新的依赖项)。我可以看到在异步操作仅限于useEffect() 并由组件安装/更新触发的情况下,这种方法或使用可取消的承诺如何工作得很好。但是我看不到在事件处理程序上触发异步操作的情况下它们将如何工作。 useEffect 清理函数应该能够看到 didCancel 变量或可取消的承诺,因此需要将它们提升到组件范围,使它们与上述 useRef 方法几乎相同。
所以我有点不知道在这里做什么。任何帮助将不胜感激! :D
【问题讨论】:
-
您链接的文档建议将可取消的承诺作为解决方案
-
@TJ 感谢您的链接!关于可取消的承诺,是的,我忘了在我的问题中提及它们,但基本上,我可以看到在使用 useEffect+cleanup 在组件安装/更新上加载数据时如何使用它们,但我不知道它们会如何在这样的示例中使用,其中异步操作在事件侦听器上触发。 useEffect 清理函数如何知道要取消哪个承诺?
-
将变量提升到组件范围可能与使用
useRef不同。 document 声明useRef()与自己创建{current: ...}对象之间的区别在于useRef将在每次渲染时为您提供相同的 ref 对象。
标签: javascript reactjs react-hooks