componentWillUnmountのフェッチをキャンセルする方法


90

タイトルがすべてを物語っていると思います。まだフェッチしているコンポーネントをアンマウントするたびに、黄色の警告が表示されます。

コンソール

警告:マウントされていないコンポーネントを呼び出す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);
      });
  }

私はその問題を抱えていないという警告は何
ですか

質問更新
ジョアン・ベロ

フェッチのコードを約束または非同期にしましたか
nimamoradi18年

フェッチコードを質問に追加
nimamoradi18年

回答:


80

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

そのため、非同期ロジックの一部をコンポーネントから移動するのが最善です。

それ以外の場合は、何らかの方法でPromiseをキャンセルする必要があります。または、最後の手段として(アンチパターンです)、変数を保持して、コンポーネントがまだマウントされているかどうかを確認できます。

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


4
したがって、componentWillUnmountのフェッチをキャンセルする実際の方法はありませんか?
ジョアン・ベロ

1
ああ、私は前にあなたの答えのコードに気づかなかった、それはうまくいった。感謝
ジョアン・ベロ


2
「そのため、非同期ロジックをコンポーネントから移動するのが最善です。」とはどういう意味ですか?すべてがコンポーネントに反応しませんか?
karpik 2018

1
@Tomasz Mularczykどうもありがとうございました、あなたは価値のあることをしました。
KARTHIKEYAN.A 2018年

25

Reactのフレンドリーな人々は、フェッチコール/プロミスをキャンセル可能なプロミスでラップすることをお勧めします。そのドキュメントには、フェッチを使用してコードをクラスまたは関数から分離することを推奨していませんが、他のクラスおよび関数がこの機能を必要とする可能性があり、コードの重複はアンチパターンであり、長引くコードに関係なく、これは推奨されるようです。で廃棄またはキャンセルする必要がありますcomponentWillUnmount()。Reactに従って、マウントされていないコンポーネントに状態が設定cancel()componentWillUnmountれないように、ラップされたpromiseを呼び出すことができます。

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の問題をフォローすると、与えられた答えが完全に正しくない可能性があることがわかりました。これが私の目的のために働く私が使用する1つのバージョンです:

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にすることで、ガベージコレクターがメモリを解放できるようにすることでした。


githubの問題へのリンクはありますか
Ren

@Ren、ページを編集して問題を議論するためのGitHubサイトがあります。
haleonj

そのGitHubプロジェクトの正確な問題がどこにあるのかもうわかりません。
haleonj

1
GitHubの問題へのリンク:github.com/facebook/react/issues/5465
sammalfix 2018

22

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

参照:https//www.npmjs.com/package/abortcontroller-polyfill

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>


2
AbortControllerのようなリクエストをキャンセルするためのWebAPIがあることを知っていたらよかったのにと思います。しかし、申し分なく、それを知るのに遅すぎることはありません。ありがとうございました。
レックスソフト

11

投稿が開かれたので、「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);
});

興味深いことに、私はこの方法を試してみます。ただし、その前に、最初にAbortControllerAPIを読みます。
レックスソフト

componentWillUnmountでこの単一のAbortControllerのabortメソッドを呼び出すと、コンポーネント内の既存のフェッチがすべてキャンセルされるように、複数のフェッチに1つのAbortControllerインスタンスのみを使用できますか?そうでない場合は、フェッチごとに異なるAbortControllerインスタンスを提供する必要があることを意味します。
レックスソフト

3

この警告の要点は、コンポーネントに、いくつかの未解決のコールバック/プロミスによって保持されているコンポーネントへの参照があることです。

2番目のパターンで行われたようにisMounted状態を維持する(コンポーネントを存続させる)アンチパターンを回避するために、reactWebサイトはオプションの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
これは、概念的にはisMountedフラグを保持することと同じです。フラグをぶら下げるのではなく、クロージャーにバインドするだけですthis
AnilRedshift 2018年

2

「すべてのサブスクリプションと非同期をキャンセルする」必要がある場合、通常、componentWillUnmountのreduxに何かをディスパッチして、他のすべてのサブスクライバーに通知し、必要に応じてサーバーにキャンセルに関するもう1つのリクエストを送信します。


2

キャンセルについてサーバーに通知する必要がない場合は、非同期/待機構文を使用するのが最善の方法だと思います(使用可能な場合)。

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

async componentDidMount() {
  try {
    const responseJson = await fetch('LINK HERE')
      .then((response) => response.json());

    this.setState({
      isLoading: false,
      dataSource: responseJson,
    }
  } catch {
    console.error(error);
  }
}

0

受け入れられたソリューションのキャンセル可能なpromiseフックの例に加えて、useAsyncCallbackリクエストコールバックをラップしてキャンセル可能なpromiseを返すフックがあると便利です。考え方は同じですが、フックが通常のように機能しuseCallbackます。実装例は次のとおりです。

function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
  const isMounted = useRef(true)

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

  const cb = useCallback(callback, dependencies)

  const cancellableCallback = useCallback(
    (...args: any[]) =>
      new Promise<T>((resolve, reject) => {
        cb(...args).then(
          value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
          error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
        )
      }),
    [cb]
  )

  return cancellableCallback
}

-2

私はそれを回避する方法を考え出したと思います。問題は、フェッチ自体ではなく、コンポーネントが閉じられた後のsetStateです。したがって、解決策は、this.state.isMountedとして設定しfalse、次にcomponentWillMountそれをtrueに変更し、componentWillUnmount再びfalseに設定することでした。次にif(this.state.isMounted)、フェッチ内のsetStateだけです。そのようです:

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

  componentDidMount(){
    this.setState({
      isMounted: true,
    })

    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        if(this.state.isMounted){
          this.setState({
            isLoading: false,
            dataSource: responseJson,
          }, function(){
          });
        }
      })
      .catch((error) =>{
        console.error(error);
      });
  }

  componentWillUnmount() {
    this.setState({
      isMounted: false,
    })
  }

3
setStateは、状態の値をすぐには更新しないため、おそらく理想的ではありません。
LeonF 2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.