在异步回调上设置状态时,防止“对卸载的组件做出反应状态更新”警告

时间:2021-06-23 06:02:44

标签: javascript reactjs react-hooks

我想从组件上的事件处理程序触发异步操作,并在该操作完成后,更新该组件中的某些 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 秒之前单击提交按钮,然后单击“其他”链接,您应该会在控制台上看到警告。

是否有一种惯用的方式或模式来处理异步操作后需要状态更新的这些场景?

我尝试了什么

我修复此警告的第一次尝试是使用可变引用和 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>
  );
}

注意 setSubmitting() 调用后对 doStuff() 的条件调用。

这个解决方案works,但我对它不太满意,因为:

  • 这很简单。所有手动 isMounted 跟踪似乎都是低级细节,与此组件尝试执行的操作无关,而且我不想在其他需要类似异步操作的地方重复。
  • 即使样板文件隐藏在自定义 useIsMounted() 钩子中,看起来也是 isMounted is an antipattern。是的,这篇文章讨论的是 Component.prototype.isMounted 方法,它不存在于我在这里使用的函数组件中,但我基本上是用 isMounted 引用模拟相同的函数。< /li>

更新:我还看到了在 useEffect 函数中包含 a didCancel boolean variable 的模式,并使用它在异步函数之后有条件地执行或不执行操作(因为卸载或更新的依赖项)。我可以看到这种方法或使用可取消的承诺如何在异步操作仅限于 useEffect() 并由组件安装/更新触发的情况下工作得很好。但是我看不到在事件处理程序上触发异步操作的情况下它们将如何工作。 useEffect 清理函数应该能够看到 didCancel 变量或可取消承诺,因此它们需要提升到组件范围,使它们几乎与上面提到的 useRef 方法相同.

所以我有点不知道在这里做什么。任何帮助将不胜感激! :D

1 个答案:

答案 0 :(得分:2)

确实不推荐使用 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>
  );
}

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