无法在已卸载的组件上执行React状态更新


142

问题

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

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

因此,我有两个问题:

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

浏览器控制台

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

在此处输入图片说明

Book.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

@ ic3b3rg如果没有事件侦听代码,问题将消失
Igor Soloydenko '18

好的,您是否尝试过建议this.setDivSizeThrottleable.cancel()而不是this.isComponentMounted警卫?
ic3b3rg

1
@ ic3b3rg仍然是相同的运行时警告。
伊戈尔·索洛伊登科

Answers:


93

这是一个针对React Hooks的解决方案

错误

警告:无法在已卸载的组件上执行React状态更新。

您可以let isMounted = true在内部声明useEffect,一旦卸载组件,它将在清理回调中进行更改。在状态更新之前,您现在有条件地检查此变量:

useEffect(() => {
  let isMounted = true; // note this flag denote mount status
  someAsyncOperation().then(data => {
    if (isMounted) setState(data);
  })
  return () => { isMounted = false }; // use effect cleanup to set flag false, if unmounted
});

扩展:自定义useAsync挂钩

我们可以将所有样板文件封装到一个自定义的Hook中,该钩子知道在组件卸载之前如何处理并自动中止异步功能:

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


2
你的把戏行之有效!我想知道背后的魔力是什么?
Niyongabo

1
我们在这里利用了内置的效果清除功能,该功能在依赖项更改时或组件卸载时都可以运行。因此,这是将isMounted标志切换到的理想位置false,可以从周围的效果回调关闭范围访问该标志。您可以认为清除功能属于其相应的效果。
ford04

1
这说得通!我对您的回答感到满意。我从中学到了。
Niyongabo '20

1
@VictorMolina不,那肯定是过大了。对于组件a)使用fetchinuseEffect和b)这样不稳定的异步操作,请考虑使用此技术,这些异步操作可能不稳定,即可能在异步结果返回之前被卸载并准备设置为状态。
ford04

1
stackoverflow.com/a/63213676medium.com/better-programming / ...很有趣,但最终您的回答是最终帮助我使我的工作正常的原因。谢谢!
瑞安

87

移除-无法对未安装的组件警告执行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() {
    ...
  }
}

3
这行得通,但是为什么要行得通呢?究竟是什么导致此错误?以及如何解决它:|
Abhinav

工作正常。它停止setState方法的重复调用,因为它在setState调用之前验证_isMounted值,然后最后再次在componentWillUnmount()中重置为false。我认为,这就是它的工作方式。
Abhishek'3

8
对于挂钩组件,请使用以下代码:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix

@ x-magix您实际上并不需要引用,只需使用返回函数可以关闭的局部变量即可。
Mordechai

@Abhinav我的最佳猜测是它为什么_isMounted不受React的管理(不同于state),因此不受React的渲染管道的约束。问题在于,当一个组件被设置为要卸载时,React使所有对它的调用都出队setState()(这将触发“重新渲染”)。因此,状态永远不会更新
Lightfire228 '20

35

如果上述解决方案不起作用,请尝试此操作,它对我有用:

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

1
谢谢,对我有用。谁能向我解释这段代码?
Badri Paudel

当逃逸组件时,@ BadriPaudel返回null,它将不再在内存中保存任何数据
May'Habit

非常感谢你做的这些!
塔莎·古普塔

还什么?像它一样粘贴吗?
加上


5

尝试更改setDivSizeThrottleable

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

我确实尝试过。现在,我一直看到警告,我只是在更改此窗口之前不时观察调整窗口大小。¯_(ツ)_ /¯不过感谢您尝试此操作。
伊戈尔(Igor Soloydenko)

5

我知道您没有使用历史记录,但就我而言,我使用的是useHistory来自React Router DOM的钩子,该钩子在状态保留在我的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状态。

将新项目推向历史的替代方法。


2

如果您是从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;
    };
}, []);

2

有一个相当普遍的钩子可以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); }
    })
  });
  ...
}

1

编辑:我刚刚意识到警告正在引用名为的组件TextLayerInternal。这可能是您的错误所在。其余的内容仍然相关,但可能无法解决您的问题。

1)很难获得该警告的组件实例。在React中似乎有一些讨论可以改善此问题,但目前尚无简便的方法。我怀疑它尚未构建的原因很可能是因为期望组件以这样一种方式编写:无论组件的状态如何,都无法在卸载后使用setState。就React团队而言,问题始终在组件代码中,而不是在组件实例中,这就是为什么要获得组件类型名称的原因。

该答案可能无法令人满意,但我认为我可以解决您的问题。

2)Lodashs的节流功能有一种cancel方法。通话cancelcomponentWillUnmount和沟isComponentMounted。与引入新属性相比,取消更像是“习惯上”的React。


问题是,我不直接控制TextLayerInternal。因此,我不知道“谁的错是setState()电话”。我会cancel根据您的建议尝试尝试,看看效果如何,
Igor Soloydenko

不幸的是,我仍然看到警告。请检查“更新1”部分中的代码,以验证我的工作方式正确。
伊戈尔(Igor Soloydenko)

1

我有一个类似的问题,感谢@ 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 />
  </>
)

1

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


您是说componentDidMount()可以两次componentWillUnmount()调用而无需中间调用吗?我认为那是不可能的。
亚历克西斯·威尔克

1
不,我是说它没有被调用两次,这就是为什么使用时页面不处理内部代码componentDidMount()的原因<Link/>。我使用Redux来解决这些问题,并将网页的数据保留在Reducer存储中,因此无论如何我都不需要重新加载页面。
coder9833idls

0

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

我通过在redux上调度一个动作来自动使用户登录(将身份验证令牌置于redux状态)

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

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

在我读完Walter的答案之后

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

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

我使该路由可见,无论令牌是否存在。


0

基于@ 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> ... ;
} ;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.