React-アンマウントされたコンポーネントのsetState()


92

私の反応コンポーネントでは、ajaxリクエストの進行中に単純なスピナーを実装しようとしています。ロードステータスを格納するために状態を使用しています。

何らかの理由で、以下のReactコンポーネントのコードはこのエラーをスローします

マウントされたコンポーネントまたはマウントされたコンポーネントのみを更新できます。これは通常、マウントされていないコンポーネントでsetState()を呼び出したことを意味します。これはノーオペレーションです。未定義のコンポーネントのコードを確認してください。

最初のsetState呼び出しを取り除くと、エラーはなくなります。

constructor(props) {
  super(props);
  this.loadSearches = this.loadSearches.bind(this);

  this.state = {
    loading: false
  }
}

loadSearches() {

  this.setState({
    loading: true,
    searches: []
  });

  console.log('Loading Searches..');

  $.ajax({
    url: this.props.source + '?projectId=' + this.props.projectId,
    dataType: 'json',
    crossDomain: true,
    success: function(data) {
      this.setState({
        loading: false
      });
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
      this.setState({
        loading: false
      });
    }.bind(this)
  });
}

componentDidMount() {
  setInterval(this.loadSearches, this.props.pollInterval);
}

render() {

    let searches = this.state.searches || [];


    return (<div>
          <Table striped bordered condensed hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Submit Date</th>
              <th>Dataset &amp; Datatype</th>
              <th>Results</th>
              <th>Last Downloaded</th>
            </tr>
          </thead>
          {
          searches.map(function(search) {

                let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
                let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
                let records = 0;
                let status = search.status ? search.status.toLowerCase() : ''

                return (
                <tbody key={search.id}>
                  <tr>
                    <td>{search.name}</td>
                    <td>{createdDate}</td>
                    <td>{search.dataset}</td>
                    <td>{records}</td>
                    <td>{downloadedDate}</td>
                  </tr>
                </tbody>
              );
          }
          </Table >
          </div>
      );
  }

問題は、コンポーネントがすでにマウントされているはずのときに(componentDidMountから呼び出されるため)このエラーが発生するのはなぜですか?コンポーネントがマウントされたら状態を設定しても安全だと思いましたか?


私のコンストラクターでは、「this.loadSearches = this.loadSearches.bind(this);」を設定しています。-不正解です。質問に追加してください
Marty

コンストラクタでロードをnullに設定してみましたか?うまくいくかもしれません。this.state = { loading : null };
Pramesh Bajracharya

回答:


69

レンダー機能を見ないで少し難しいです。はあなたがすべきことをすでに見つけることができますが、間隔を使用するたびに、アンマウント時にそれをクリアする必要があります。そう:

componentDidMount() {
    this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}

componentWillUnmount () {
    this.loadInterval && clearInterval(this.loadInterval);
    this.loadInterval = false;
}

これらの成功とエラーのコールバックはアンマウント後も呼び出される可能性があるため、interval変数を使用して、マウントされているかどうかを確認できます。

this.loadInterval && this.setState({
    loading: false
});

これが役立つことを願って、これがうまくいかない場合はレンダリング機能を提供してください。

乾杯


2
ブルーノ、 "this"コンテキストの存在をテストすることはできませんでした。ala this && this.setState .....
james emanon

6
または単に:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz

@GregHerbowiczタイマーを使用してコンポーネントをアンマウントおよびマウントしている場合、単純なクリアを実行しても、コンポーネントはまだ起動されます。
corlaez 2018

14

問題は、コンポーネントがすでにマウントされているはずのときに(componentDidMountから呼び出されるため)このエラーが発生するのはなぜですか?コンポーネントがマウントされたら状態を設定しても安全だと思いましたか?

それはされていないから呼び出されますcomponentDidMount。あなたのcomponentDidMountスポーンではないの積み重ねで、タイマハンドラのスタックで実行されるコールバック関数componentDidMount。どうやら、コールバック(this.loadSearches)が実行されるまでに、コンポーネントはマウント解除されています。

だから、受け入れられた答えはあなたを守ります。非同期関数をキャンセルできない他の非同期APIを使用している場合(既にいくつかのハンドラーに送信されている)、次のようにすることができます。

if (this.isMounted())
     this.setState(...

これは、特にAPIがキャンセル機能を提供している場合(のようsetIntervalclearInterval)、敷物の下にあるものを一掃するように感じますが、すべてのケースで報告するエラーメッセージを取り除きます。


12
isMountedFacebookは使用しないように助言するというアンチパターンは次のとおりです。facebook.github.io/react/blog/2015/12/16/...
マーティ

1
はい、私は「それは敷物の下にあるものを掃くように感じる」と言います。
マーカスジュニウスブルータス2016年

5

別のオプションが必要な人にとっては、ref属性のコールバックメソッドが回避策になります。handleRefのパラメーターは、div DOM要素への参照です。

refとDOMの詳細については、https//facebook.github.io/react/docs/refs-and-the-dom.htmlをご覧ください。

handleRef = (divElement) => {
 if(divElement){
  //set state here
 }
}

render(){
 return (
  <div ref={this.handleRef}>
  </div>
 )
}

5
refを効果的に「isMounted」に使用することは、isMountedを使用することとまったく同じですが、あまり明確ではありません。isMountedはその名前からアンチパターンではありませんが、マウントされていないコンポーネントへの参照を保持するためのアンチパターンであるためです。
Pajn 2017年

3
class myClass extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

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

  componentDidMount() {
    this._isMounted = true;
    this._getData();
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  _getData() {
    axios.get('https://example.com')
      .then(data => {
        if (this._isMounted) {
          this.setState({ data })
        }
      });
  }


  render() {
    ...
  }
}

機能コンポーネントでこれを達成する方法はありますか?@john_per
Tamjid

関数コンポーネントの場合、refを使用します。const _isMounted = useRef(false); @Tamjid
john_per

1

後世のために

私たちの場合、このエラーは、Reflux、コールバック、リダイレクト、およびsetStateに関連していました。setStateをonDoneコールバックに送信しましたが、リダイレクトをonSuccessコールバックに送信しました。成功した場合、onSuccessコールバックはonDoneの前に実行されます。これにより、試行されたsetStateの前にリダイレクトが発生します。したがって、マウントされていないコンポーネントのエラー、setState。

還流ストアアクション:

generateWorkflow: function(
    workflowTemplate,
    trackingNumber,
    done,
    onSuccess,
    onFail)
{...

修正前に呼び出す:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    this.setLoading.bind(this, false),
    this.successRedirect
);

修正後の呼び出し:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    null,
    this.successRedirect,
    this.setLoading.bind(this, false)
);

もっと

場合によっては、ReactのisMountedが「非推奨/アンチパターン」であるため、_mount変数の使用を採用し、それを自分で監視しています。


1

反応フックによって有効化されたソリューションを共有します。

React.useEffect(() => {
  let isSubscribed = true

  callApi(...)
    .catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
    .then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
    .catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))

  return () => (isSubscribed = false)
}, [])

同じソリューションを、フェッチIDの変更に対する以前のリクエストをキャンセルしたい場合に拡張できます。それ以外の場合は、複数の処理中のリクエスト(this.setState順序外と呼ばれる)間で競合状態が発生します。

React.useEffect(() => {
  let isCancelled = false

  callApi(id).then(...).catch(...) // similar to above

  return () => (isCancelled = true)
}, [id])

これは、JavaScriptのクロージャのおかげで機能します。

一般的に、上記のアイデアは、反応ドキュメントで推奨されているmakeCancelableのアプローチに近いものでした。

isMountedはアンチパターンです

クレジット

https://juliangaramendy.dev/use-promise-subscription/

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