【问题标题】:Prevent "React state update on unmounted component" warning when setting state on async callback在异步回调上设置状态时防止“对未安装的组件做出反应状态更新”警告
【发布时间】: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


【解决方案1】:

确实,this.isMounted() 已被弃用,使用 _isMounted 引用或实例变量是一种反模式,请注意,当 this.isMounted() 被弃用时,建议使用 _isMounted 实例属性作为临时迁移解决方案,因为最终它和this.isMounted()有同样的问题,导致内存泄漏。

该问题的解决方案是您的组件——无论是基于钩子的组件还是基于类的组件,都应该清理它的异步效果,并确保在卸载组件时,没有任何东西仍然持有对该组件的引用或需要运行在组件的上下文中(基于钩子的组件),这使得垃圾收集器能够在它启动时收集它。

在你的具体情况下,你可以做这样的事情

function ExampleButton() {
  const [submitting, setSubmitting] = useState(false);

  useEffect(() => {
    if (submitting) {
      // using an ad hoc cancelable promise, since ECMAScript still has no native way to cancel promises  
      // see example makeCancelable() definition on https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
      const cancelablePromise = makeCancelable(doStuff())
      // using then since to use await you either have to create an inline function or use an async iife
      cancelablePromise.promise.then(() => setSubmitting(false))
      return () => cancelablePromise.cancel(); // we return the cleanup function
    }

  }, [submitting]);

  const handleClick = () => {
    setSubmitting(true);
  };

  return (
    <button onClick={handleClick} disabled={submitting}>
      {submitting ? "Submitting" : "Submit"}
    </button>
  );
}

现在请注意,无论卸载组件时发生什么情况,都不会再有与其相关的功能可能/将要运行

【讨论】:

  • 这很好,谢谢!我没有想过将所有异步逻辑放在useEffect 挂钩中,并将事件处理程序仅用作useEffect 的“触发器”。对我来说看起来很干净!
  • 另外,我可以建议在答案中包含对临时可取消承诺和自定义usePreviousState 挂钩实现的参考吗?后者可能是指向usehooks.com/usePrevious 的链接,甚至可以复制答案中的实现以使其更加独立:)
  • @epidemian 我添加了 usehooks.com 作为示例,作为我没有添加的可取消承诺的参考,因为参考可能与我给出的使用方法的示例不匹配,我做了不想关注可取消承诺的细节,因为它与答案并没有太大关系。随意编辑我的示例以匹配某个实现,并根据需要添加对该实现的引用
  • 感谢@ehab。我继续调整实现以删除 prvSubmitting 状态,因为它是不需要的,因为 useEffect() 中的代码仅在 submitting 更改为 true 时才执行。我还添加了对 React 博客文章中提到的可取消承诺实现的引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-05
  • 1970-01-01
  • 1970-01-01
  • 2021-11-15
  • 2021-02-24
相关资源
最近更新 更多