マウントされていないコンポーネントでReact状態の更新を実行できません


141

問題

私はReactでアプリケーションを書いていますが、setState(...)後に呼び出される非常に一般的な落とし穴を回避できませんでしたcomponentWillUnmount(...)

私は自分のコードを注意深く見て、いくつかの保護句を配置しようとしましたが、問題は解決せず、まだ警告を監視しています。

したがって、2つの質問があります。

  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 2018

@ ic3b3rgイベントリスニングコードがない場合、問題は解消されます
Igor Soloydenko 2018

わかりました、警備員のthis.setDivSizeThrottleable.cancel()代わりに提案を試みましたthis.isComponentMountedか?
ic3b3rg 2018

1
@ ic3b3rgそれでも同じ実行時警告。
Igor Soloydenko 2018

回答:


92

これがReactHooks固有のソリューションです

エラー

警告:マウントされていないコンポーネントでReact状態の更新を実行することはできません。

解決

コンポーネントがアンマウントされるとすぐに、クリーンアップコールバックで変更されるlet isMounted = trueinsideを宣言でき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フック

すべてのボイラープレートをカスタムフックにカプセル化できます。カスタムフックは、コンポーネントが以前にアンマウントされた場合に、非同期関数を処理して自動的に中止する方法を知っています。

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


2
あなたのトリックはうまくいきます!背後にある魔法は何だろうか?
ニヨンガボ

1
ここでは、依存関係が変更されたとき、およびいずれの場合もコンポーネントがアンマウントされたときに実行される、組み込みのエフェクトクリーンアップ機能を利用します。したがって、これはisMountedフラグをに切り替えるのに最適な場所です。これには、false周囲のエフェクトコールバッククロージャスコープからアクセスできます。クリーンアップ機能は、対応する効果に属するものと考えることができます。
ford 0420

1
それは理にかなっている!私はあなたの答えに満足しています。私はそれから学びました。
ニヨンガボ

聖なる煙...これはisMountedうまくいきます。私はreact-testing-lib10.4.7とformikを使用してい^2.1.4ます。これは完全なハックのように感じられ、Formikで何かが起こった結果です。
PhilLucks20年

1
@VictorMolinaいいえ、それは確かにやり過ぎでしょう。a)fetchinuseEffectやb)のような非同期操作を使用するコンポーネントについては、この手法を検討してください。安定していない、つまり、非同期結果が返され、状態として設定する準備ができる前にマウント解除される可能性があります。
ford 0420年

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呼び出しの前に_isMounted値を検証し、最後にcomponentWillUnmount()で再びfalseにリセットされるため、setStateメソッドの繰り返し呼び出しを停止します。私はそれが機能する方法だと思います。
アビシェーク

8
フックコンポーネントの場合は、これを使用します。const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix20年

@ x-magixこれには実際には参照は必要ありません。戻り関数が閉じることができるローカル変数を使用するだけで、
モルデチャイ

@Abhinavこれが機能する理由は、_isMounted(とは異なりstate)Reactによって管理されていないため、Reactのレンダリングパイプラインの影響を受けないためです。問題は、コンポーネントがアンマウントされるように設定されている場合、Reactはへのすべての呼び出しをデキューすることですsetState()(これにより「再レンダリング」がトリガーされます)。したがって、状態が更新されることはありません
Lightfire2 2820年

34

上記の解決策が機能しない場合は、これを試してみてください。

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

1
おかげでそれは私のために働きます。誰かが私にこのコードを説明できますか?
BadriPaudel20年

@BadriPaudelは、コンポーネントをエスケープするとnullを返し、メモリにデータを保持しなくなります
May'Habit

どうもありがとうございました!
TusharGupta20年

何を返しますか?そのまま貼り付けますか?
さらに

11

この警告は、おそらくsetStateエフェクトフックからの呼び出しが原因で発生しました(これについては、リンクされているこれら3つの問題で 説明し います)。

とにかく、reactバージョンをアップグレードすると警告が削除されました。


5

に変更setDivSizeThrottleableしてみてください

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

私はそれを試しました。現在、この変更を行う前にウィンドウのサイズを変更する際に時々観察していた警告が一貫して表示されています。¯_(ツ)_ /¯これを試していただきありがとうございます。
Igor Soloydenko 2018

5

履歴を使用していないことはわかっていますが、私の場合はuseHistory、React Router DOMのフックを使用していました。これは、状態がReact ContextProviderに保持される前にコンポーネントをアンマウントします。

この問題を修正するために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からデータをフェッチしているのにエラーが発生する場合は、条件内でセッターをラップするだけです。

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)Lodashesスロットル機能にはcancel方法があります。呼び出してcancelcomponentWillUnmountを捨てますisComponentMounted。キャンセルは、新しいプロパティを導入するよりも「慣用的に」反応します。


問題は、私が直接制御していないことです TextLayerInternalです。したがって、「誰のせいがsetState()電話なのか」はわかりません。私はcancelあなたのアドバイスに従って試してみて、それがどうなるか見ていきます
Igor Soloydenko 2018

残念ながら、まだ警告が表示されます。Update 1セクションのコードをチェックして、私が正しい方法で作業していることを確認してください。
Igor Soloydenko 2018

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

Webページを開く方法によっては、マウントが発生していない可能性があります。このような使用として<Link/>componentDidMountライフサイクルからデータをので、すでに仮想DOMに搭載されたページへのバックを必要とキャッチされます。


componentDidMount()中間のcomponentWillUnmount()呼び出しなしで2回呼び出すことができると言っていますか?それは不可能だと思います。
AlexisWilke20年

1
いいえ、2回呼び出されないと言っているので、ページは内部のコードを処理しません componentDidMount()、を使用する<Link/>。私はこれらの問題にReduxを使用し、WebページのデータをReducerストアに保持して、とにかくページをリロードする必要がないようにします。
coder9833idls

0

私は同様の問題を抱えていて、それを解決しました:

reduxでアクションをディスパッチする(認証トークンをredux状態にする)ことで、ユーザーを自動的にログインさせていました

次に、コンポーネントにthis.setState({succ_message: "...")を含むメッセージを表示しようとしました。

コンポーネントが空に見え、コンソールで同じエラーが発生しました:「マウントされていないコンポーネント」..「メモリリーク」など。

このスレッドでウォルターの答えを読んだ後

アプリケーションのルーティングテーブルで、ユーザーがログインしている場合、コンポーネントのルートが無効であることに気付きました。

{!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> ... ;
} ;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.