ChatGPT解决这个技术问题 Extra ChatGPT

无法对未安装的组件执行 React 状态更新

问题

我正在用 React 编写一个应用程序,但无法避免一个超级常见的陷阱,即在 componentWillUnmount(...) 之后调用 setState(...)

我非常仔细地查看了我的代码并尝试放置一些保护条款,但问题仍然存在,我仍在观察警告。

因此,我有两个问题:

我如何从堆栈跟踪中找出哪个特定组件和事件处理程序或生命周期挂钩对规则违规负责?好吧,如何解决问题本身,因为我的代码是在考虑到这个陷阱的情况下编写的,并且已经在尝试防止它,但是一些底层组件仍在生成警告。

浏览器控制台

Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.
    in TextLayerInternal (created by Context.Consumer)
    in TextLayer (created by PageInternal) index.js:1446
d/console[e]
index.js:1446
warningWithoutStack
react-dom.development.js:520
warnAboutUpdateOnUnmounted
react-dom.development.js:18238
scheduleWork
react-dom.development.js:19684
enqueueSetState
react-dom.development.js:12936
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:356
_callee$
TextLayer.js:97
tryCatch
runtime.js:63
invoke
runtime.js:282
defineIteratorMethods/</prototype[method]
runtime.js:116
asyncGeneratorStep
asyncToGenerator.js:3
_throw
asyncToGenerator.js:29

https://i.stack.imgur.com/ATfMj.png

代码

书.tsx

import { throttle } from 'lodash';
import * as React from 'react';
import { AutoWidthPdf } from '../shared/AutoWidthPdf';
import BookCommandPanel from '../shared/BookCommandPanel';
import BookTextPath from '../static/pdf/sde.pdf';
import './Book.css';

const DEFAULT_WIDTH = 140;

class Book extends React.Component {
  setDivSizeThrottleable: () => void;
  pdfWrapper: HTMLDivElement | null = null;
  isComponentMounted: boolean = false;
  state = {
    hidden: true,
    pdfWidth: DEFAULT_WIDTH,
  };

  constructor(props: any) {
    super(props);
    this.setDivSizeThrottleable = throttle(
      () => {
        if (this.isComponentMounted) {
          this.setState({
            pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
          });
        }
      },
      500,
    );
  }

  componentDidMount = () => {
    this.isComponentMounted = true;
    this.setDivSizeThrottleable();
    window.addEventListener("resize", this.setDivSizeThrottleable);
  };

  componentWillUnmount = () => {
    this.isComponentMounted = false;
    window.removeEventListener("resize", this.setDivSizeThrottleable);
  };

  render = () => (
    <div className="Book">
      { this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }

      <div className={this.getPdfContentContainerClassName()}>
        <BookCommandPanel
          bookTextPath={BookTextPath}
          />

        <div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
          <AutoWidthPdf
            file={BookTextPath}
            width={this.state.pdfWidth}
            onLoadSuccess={(_: any) => this.onDocumentComplete()}
            />
        </div>

        <BookCommandPanel
          bookTextPath={BookTextPath}
          />
      </div>
    </div>
  );

  getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';

  onDocumentComplete = () => {
    try {
      this.setState({ hidden: false });
      this.setDivSizeThrottleable();
    } catch (caughtError) {
      console.warn({ caughtError });
    }
  };
}

export default Book;

AutoWidthPdf.tsx

import * as React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

interface IProps {
  file: string;
  width: number;
  onLoadSuccess: (pdf: any) => void;
}
export class AutoWidthPdf extends React.Component<IProps> {
  render = () => (
    <Document
      file={this.props.file}
      onLoadSuccess={(_: any) => this.props.onLoadSuccess(_)}
      >
      <Page
        pageNumber={1}
        width={this.props.width}
        />
    </Document>
  );
}

更新 1:取消节流功能(仍然没有运气)

const DEFAULT_WIDTH = 140;

class Book extends React.Component {
  setDivSizeThrottleable: ((() => void) & Cancelable) | undefined;
  pdfWrapper: HTMLDivElement | null = null;
  state = {
    hidden: true,
    pdfWidth: DEFAULT_WIDTH,
  };

  componentDidMount = () => {
    this.setDivSizeThrottleable = throttle(
      () => {
        this.setState({
          pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
        });
      },
      500,
    );

    this.setDivSizeThrottleable();
    window.addEventListener("resize", this.setDivSizeThrottleable);
  };

  componentWillUnmount = () => {
    window.removeEventListener("resize", this.setDivSizeThrottleable!);
    this.setDivSizeThrottleable!.cancel();
    this.setDivSizeThrottleable = undefined;
  };

  render = () => (
    <div className="Book">
      { this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }

      <div className={this.getPdfContentContainerClassName()}>
        <BookCommandPanel
          BookTextPath={BookTextPath}
          />

        <div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
          <AutoWidthPdf
            file={BookTextPath}
            width={this.state.pdfWidth}
            onLoadSuccess={(_: any) => this.onDocumentComplete()}
            />
        </div>

        <BookCommandPanel
          BookTextPath={BookTextPath}
          />
      </div>
    </div>
  );

  getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';

  onDocumentComplete = () => {
    try {
      this.setState({ hidden: false });
      this.setDivSizeThrottleable!();
    } catch (caughtError) {
      console.warn({ caughtError });
    }
  };
}

export default Book;
如果您注释掉添加和删除侦听器,问题是否仍然存在?
@ic3b3rg 如果没有事件监听代码,问题就会消失
好的,您是否尝试过使用 this.setDivSizeThrottleable.cancel() 而不是 this.isComponentMounted 守卫的建议?
@ic3b3rg 仍然是相同的运行时警告。

f
ford04

这是一个 React Hooks 特定的解决方案

错误

警告:无法对未安装的组件执行 React 状态更新。

解决方案

您可以在 useEffect 中声明 let isMounted = true,一旦卸载组件,它将在 cleanup callback 中更改。在状态更新之前,您现在有条件地检查此变量:

useEffect(() => {
  let isMounted = true;               // note mutable flag
  someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // add conditional check
  })
  return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);                               // adjust dependencies to your needs

const Parent = () => { const [mounted, setMounted] = useState(true); return (

Parent: {mounted && }

卸载Child,在它还在加载的时候,它不会在后面设置状态,所以不会触发错误。

); }; const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useEffect(() => { let isMounted = true; fetchData(); return () => { isMounted = false; }; // 模拟一些 Web API 抓取函数 fetchData() { setTimeout(() => { // drop "if (isMounted)" 再次触发错误 // (使用 IDE,不适用于堆栈片段) if (isMounted) setState("data fetched") else console.log("aborted setState on unmounted component") }, 4000); } }, []);返回
子:{state}
; }; ReactDOM.render(, document.getElementById("root"));

扩展:自定义 useAsync Hook

我们可以将所有样板封装到一个自定义 Hook 中,如果组件卸载或依赖值之前发生更改,它会自动中止异步函数:

function useAsync(asyncFn, onSuccess) {
  useEffect(() => {
    let isActive = true;
    asyncFn().then(data => {
      if (isActive) onSuccess(data);
    });
    return () => { isActive = false };
  }, [asyncFn, onSuccess]);
}

// 用于在卸载或依赖项更改时自动中止的自定义 Hook // 您也可以为 Promise 错误添加 onFailure。 function useAsync(asyncFn, onSuccess) { useEffect(() => { let isActive = true; asyncFn().then(data => { if (isActive) onSuccess(data) else console.log("未安装组件上的中止 setState" ) }); return () => { isActive = false; }; }, [asyncFn, onSuccess]); } const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useAsync(simulateFetchData, setState);返回

子:{state}
; }; const Parent = () => { const [mounted, setMounted] = useState(true); return (
Parent: {mounted && }

卸载Child,在它还在加载的时候,它不会在后面设置状态,所以不会触发错误。

); }; const simulationFetchData = () => new Promise(resolve => setTimeout(() => resolve("数据获取"), 4000)); ReactDOM.render(, document.getElementById("root"));

有关效果清理的更多信息:Overreacted: A Complete Guide to useEffect


我们在此处利用内置效果 cleanup 功能,该功能在依赖项更改时运行,并且在任何一种情况下都在组件卸载时运行。因此,这是将 isMounted 标志切换为 false 的理想场所,可以从周围的效果回调闭包范围访问。您可以将清理函数视为 belonging to 的相应效果。
@VictorMolina 不,那肯定是矫枉过正。考虑这种技术用于组件 a) 使用异步操作,如 useEffect 中的 fetch 和 b) 不稳定,即可能在异步结果返回之前卸载并准备好设置为状态。
stackoverflow.com/a/63213676medium.com/better-programming/… 很有趣,但最终是您的回答最终帮助我开始工作。谢谢!
如果您已经缩小了导致问题的原因/useEffect 的范围,这似乎是一个不错的解决方案,但其中一个问题 1. 询问如何确定哪个组件、处理程序、钩子等是造成此错误的原因。为什么对一个没有提及整个问题并回答两个问题的问题有这么多的赞成票?我检查了整个线程中也没有任何关于查找错误来源的提示的帖子;根据堆栈跟踪无法弄清楚吗?
@Woodz 是的,很好的提示。 useCallback 是 React 中将依赖关系的责任推迟到 useAsync 的客户端的常用且推荐的方式。您可以切换到 useAsync 内的可变引用来存储最近的回调,因此客户端可以直接传递他们的函数/回调而无需依赖。但我会谨慎使用这种模式,因为它可能更令人困惑和命令式的方法。
v
vinod

要删除 - 无法对未安装的组件警告执行 React 状态更新,请在某个条件下使用 componentDidMount 方法,并在 componentWillUnmount 方法上将该条件设为 false。例如 : -

class Home extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

    this.state = {
      news: [],
    };
  }

  componentDidMount() {
    this._isMounted = true;

    ajaxVar
      .get('https://domain')
      .then(result => {
        if (this._isMounted) {
          this.setState({
            news: result.data.hits,
          });
        }
      });
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  render() {
    ...
  }
}

这行得通,但为什么要这样做呢?究竟是什么导致了这个错误?以及这是如何解决的:|
它工作正常。它停止了 setState 方法的重复调用,因为它在 setState 调用之前验证 _isMounted 值,然后最后在 componentWillUnmount() 中再次重置为 false。我想,这就是它的工作方式。
对于钩子组件,请使用:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
@x-magix 您实际上并不需要 ref,您可以使用返回函数可以关闭的局部变量。
@Abhinav 我最好的猜测是为什么这会起作用 _isMounted 不是由 React 管理的(与 state 不同),因此不受 React 的 rendering pipeline 的约束。问题是,当一个组件被设置为卸载时,React 会取消对 setState() 的任何调用(这将触发“重新渲染”);因此,状态永远不会更新
M
May'Habit

如果上述解决方案不起作用,试试这个,它对我有用:

componentWillUnmount() {
    // fix Warning: Can't perform a React state update on an unmounted component
    this.setState = (state,callback)=>{
        return;
    };
}

@BadriPaudel 转义组件时返回 null,它将不再在内存中保存任何数据
返回什么?照原样粘贴?
我不推荐这个解决方案,它很hacky。 @BadriPaudel 这将在 componentWillUnmount 之后用一个什么都不做的函数替换 setState 函数。 setState 函数将继续被调用。
s
sfletche

有一个相当常见的钩子 useIsMounted 可以解决这个问题(对于功能组件)......

import { useRef, useEffect } from 'react';

export function useIsMounted() {
  const isMounted = useRef(false);

  useEffect(() => {
    isMounted.current = true;
    return () => isMounted.current = false;
  }, []);

  return isMounted;
}

然后在您的功能组件中

function Book() {
  const isMounted = useIsMounted();
  ...

  useEffect(() => {
    asyncOperation().then(data => {
      if (isMounted.current) { setState(data); }
    })
  });
  ...
}

我们可以对多个组件使用同一个钩子吗?
@AyushKumar:是的,你可以!这就是钩子的美妙之处! isMounted 状态将特定于调用 useIsMounted 的每个组件!
我想useIsMounted的这种解决方式应该包含在核心包中。
另一个问题是我是否在我的 useEffect 挂钩中添加了 UseIsMounted,并且我已经启动了一个 listener。在代码中添加 return () => 会导致任何泄漏吗?
M
Matthieu Kern

检查组件是否已安装实际上是反模式 as per React documentationsetState 警告的解决方案是利用 AbortController

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort()                       // stop the query by aborting on the AbortController on unmount
  }
}, [])

对于不基于 Fetch API 的异步操作,仍然应该有一种方法可以取消这些异步操作,并且您应该利用这些方法而不仅仅是检查组件是否已挂载。如果您正在构建自己的 API,则可以在其中实现 AbortController API 来处理它。

对于更多上下文,检查组件是否已安装是一种反模式,因为 React 在内部检查组件是否已安装以显示该警告。再次执行相同的检查只是隐藏警告的一种方法,还有一些比在代码库的很大一部分中添加这段代码更简单的方法来隐藏它们。

来源:https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934


P
Peter Lamberg

我收到此警告可能是因为从效果挂钩调用 setState(这在这 3 个 issues linked together 中进行了讨论)。

无论如何,升级反应版本删除了警告。


s
scr2em

React 已经删除了这个警告,但这里有一个更好的解决方案(不仅仅是解决方法)

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort() 
  }
}, [])

取东西时的绝佳解决方案!更好地停止获取(为用户+服务器资源保存数据)
D
Daniel Santana

@ford04 的解决方案对我不起作用,特别是如果您需要在多个地方使用 isMounted(例如多个 useEffect),建议使用 useRef,如下所示:

基本包

"dependencies": 
{
  "react": "17.0.1",
}
"devDependencies": { 
  "typescript": "4.1.5",
}

我的钩子组件

export const SubscriptionsView: React.FC = () => {
  const [data, setData] = useState<Subscription[]>();
  const isMounted = React.useRef(true);

  React.useEffect(() => {
    if (isMounted.current) {
      // fetch data
      // setData (fetch result)

      return () => {
        isMounted.current = false;
      };
    }
  }
});

同意这一点,使用此解决方案更方便,并确保单一事实来源。
i
ic3b3rg

尝试将 setDivSizeThrottleable 更改为

this.setDivSizeThrottleable = throttle(
  () => {
    if (this.isComponentMounted) {
      this.setState({
        pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
      });
    }
  },
  500,
  { leading: false, trailing: true }
);

我确实试过了。现在我一直看到警告,我只是在进行此更改之前不时地观察调整窗口大小。 ¯_(ツ)_/¯ 感谢您尝试这个。
佚名

我知道您没有使用历史记录,但在我的情况下,我使用了来自 React Router DOM 的 useHistory 钩子,它会在状态持久保存在我的 React Context Provider 之前卸载组件。

为了解决这个问题,我使用了嵌套组件的钩子 withRouter,在我的例子中是 export default withRouter(Login),并在组件 const Login = props => { ...; props.history.push("/dashboard"); ... 内。我还从组件中删除了另一个 props.history.push,例如 if(authorization.token) return props.history.push('/dashboard'),因为这会导致循环,因为 authorization 状态。

将新项目推送到 history 的替代方法。


S
Scott Wager

将 ref 添加到 jsx 组件,然后检查它是否存在

function Book() {
  const ref = useRef();

  useEffect(() => {
    asyncOperation().then(data => {
      if (ref.current) setState(data);
    })
  });

  return <div ref={ref}>content</div>
}

这对我有用,Nextjs 应用程序
N
Niyongabo Eric

我遇到了类似的问题,感谢@ford04 帮助了我。

但是,发生了另一个错误。

注意。我正在使用 ReactJS 钩子

ndex.js:1 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

是什么导致错误?

import {useHistory} from 'react-router-dom'

const History = useHistory()
if (true) {
  history.push('/new-route');
}
return (
  <>
    <render component />
  </>
)

这无法正常工作,因为尽管您正在重定向到新页面,但所有状态和道具都在 dom 上进行操作,或者只是渲染到前一页并没有停止。

我找到了什么解决方案

import {Redirect} from 'react-router-dom'

if (true) {
  return <redirect to="/new-route" />
}
return (
  <>
    <render component />
  </>
)

不错的解决方案,但应该返回
D
Drew Cordano

如果您从 axios 获取数据并且仍然出现错误,只需将 setter 包装在条件中即可

let isRendered = useRef(false);
useEffect(() => {
    isRendered = true;
    axios
        .get("/sample/api")
        .then(res => {
            if (isRendered) {
                setState(res.data);
            }
            return null;
        })
        .catch(err => console.log(err));
    return () => {
        isRendered = false;
    };
}, []);

为什么不使用 isRendered 而不使用 .current ?这是一个功能吗?
@usertest我刚刚实现了这个解决方案,但必须使用.current。
D
Dmitriy Mozgovoy

在大多数情况下,isMounted 方法是一种反模式,因为它实际上并没有清理/取消任何东西,它只是避免更改未安装组件的状态,但对挂起的异步任务不做任何事情。 React 团队 recently removed 发出泄漏警告,因为用户不断创建大量反模式来隐藏警告而不是修复其原因。

但是用纯 JS 编写可取消的代码确实很棘手。为了解决这个问题,我使用自定义钩子创建了自己的库 useAsyncEffect2,构建在可取消承诺 (c-promise2) 之上,用于执行可取消异步代码以实现其优雅取消。所有异步阶段(承诺),包括深度阶段,都是可以取消的。这意味着如果其父上下文被取消,此处的请求将自动中止。当然,可以使用任何其他异步操作来代替请求。

具有普通 useState 用法的 useAsyncEffect 演示(现场演示):

    import React, { useState } from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({url}) {
      const [text, setText] = useState("");
    
      const cancel = useAsyncEffect(
        function* () {
          setText("fetching...");
          const json = (yield cpAxios(url)).data;
          setText(`Success: ${JSON.stringify(json)}`);
        },
        [url]
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick={cancel}>
            Cancel request
          </button>
        </div>
      );
    }

useAsyncEffect Demo 与内部状态使用(现场演示):

    import React from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({ url, timeout }) {
      const [cancel, done, result, err] = useAsyncEffect(
        function* () {
          return (yield cpAxios(url).timeout(timeout)).data;
        },
        { states: true, deps: [url] }
      );
    
      return (
        <div>
          {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
          <button onClick={cancel} disabled={done}>
            Cancel async effect (abort request)
          </button>
        </div>
      );
    }

使用装饰器的类组件(现场演示)

import React, { Component } from "react";
import { ReactComponent } from "c-promise2";
import cpAxios from "cp-axios";

@ReactComponent
class TestComponent extends Component {
  state = {
    text: ""
  };

  *componentDidMount(scope) {
    const { url, timeout } = this.props;
    const response = yield cpAxios(url).timeout(timeout);
    this.setState({ text: JSON.stringify(response.data, null, 2) });
  }

  render() {
    return (<div>{this.state.text}</div>);
  }
}

export default TestComponent;

更多其他示例:

带有错误处理的 Axios 请求

通过坐标获取天气

实时搜索

暂停和恢复

进度捕获


P
ParisaN

对于这个错误,我有 2 个解决方案:

返回:

如果你使用了hookuseEffect,那么在useEffect后面放一个return

useEffect(() => {
    window.addEventListener('mousemove', logMouseMove)
    return () => {
        window.removeEventListener('mousemove', logMouseMove)
    }
}, [])

组件将卸载:

如果您使用componentDidMount,那么将componentWillUnmount放在它旁边。

componentDidMount() { 
    window.addEventListener('mousemove', this.logMouseMove)
}

componentWillUnmount() {
    window.removeEventListener('mousemove', this.logMouseMove)
}

R
R Esmond

编辑:我刚刚意识到警告引用了一个名为 TextLayerInternal 的组件。这很可能是你的错误所在。其余部分仍然相关,但它可能无法解决您的问题。

1) 获取此警告的组件实例很困难。看起来有一些讨论可以在 React 中改进这一点,但目前还没有简单的方法来做到这一点。我怀疑它尚未构建的原因可能是因为组件的预期编写方式是,无论组件的状态如何,卸载后的 setState 都是不可能的。就 React 团队而言,问题始终出在 Component 代码中,而不是 Component 实例中,这就是您获得 Component Type 名称的原因。

这个答案可能不令人满意,但我想我可以解决你的问题。

2) Lodashes 节流函数有一个 cancel 方法。在 componentWillUnmount 中调用 cancel 并放弃 isComponentMounted。取消比引入新属性更“惯用” React。


问题是,我不直接控制 TextLayerInternal。因此,我不知道“谁的错是 setState() 电话”。我会按照您的建议尝试 cancel,看看效果如何,
不幸的是,我仍然看到警告。请检查更新 1 部分中的代码,以验证我以正确的方式做事。
A
Archimedes Trajano

更新不要使用我的原始答案,因为它不起作用

这个答案是基于使用可取消的承诺和 makecancelable 中的一个注释,我迁移到使用钩子。但是,它似乎不会取消 async/await 甚至 cancelable-promise does not support canceling of a chain of awaits 的链

对此进行更多研究,看来some internal Google reasons prevented cancelable promises from coming into the standard

此外,Bluebird 有一些承诺,它引入了可取消的承诺,但它在 Expo 中不起作用,或者至少我还没有看到它在 Expo 中起作用的例子。

accepted answer 是最好的。由于我使用 TypeScript,我对代码进行了一些修改(我明确设置了依赖项,因为接受的答案的隐式依赖项似乎在我的应用程序上提供了重新渲染循环,添加并使用 async/await 而不是 promise 链,传递一个引用挂载的对象,以便在需要时可以提前取消异步/等待链)

/**
 * This starts an async function and executes another function that performs
 * React state changes if the component is still mounted after the async
 * operation completes
 * @template T
 * @param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
 *   it has a copy of the mounted ref so an await chain can be canceled earlier.
 * @param {(asyncResult: T) => void} onSuccess this gets executed after async
 *   function is resolved and the component is still mounted
 * @param {import("react").DependencyList} deps
 */
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
  const mountedRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    (async () => {
      const x = await asyncFunction(mountedRef);
      if (mountedRef.current) {
        onSuccess(x);
      }
    })();
    return () => {
      mountedRef.current = false;
    };
  }, deps);
}

原始答案

由于我有许多不同的操作是 async,因此我使用 cancelable-promise 包以最少的代码更改来解决此问题。

以前的代码:

useEffect(() => 
  (async () => {
    const bar = await fooAsync();
    setSomeState(bar);
  })(),
  []
);

新代码:

import { cancelable } from "cancelable-promise";

...

useEffect(
  () => {
    const cancelablePromise = cancelable(async () => {
      const bar = await fooAsync();
      setSomeState(bar);
    })
    return () => cancelablePromise.cancel();
  },
  []
);

您也可以像这样在自定义实用程序函数中编写它

/**
 * This wraps an async function in a cancelable promise
 * @param {() => PromiseLike<void>} asyncFunction
 * @param {React.DependencyList} deps
 */
export function useCancelableEffect(asyncFunction, deps) {
  useEffect(() => {
    const cancelablePromise = cancelable(asyncFunction());
    return () => cancelablePromise.cancel();
  }, deps);
}

N
Nicolas

基于@ford04 的回答,这里同样封装在一个方法中:

import React, { FC, useState, useEffect, DependencyList } from 'react';

export function useEffectAsync( effectAsyncFun : ( isMounted: () => boolean ) => unknown, deps?: DependencyList ) {
    useEffect( () => {
        let isMounted = true;
        const _unused = effectAsyncFun( () => isMounted );
        return () => { isMounted = false; };
    }, deps );
} 

用法:

const MyComponent : FC<{}> = (props) => {
    const [ asyncProp , setAsyncProp ] = useState( '' ) ;
    useEffectAsync( async ( isMounted ) =>
    {
        const someAsyncProp = await ... ;
        if ( isMounted() )
             setAsyncProp( someAsyncProp ) ;
    });
    return <div> ... ;
} ;

c
coder9833idls

根据您打开网页的方式,您可能不会导致安装。例如使用 <Link/> 返回到已经安装在虚拟 DOM 中的页面,因此需要来自 componentDidMount 生命周期的数据被捕获。


您是说 componentDidMount() 可以被调用两次而没有中间的 componentWillUnmount() 调用?我不认为这是可能的。
不,我是说它没有被调用两次,这就是为什么在使用 <Link/> 时页面不处理 componentDidMount() 内的代码的原因。我使用 Redux 来解决这些问题,并将网页的数据保存在 Reducer 存储中,这样我就不需要重新加载页面。
E
Engr.Aftab Ufaq

这是一个简单的解决方案。这个警告是由于当我们在后台执行一些获取请求时(因为一些请求需要一些时间。)并且我们从那个屏幕导航回来,然后他们反应不能更新状态。这是示例代码。在每个状态更新之前写下这一行。

if(!isScreenMounted.current) return;

这是完整的代码

import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
    const isScreenMounted = useRef(true)
    useEffect(() => {
        return () =>  isScreenMounted.current = false
    },[])

    const ConvertFileSubmit = () => {
        if(!isScreenMounted.current) return;
         setUpLoading(true)
 
         var formdata = new FormData();
         var file = {
             uri: `file://${route.params.selectedfiles[0].uri}`,
             type:`${route.params.selectedfiles[0].minetype}`,
             name:`${route.params.selectedfiles[0].displayname}`,
         };
         
         formdata.append("file",file);
         
         fetch(`${BASEURL}/UploadFile`, {
             method: 'POST',
             body: formdata,
             redirect: 'manual'
         }).then(response => response.json())
         .then(result => {
             if(!isScreenMounted.current) return;
             setUpLoading(false)    
         }).catch(error => {
             console.log('error', error)
         });
     }

    return(
    <>
        <StatusBar barStyle="dark-content" />
        <SafeAreaView>
            <ScrollView
            contentInsetAdjustmentBehavior="automatic"
            style={styles.scrollView}>
               <Text>Search Screen</Text>
            </ScrollView>
        </SafeAreaView>
    </>
    )
}

export default SearchScreen;


const styles = StyleSheet.create({
    scrollView: {
        backgroundColor:"red",
    },
    container:{
        flex:1,
        justifyContent:"center",
        alignItems:"center"
    }
})

A
Abdulla Ababakre

我通过提供 useEffect 挂钩中使用的所有参数解决了这个问题

代码报告了错误:

useEffect(() => {
    getDistrict({
      geonameid: countryId,
      subdistrict: level,
    }).then((res) => {
      ......
    });
  }, [countryId]);

修复后的代码:

useEffect(() => {
    getDistrict({
      geonameid: countryId,
      subdistrict: level,
    }).then((res) => {
      ......
    });
  }, [countryId,level]);

可以看到,在我提供了所有应该通过的参数(包括级别参数)后,问题就解决了。


B
Bulent Balci

我有一个类似的问题并解决了它:

我通过在 redux 上发送一个操作来自动让用户登录(将身份验证令牌放在 redux 状态)

然后我试图在我的组件中显示一条带有 this.setState({succ_message: "...") 的消息。

组件在控制台上看起来是空的,出现相同的错误:“未安装的组件”..“内存泄漏”等。

在我阅读了沃尔特在这个帖子中的回答之后

我注意到在我的应用程序的路由表中,如果用户登录,我的组件的路由无效:

{!this.props.user.token &&
        <div>
            <Route path="/register/:type" exact component={MyComp} />                                             
        </div>
}

无论令牌是否存在,我都使 Route 可见。


g
guneetgstar

受@ford04 接受的答案的启发,我有更好的方法来处理它,而不是在 useAsync 中使用 useEffect 创建一个返回回调的新函数 componentWillUnmount

function asyncRequest(asyncRequest, onSuccess, onError, onComplete) {
  let isMounted=true
  asyncRequest().then((data => isMounted ? onSuccess(data):null)).catch(onError).finally(onComplete)
  return () => {isMounted=false}
}

...

useEffect(()=>{
        return asyncRequest(()=>someAsyncTask(arg), response=> {
            setSomeState(response)
        },onError, onComplete)
    },[])


我不建议依赖本地 isMounted 变量,而是将其设为状态(通过 useState 挂钩)。
有什么关系?至少我想不出任何不同的行为。
K
Kaushal Shah
const handleClick = async (item: NavheadersType, index: number) => {
    const newNavHeaders = [...navheaders];
    if (item.url) {
      await router.push(item.url);   =>>>> line causing error (causing route to happen)
      // router.push(item.url);  =>>> coreect line
      newNavHeaders.forEach((item) => (item.active = false));
      newNavHeaders[index].active = true;
      setnavheaders([...newNavHeaders]);
    }
  };

你能添加一些定义吗?
D
Devyn Collier Johnson

下面将最简单和最紧凑的解决方案(带有解释)视为单线解决方案。

useEffect(() => { return () => {}; }, []);

上面的 useEffect() 示例返回一个回调函数,触发 React 在更新状态之前完成其生命周期的卸载部分。

只需要这个非常简单的解决方案。此外,它也不同于 @ford04 和 @sfletche 提供的虚构语法。顺便说一句,来自 @ford04 的以下代码片段纯属虚构语法(@sfletche@vinod@guneetgstar@Drew Cordano 使用了完全相同的虚构语法)。

data => { <--- 虚构/虚构语法

someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // add conditional check
  })

我所有的 linter 和我整个团队的所有 linter 都不会接受它,他们会报告 Uncaught SyntaxError: unexpected token: '=>'。我很惊讶没有人掌握想象的语法。任何参与过这个问题线程的人,尤其是支持投票的人,会向我解释他们是如何找到适合他们的解决方案的吗?


您关于“虚构语法”的说法是一个错误。它适用于许多其他人!如果您的 someAsynchronousOperation() 返回值类型是 Promise<void>,那么肯定 data 将导致 TypeScript 编译错误。但是,如果 X 不是 undefined/void/neverPromise<X>,那么您肯定可以使用 .then(data => {...})!您还没有提供一个完整的最小示例来推理它。如果您希望解决您的特定代码问题,请在 StackOverflow 上打开一个单独的问题。您不想被否决或标记答案。
你说的是箭头功能吗?这是在 ES6 中引入的。我不知道您是如何配置 linter 的,但这是非常常见的语法。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
好吧,我没有对您投反对票...在不了解您的 linter 设置或有关您的技术堆栈的任何信息的情况下,我无法确定为什么这对您来说显示为语法错误,但也许是因为您的 linter 正在使用不支持此功能的旧版本 Javascript 的设置,但这只是猜测。
M
Morten Moe

受@ford04 回答的启发,我使用了这个钩子,它还接受成功、错误、finally 和 abortFn 的回调:

export const useAsync = (
        asyncFn, 
        onSuccess = false, 
        onError = false, 
        onFinally = false, 
        abortFn = false
    ) => {

    useEffect(() => {
        let isMounted = true;
        const run = async () => {
            try{
                let data = await asyncFn()
                if (isMounted && onSuccess) onSuccess(data)
            } catch(error) {
                if (isMounted && onError) onSuccess(error)
            } finally {
                if (isMounted && onFinally) onFinally()
            }
        }
        run()
        return () => {
            if(abortFn) abortFn()
            isMounted = false
        };
    }, [asyncFn, onSuccess])
}

如果 asyncFn 正在从后端进行某种获取,则在卸载组件时中止它通常是有意义的(但并非总是如此,有时,如果您正在将一些数据加载到存储中,那么您可能只想即使组件未安装也完成它)