javascript - Prevent "React state update on unmounted component" warning when setting state on async callback

I want to trigger an asynchronous operation from an event handler on a ponent and after that operation

I want to trigger an asynchronous operation from an event handler on a ponent and after that operation pletes, update some UI state in that ponent. But the ponent may be removed from the DOM at any time, due to user navigating to another page. If that happens while the operation hasn't pleted yet, React logs this warning:

Warning: Can't perform a React state update on an unmounted ponent. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Here's a reproducible example:

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"));

You can see and run the example here. If you click the Submit button and then the "Other" link before 2 seconds pass, you should see the warning on the console.

Is there an idiomatic way or pattern for dealing with these scenarios where a state update is needed after an async operation?

What i've tried

My first attempt to fix this warning was to track whether the ponent has been unmounted or not using a mutable ref and a useEffect() hook:

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>
  );
}

Notice the conditional call to setSubmitting() after the doStuff() call.

This solution works, but i'm not too satisfied with it because:

  • It's quite bolerplate-ish. All the manual isMounted tracking seems like a low-level detail, unrelated to what this ponent is trying to do, and not something i'd want to repeat on other places that need a similar async operation.
  • Even if the boilerplate was hidden into a custom useIsMounted() hook, is seems that isMounted is an antipattern. Yes, the article is talking about the Component.prototype.isMounted method, which is not present on function ponents like the one i'm using here, but i'm basically emulating the same function with the isMounted ref.

Update: i've also seen the pattern of having a didCancel boolean variable inside the useEffect function, and using that to conditionally do stuff after the async function or not (because of an unmount or updated dependencies). I can see how this approach, or using a cancellable promise, would work nice in cases where the async operation is confined to a useEffect() and is triggered by ponent mount/update. But i cannot see how they would work in cases when the async operation is triggered on an event handler. The useEffect cleanup function should be able to see the didCancel variable, or the cancellable promise, so they would need to be lifted up to the ponent scope, making them virtually the same as the useRef approach mentioned above.

So i'm kind of lost on what to do here. Any help will be appreciated! :D

I want to trigger an asynchronous operation from an event handler on a ponent and after that operation pletes, update some UI state in that ponent. But the ponent may be removed from the DOM at any time, due to user navigating to another page. If that happens while the operation hasn't pleted yet, React logs this warning:

Warning: Can't perform a React state update on an unmounted ponent. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Here's a reproducible example:

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"));

You can see and run the example here. If you click the Submit button and then the "Other" link before 2 seconds pass, you should see the warning on the console.

Is there an idiomatic way or pattern for dealing with these scenarios where a state update is needed after an async operation?

What i've tried

My first attempt to fix this warning was to track whether the ponent has been unmounted or not using a mutable ref and a useEffect() hook:

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>
  );
}

Notice the conditional call to setSubmitting() after the doStuff() call.

This solution works, but i'm not too satisfied with it because:

  • It's quite bolerplate-ish. All the manual isMounted tracking seems like a low-level detail, unrelated to what this ponent is trying to do, and not something i'd want to repeat on other places that need a similar async operation.
  • Even if the boilerplate was hidden into a custom useIsMounted() hook, is seems that isMounted is an antipattern. Yes, the article is talking about the Component.prototype.isMounted method, which is not present on function ponents like the one i'm using here, but i'm basically emulating the same function with the isMounted ref.

Update: i've also seen the pattern of having a didCancel boolean variable inside the useEffect function, and using that to conditionally do stuff after the async function or not (because of an unmount or updated dependencies). I can see how this approach, or using a cancellable promise, would work nice in cases where the async operation is confined to a useEffect() and is triggered by ponent mount/update. But i cannot see how they would work in cases when the async operation is triggered on an event handler. The useEffect cleanup function should be able to see the didCancel variable, or the cancellable promise, so they would need to be lifted up to the ponent scope, making them virtually the same as the useRef approach mentioned above.

So i'm kind of lost on what to do here. Any help will be appreciated! :D

Share Improve this question edited Jun 23, 2021 at 7:00 epidemian asked Jun 23, 2021 at 6:02 epidemianepidemian 19.2k3 gold badges64 silver badges73 bronze badges 4
  • 3 stackoverflow./q/54327076/2333214 – T J Commented Jun 23, 2021 at 6:08
  • The documentation you linked suggests cancellable promises as a solution – T J Commented Jun 23, 2021 at 6:15
  • @TJ thanks for the link! and regarding cancellable promises, yes, i forgot to mention them in my question, but basically, i can see how they could be used when loading data on ponent mount/update with useEffect+cleanup, but i don't see how they could be used on an example like this where the async op is triggered on an event listener. how would the useEffect cleanup function know which promise to cancel? – epidemian Commented Jun 23, 2021 at 6:30
  • Lifting up variables to the ponent scope may not be the same as using useRef. The document stated that The difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render. – Quicksilver Commented Jun 23, 2021 at 9:12
Add a ment  | 

2 Answers 2

Reset to default 4

Indeed this.isMounted() is deprecated, and the usage of a _isMounted ref or instance variable is an anti pattern, notice that the usage of a _isMounted instance property was suggested as a temporary migration solution when this.isMounted() was deprecated because eventually it has the same problem of this.isMounted() which is leading to memory leaks.

The solution to that problem is that your ponent -whether a hook based or class based ponent, should clean it's async effects, and make sure that when the ponent is unmounted, nothing is still holding reference to the ponent or needs to run in the context of the ponent (hook based ponents), which makes the garbage collector able to collect it when it kicks in.

In your specific case you could do something like this

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/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>
  );
}

Notice now that no matter what happens when the ponent is unmounted there is no more functionality related to it that might/will run

The pattern to use to to set only if you're still mounted or not. You know if the ponent is still mounted as long as useEffect cleanup function was never called for the ponent.

export type IsMountedFunction = () => boolean;
export function useMounted(): IsMountedFunction {
  const mountedRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    return function useMountedEffectCleanup() {
      mountedRef.current = false;
    };
  }, []);
  return useCallback(() => mountedRef.current, [mountedRef]);
}

Given the above you the following hook that would handle the async then set state effect.

export function useAsyncSetEffect<T>(
  asyncFunction: () => Promise<T>,
  onSuccess: (asyncResult: T) => void,
  deps: DependencyList = []
): void {
  const isMounted = useMounted();
  useEffect((): ReturnType<EffectCallback> => {
    (async function wrapped() {
      const asyncResult = await asyncFunction();
      if (isMounted()) {
        onSuccess(asyncResult);
      }
    })();
  }, [asyncFunction, isMounted, onSuccess, ...deps]);
}

Sources and test are in

https://github./trajano/react-hooks/blob/master/src/useAsyncSetEffect/useAsyncSetEffect.ts

Note this does not get processed for correctness using ESLint react-hooks/exhaustive-deps

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745651214a4638276.html

相关推荐

  • 开发体育直播系统后台权限设计实践分享|ThinkPHP 技术栈落地案例

    今天我们分享的是一套由 东莞梦幻网络科技 自研的体育直播源码,在 ThinkPHP + MySQL 技术栈的加持下,后台权限系统如何从0到1落地,并支撑整个平台稳定运行。一、整体架构设计代码语言:html复制用户端(APPH5P

    1小时前
    20
  • 精品网络时代:联通AS9929与10099的强强联合

    中国联通的网络架构犹如一座精心设计的立交桥系统,由AS4837、AS9929和AS10099三张骨干网共同构建。这三张网络各司其职又相互配合,形成了联通独具特色的网络服务体系。联通AS4837、AS9929和AS10099线路介绍一、线路组

    1小时前
    20
  • Kibana Alerting: 突破扩展性限制,实现50倍规模提升

    在过去的几年里,Kibana告警一直是许多大型组织的首选监控解决方案。随着使用的不断增加,用户创建的告警规则数量也在增长。越来越多的组织依赖Kibana进行大规模告警,我们看到了提高效率和确保未来工作负载性能的机会。在Kibana 8.16

    1小时前
    20
  • 怎么用html写出哆啦A梦?

    用HTML和CSS来画哆啦A梦(Doraemon)是一项有趣且具有挑战性的任务。虽然HTML和CSS主要用于网页布局和样式,但通过巧妙的组合和定位,可以创建出相对简单的图形和图案。下面是一个简单的示例,展示了如何用HTML和CSS绘制哆啦A

    1小时前
    00
  • MySQL 8.4 配置复制

    参考文档:.4enreplication-configuration.html1.先在源数据库主机的myf添加这几项代码语言:javascript代码运行次数:0运行复制[mysqld]server-id = 2binlog_forma

    1小时前
    00
  • MySQL8.4 Enterprise安装Firewall及测试

    参考:.4enfirewall.html1.首先执行安装SQL,路径在baseshare目录下代码语言:javascript代码运行次数:0运行复制cd u01mysql3308baseshare[root@mysql8_3

    1小时前
    00
  • 取消Win10开机系统选择倒计时,让电脑秒进系统

    取消Win10开机系统选择倒计时,让电脑秒进系统 近期,不少Win10用户反映在开机时会遇到一个选择系统的倒计时画面,这在一定程度上延缓了开机进程。对于追求高效启动体验的用户来说,这无疑是一个不必要的步骤。那么,如何取消这个倒计时,让电脑

    59分钟前
    00
  • PyMC+AI提示词贝叶斯项目反应IRT理论Rasch分析篮球比赛官方数据:球员能力与位置层级结构研究

    全文链接:tecdat?p=41666在体育数据分析领域不断发展的当下,数据科学家们致力于挖掘数据背后的深层价值,为各行业提供更具洞察力的决策依据。近期,我们团队完成了一项极具意义的咨询项目,旨在通过先进的数据分析手段,深入探究篮球比赛中

    57分钟前
    00
  • 聊聊Spring AI Alibaba的ObsidianDocumentReader

    序本文主要研究一下Spring AI Alibaba的ObsidianDocumentReaderObsidianDocumentReadercommunitydocument-readersspring-ai-alibaba-star

    55分钟前
    00
  • 10个 DeepSeek 神级提示词,建议收藏!

    在当下人工智能飞速发展的时代,DeepSeek 作为一款功能强大的 AI 工具,能够帮助我们实现各种创意和需求。然而,要充分发挥它的潜力,掌握一些巧妙的提示词至关重要。今天,就为大家精心整理了 15 个 DeepSeek 神级提示词,涵盖多

    49分钟前
    00
  • 初始JESD204B高速接口协议(JESD204B一)

    01、对比LVDS与JESD204JESD204B是逻辑器件和高速ADCDAC通信的一个串行接口协议,在此之前,ADCDAC与逻辑器件交互的接口大致分为如下几种。低速串行接口(I2C、SPI)、低速并行接口(包含时钟信号和并行数据信号,

    47分钟前
    00
  • HLS最全知识库

    HLS最全知识库副标题-FPGA高层次综合HLS(二)-Vitis HLS知识库高层次综合(High-level Synthesis)简称HLS,指的是将高层次语言描述的逻辑结构,自动转换成低抽象级语言描述的电路模型的过程。对于AMD Xi

    45分钟前
    00
  • Prometheus配置docker采集器

    Prometheus 配置 Docker 采集器Prometheus 是一个开源的监控系统和时间序列数据库,广泛用于容器化环境中。通过监控 Docker 容器,用户可以实时获取服务性能、资源使用情况等信息。本文将介绍如何为 Docker 容

    26分钟前
    00
  • Power BI 无公式实现帕累托图表

    帕累托分析(Pareto Analysis),也被称为8020法则、关键少数法则,是一种常用的管理工具,用于识别和处理影响业务的主要因素。看到李伟坚老师在Excel使用Vega实现了花式帕累托(参考:Excel 零公式实现高级帕累托图表)

    21分钟前
    00
  • MongoDB “升级项目” 大型连续剧(2)

    上期写的是非必要不升级,想到这个事情,有一些事情的仔细琢磨琢磨,为什么数据库升级的事情在很多公司都是一个困扰,从一个技术人的观点,升级是一件好事,功能提升了,性能提升了,开发效率和一些数据库使用的痛点也被解决了,为什么就不愿意升级呢?如果只

    16分钟前
    00
  • CUT&amp;amp;Tag 数据处理和分析教程(7)

    过滤某些项目可能需要对比对质量分数进行更严格的过滤。本文细讨论了bowtie如何分配质量分数,并举例说明。MAPQ(x) = -10 * log10log10(P(x is mapped wrongly)) = -10 * log10(p)

    14分钟前
    10
  • 深度学习在DOM解析中的应用:自动识别页面关键内容区块

    爬虫代理摘要本文介绍了如何在爬取东方财富吧()财经新闻时,利用深度学习模型对 DOM 树中的内容区块进行自动识别和过滤,并将新闻标题、时间、正文等关键信息分类存储。文章聚焦爬虫整体性能瓶颈,通过指标对比、优化策略、压测数据及改进结果,展示了

    13分钟前
    10
  • 推荐一个轻量级的监控平台并且支持移动端

    简介XUGOU 是基于Cloudflare构建的轻量化监控平台,专精于系统资源监控与可视化状态页面服务。该平台提供英文简体中文双语支持,满足全球化部署需求。面向开发者及中小团队,项目致力于提供高可用性的监控解决方案。核心功能与实现平台功能

    8分钟前
    00
  • 【Docker项目实战】使用Docker部署IT工具箱Team·IDE

    一、Team·IDE介绍1.1 Team·IDE简介Team IDE 是一款集成多种数据库(如 MySQL、Oracle、金仓、达梦、神通等)与分布式系统组件(如 Redis、Zookeeper、Kafka、Elasticsearch)管理

    5分钟前
    00
  • 重装系统只影响C盘吗?深入解析系统重装的全过程

    重装系统只影响C盘吗?深入解析系统重装的全过程 在计算机的日常使用中,重装系统是一个常见的操作,尤其是在系统出现故障、感染病毒或需要优化系统性能时。然而,许多用户对于重装系统的具体过程和影响存在误解,认为重装系统仅仅是对C盘进行清空和重置

    4分钟前
    00

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信