web-dev-qa-db-ja.com

componentWillUnmountの取得をキャンセルする方法

タイトルがそれをすべて言っていると思います。まだ取得中のコンポーネントをマウント解除するたびに、黄色の警告が表示されます。

警告:マウントされていないコンポーネントに対してsetState(またはforceUpdate)を呼び出すことはできません。これは何もしませんが、修正するにはcomponentWillUnmountメソッド内のすべてのサブスクリプションと非同期タスクをキャンセルします。

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }
53
João Belo

Promiseを起動すると、それが解決するまでに数秒かかることがあり、それまでにユーザーはアプリ内の別の場所に移動した可能性があります。そのため、Promiseが解決するとき、マウントされていないコンポーネントに対してsetStateが実行され、エラーが発生します - あなたの場合と同じです。これもメモリリークを引き起こす可能性があります。

だからこそ、非同期ロジックの一部をコンポーネントから外すのが最善の方法です。

そうでなければ、どういうわけか あなたの約束を取り消す必要があります 。あるいは - 最後の手段として(アンチパターン)、コンポーネントがまだマウントされているかどうかをチェックする変数を保持することができます。

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

私はもう一度強調します - これ はアンチパターン ですが、 Formik の実装の場合と同じように、十分な場合があります).

GitHub についての同様の議論

編集:

これはおそらく私が フック を使って同じ問題(React以外には何もない)を解決する方法でしょう:

オプションA:

import React, { useState, useEffect } from "react";

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

オプションB:クラスの静的プロパティのように振る舞うuseRefの代わりに、値が変わってもコンポーネントをレンダリングしないことを意味します。

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);


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

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

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

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

例: https://codesandbox.io/s/86n1wq2z8

40

Reactの友好的な人々 は、フェッチコール/約束をキャンセル可能な約束にまとめること をお勧めします。このドキュメントでは、フェッチによってクラスや関数からコードを分離することは推奨されていませんが、他のクラスや関数ではこの機能が必要になる可能性があるため、コードの複製はアンチパターンです。 componentWillUnmount()内で破棄またはキャンセルする必要があります。 Reactによれば、マウントされていないコンポーネントに状態が設定されないようにするために、componentWillUnmount内のラップされた約束でcancel()を呼び出すことができます。

ガイドとしてReactを使用した場合、提供されたコードは次のコードスニペットのようになります。

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

----編集----

私はGitHubの問題に従うことによって与えられた答えが全く正しくないかもしれないのを見つけました。これが私の目的のために働く私が使う一つのバージョンです:

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

アイデアは、関数をnullにするなどして、ガベージコレクタがメモリを解放するのを手助けすることでした。

14
haleonj

フェッチ要求をキャンセルするには、 AbortController を使用できます。

class FetchComponent extends React.Component{
  state = { todos: [] };
  
  controller = new AbortController();
  
  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }
  
  componentWillUnmount(){
    this.controller.abort();
  }
  
  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };
  
  componentDidMount(){
    this.setState({ fetch: false });
  }
  
  render(){
    return this.state.fetch && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
10
Paduado

投稿が開かれてから、 "abortable-fetch"が追加されました。 https://developers.google.com/web/updates/2017/09/abortable-fetch

(ドキュメントより)

コントローラ+シグナ​​ル操作AbortControllerとAbortSignalを満たします。

const controller = new AbortController();
const signal = controller.signal;

コントローラには1つのメソッドしかありません。

controller.abort();これを行うと、シグナルを通知します。

signal.addEventListener('abort', () => {
  // Logs true:
  console.log(signal.aborted);
});

このAPIはDOM標準によって提供されており、それがAPI全体です。意図的に汎用的なので、他のWeb標準やJavaScriptライブラリでも使用できます。

たとえば、5秒後にフェッチタイムアウトを設定する方法は次のとおりです。

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
});
7
Ben Yitzhaki

この警告の最も重要な点は、あなたのコンポーネントがそれへの参照を持っているということです。

2番目のパターンで行われていたようにisMounted状態を維持する(コンポーネントが生き続ける)という逆パターンを避けるために、反応のウェブサイトは をオプションのpromise を使って提案します。しかし、そのコードはあなたのオブジェクトを生かし続けるようにも見えます。

代わりに、入れ子になった束縛関数を持つクロージャをsetStateに使うことでそれを行いました。

これが私のコンストラクタです(TypeScript)…

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}
3
Anthony Wieser

「すべての購読をキャンセルして非同期」にする必要がある場合は、通常、componentWillUnmountの値を減らして他のすべての購読者に通知し、必要に応じてもう1つ要求をサーバーに送信します。

3
Sasha Kos