非同期のcomponentDidMount()の使用は良いですか?


139

componentDidMount()React Nativeで非同期関数として使用することは良い習慣ですか、それとも避けるべきですか?

AsyncStorageコンポーネントのマウント時にいくつかの情報を取得する必要がありますが、それを可能にする唯一の方法は、componentDidMount()関数を非同期にすることです。

async componentDidMount() {
    let auth = await this.getAuth();
    if (auth) 
        this.checkAuth(auth);
}

これに問題はありますか?この問題の他の解決策はありますか?


1
「良い習慣」は意見の問題です。うまくいきますか?はい。
Kraylog 2017


問題を解決するredux-thunkを使用するだけ
Tilak Maddy

@TilakMaddyなぜすべての反応アプリがreduxを使用すると想定するのですか?
がmirakurun

@Mirakurunなぜ私がかつてプレーンJavaScriptの質問をしていたときに、スタックオーバーフロー全体が私がjQueryを使用していると想定したのですか?
Tilak Maddy

回答:


162

まず、違いを指摘し、それがどのように問題を引き起こす可能性があるかを判断します。

以下は、非同期および「同期」componentDidMount()ライフサイクルメソッドのコードです。

// This is typescript code
componentDidMount(): void { /* do something */ }

async componentDidMount(): Promise<void> {
    /* do something */
    /* You can use "await" here */
}

コードを見ると、次の違いを指摘できます。

  1. asyncキーワードは:typescriptです、これは単にコードのマーカーです。それは2つのことをします:
    • 戻り値の型をのPromise<void>代わりに強制しますvoid。戻り値の型を約束しないように明示的に指定した場合(例:void)、typescriptはエラーを吐き出します。
    • awaitメソッド内でキーワードを使用できるようにします。
  2. 戻り値の型がからvoidに変更されましたPromise<void>
    • これはあなたが今これを行うことができることを意味します:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. awaitメソッド内でキーワードを使用して、一時的にその実行を一時停止できます。このような:

    async componentDidMount(): Promise<void> {
        const users = await axios.get<string>("http://localhost:9001/users");
        const questions = await axios.get<string>("http://localhost:9001/questions");
    
        // Sleep for 10 seconds
        await new Promise(resolve => { setTimeout(resolve, 10000); });
    
        // This line of code will be executed after 10+ seconds
        this.setState({users, questions});
        return Promise.resolve();
    }

さて、彼らはどのようにトラブルを引き起こすのでしょうか?

  1. asyncキーワードは、絶対に無害です。
  2. componentDidMount()メソッドを呼び出す必要があるため、戻り値の型Promise<void>も無害な状況は想像できません。

    キーワードPromise<void>なしで戻り値型のメソッドを呼び出しても、戻り値型のメソッドを呼び出してawaitも違いはありませんvoid

  3. componentDidMount()実行を遅延させた後のライフサイクルメソッドがないため、かなり安全に見えます。しかし、落とし穴があります。

    たとえば、上記this.setState({users, questions});は10秒後に実行されます。遅延時間の真ん中に、別の...

    this.setState({users: newerUsers, questions: newerQuestions});

    ...正常に実行され、DOMが更新されました。結果はユーザーに表示されました。時計は時を刻み続け、10秒が経過しました。this.setState(...)その後、delayed が実行され、DOMが再度更新されます。このとき、古いユーザーと古い質問が表示されます。結果はユーザーにも表示されます。

=>これは、使用する(私は100%についてはよく分からない)かなり安全であるasynccomponentDidMount()する方法。私はそれの大ファンであり、今のところ私はあまり頭痛を与えるような問題に遭遇していません。


保留中のPromiseの前に別のsetStateが発生する問題について話すとき、それは非同期/待機構文シュガーまたは従来のコールバックなしのPromiseと同じではありませんか?
Clafou 2018

3
はい!を遅らせることはsetState()常に小さなリスクを伴います。注意して進めるべきです。
–CùĐứcHiếu2018

問題を回避する1つの方法はisFetching: true、コンポーネントの状態の内部などを使用することだと思います。私はこれをreduxでのみ使用しましたが、反応のみの状態管理で完全に有効だと思います。同じ状態がコードの他の場所で更新されるという問題は実際には解決しませんが...
Clafou

1
私はそれに同意します。実際、isFetchingフラグソリューションは、バックエンドの応答を待つ間にフロントエンドでアニメーションを再生したい場合に特に一般的です(isFetching: true)。
–CùĐứcHiếu2018

3
コンポーネントがマウント解除された後にsetStateを実行すると、問題が発生する可能性があります
Eliezer Steinbock

18

2020年4月の更新: この問題は最新のReact 16.13.1で修正されたようです。このサンドボックスの例を参照してください。これを指摘してくれた@abernierに感謝します。


私はいくつかの調査を行い、1つの重要な違いを発見しました 。Reactは非同期ライフサイクルメソッドからのエラーを処理しません。

したがって、次のようなものを書くと:

componentDidMount()
{
    throw new Error('I crashed!');
}

その後、エラーはエラー境界によって捕捉され、それを処理して優雅なメッセージを表示できます。

次のようにコードを変更すると、

async componentDidMount()
{
    throw new Error('I crashed!');
}

これはこれと同等です:

componentDidMount()
{
    return Promise.reject(new Error('I crashed!'));
}

その後、あなたのエラーは黙って飲み込まれます。恥ずかしい、反応...

では、どのようにエラーを処理するのでしょうか?唯一の方法は、次のような明示的なキャッチであると思われます。

async componentDidMount()
{
    try
    {
         await myAsyncFunction();
    }
    catch(error)
    {
        //...
    }
}

またはこのように:

componentDidMount()
{
    myAsyncFunction()
    .catch(()=>
    {
        //...
    });
}

それでもエラーがエラー境界に到達するようにしたい場合は、次のトリックを考えることができます。

  1. エラーをキャッチし、エラーハンドラーにコンポーネントの状態を変更させます。
  2. 状態がエラーを示している場合は、renderメソッドからそれをスローします

例:

class BuggyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  buggyAsyncfunction(){ return Promise.reject(new Error('I crashed async!'));}

  async componentDidMount() {
    try
    {
      await this.buggyAsyncfunction();
    }
    catch(error)
    {
        this.setState({error: error});
    }
  }

  render() {
    if(this.state.error)
        throw this.state.error;

    return <h1>I am OK</h1>;
  }
}

これについて報告された問題はありますか?それでも問題が解決しない場合は、報告してください... thx
abernier

@abernierそれは設計によるものだと思います...おそらく彼らはそれを改善できるかもしれませんが。私はこれについて問題を提出しませんでした...
CF

1
少なくともここでテストされたReact 16.13.1では、そうではないようです。codesandbox.io
s

9

あなたのコードは細かく、私にはとても読みやすいです。このデールジェファーソンの記事を参照してください。彼は非同期のcomponentDidMount例を示しており、とてもよく見えます。

しかし、一部の人々は、コードを読んだ人は、Reactが返されたプロミスで何かをすると仮定するかもしれないと言うでしょう。

したがって、このコードの解釈は、それが良い習慣であるかどうかは非常に個人的なものです。

別のソリューションが必要な場合は、promiseを使用できます。例えば:

componentDidMount() {
    fetch(this.getAuth())
      .then(auth => {
          if (auth) this.checkAuth(auth)
      })
}

3
...または、内部asyncawaitsを使用してインライン関数を使用します...?
エリックカプルン2018年

また、オプション@ErikAllik :)
Tiago Alves

@ErikAllikたまたま例がありますか?
パブロリンコン2018

1
@PabloRincon smthはwhereのようなもの(async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()fetch、promise submitRequestを返す関数です。
エリックカプルン2018年

getAuth関数で発生したエラーを飲み込むため、このコードは間違いなく悪いものです。また、関数がネットワークで何かを行う場合(たとえば)、エラーが予想されます。
CF

6

キーワードcomponentDidMountなしで使用するasyncと、ドキュメントは次のように言います:

componentDidMount()ですぐにsetState()を呼び出すことができます。追加のレンダリングがトリガーされますが、ブラウザーが画面を更新する前に発生します。

使用async componentDidMountすると、この機能が失われます。ブラウザが画面を更新した後、別のレンダリングが行われます。ただし、データのフェッチなど、非同期の使用を考えている場合は、ブラウザが画面を2回更新するのを避けることはできません。別の世界では、ブラウザが画面を更新する前にcomponentDidMountを一時停止することはできません


1
この回答は簡潔で、ドキュメントでサポートされているため、気に入っています。参照しているドキュメントへのリンクを追加していただけますか。
TheUtherSide

これは、たとえば、リソースのロード中にロード状態を表示し、完了時にコンテンツを表示する場合など、良いことかもしれません。
Hjulle

3

更新:

(私のビルド:React 16、Webpack 4、Babel 7):

Babel 7を使用すると、次のことがわかります。

このパターンを使用しています...

async componentDidMount() {
    try {
        const res = await fetch(config.discover.url);
        const data = await res.json();
        console.log(data);
    } catch(e) {
        console.error(e);
    }
}

次のエラーが発生します...

キャッチされていないReferenceError:regeneratorRuntimeが定義されていません

この場合、babel-plugin-transform-runtimeをインストールする必要があります

https://babeljs.io/docs/en/babel-plugin-transform-runtime.html

何らかの理由で上記のパッケージ(babel-plugin-transform-runtime)をインストールしたくない場合は、Promiseパターンに固執する必要があります...

componentDidMount() {
    fetch(config.discover.url)
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })
    .catch(err => console.error(err));
}

3

何をしているのか分かっていれば大丈夫だと思います。ただしasync componentDidMount()componentWillUnmountが実行され、コンポーネントがマウント解除された後でも実行できるため、混乱を招く可能性があります。

内で同期タスクと非同期タスクの両方を開始することもできcomponentDidMountます。componentDidMount非同期の場合、最初のの前にすべての同期コードを配置する必要がありますawait。最初のコードawaitが同期して実行されることは、誰かには明らかではないかもしれません。この場合、おそらくcomponentDidMount同期を維持しますが、syncおよびasyncメソッドを呼び出します。

呼び出しメソッドを選択するかasync componentDidMount()同期メソッドを選択するかにかかわらず、コンポーネントがマウント解除されたときにまだ実行されている可能性があるリスナーまたは非同期メソッドを確実にクリーンアップする必要があります。componentDidMount()async


2

実際、Reactがレガシーライフサイクルメソッド(componentWillMount、componentWillReceiveProps、componentWillUpdate)から離れて非同期レンダリングに移行するため、ComponentDidMountでの非同期読み込みは推奨される設計パターンです。

このブログ投稿は、これが安全である理由を説明し、ComponentDidMountでの非同期読み込みの例を提供するのに非常に役立ちます。

https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html


3
非同期レンダリングは、実際にはライフサイクルを明示的に非同期にすることとは何の関係もありません。それは実際にはアンチパターンです。推奨されるソリューションは、実際にライフサイクルメソッドから非同期メソッドを呼び出すことです
Clayton Ray

1

このようなものを使いたい

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.