v19.2Latest

イベントとエフェクトの分離

イベントハンドラは、同じ操作を再度実行したときにのみ再実行されます。イベントハンドラとは異なり、エフェクトは、読み取るプロップや状態変数などの値が前回のレンダー時と異なる場合に再同期されます。場合によっては、両方の動作を組み合わせたいこともあります。つまり、一部の値には反応して再実行されるが、他の値には反応しないエフェクトです。このページでは、その方法を説明します。

学習内容
  • イベントハンドラとエフェクトのどちらを選択するか
  • エフェクトがリアクティブで、イベントハンドラがそうでない理由
  • エフェクトのコードの一部をリアクティブにしたくない場合の対処法
  • エフェクトイベントとは何か、およびエフェクトからエフェクトイベントを抽出する方法
  • エフェクトイベントを使用してエフェクトから最新のプロップと状態を読み取る方法

イベントハンドラとエフェクトの選択

まず、イベントハンドラとエフェクトの違いを復習しましょう。

チャットルームコンポーネントを実装していると想像してください。要件は次のようになります:

  1. コンポーネントは選択されたチャットルームに自動的に接続する必要があります。
  2. 「送信」ボタンをクリックすると、チャットにメッセージを送信する必要があります。

すでにそれらのコードを実装したが、どこに配置すべきかわからないとします。イベントハンドラとエフェクトのどちらを使用すべきでしょうか?この質問に答える必要があるたびに、コードが実行される必要がある理由を考慮してください。

イベントハンドラは特定の操作に応答して実行される

ユーザーの観点から見ると、メッセージの送信は、特定の「送信」ボタンがクリックされたために発生するはずです。他のタイミングや他の理由でメッセージが送信されると、ユーザーはかなり不満を感じるでしょう。これが、メッセージの送信がイベントハンドラであるべき理由です。イベントハンドラを使用すると、特定の操作を処理できます:

イベントハンドラを使用すると、sendMessage(message)がユーザーがボタンを押した場合にのみ実行されることを確信できます。

エフェクトは同期が必要なときに実行される

コンポーネントをチャットルームに接続し続ける必要もあることを思い出してください。そのコードはどこに配置すべきでしょうか?

このコードを実行する理由は、特定のインタラクションではありません。ユーザーがどのように、あるいはなぜチャットルーム画面に移動したかは関係ありません。ユーザーがそれを見て、インタラクションできる状態にある今、コンポーネントは選択されたチャットサーバーに接続し続ける必要があります。たとえチャットルームコンポーネントがアプリの初期画面であり、ユーザーが何のインタラクションも行っていなかったとしても、それでも接続する必要があります。これが、これがエフェクトである理由です:

このコードにより、ユーザーが行った特定のインタラクションにかかわらず、現在選択されているチャットサーバーへのアクティブな接続が常に存在することが保証されます。ユーザーがアプリを開いただけの場合でも、別のルームを選択した場合でも、他の画面に移動して戻ってきた場合でも、あなたのエフェクトはコンポーネントが現在選択されているルームと同期し続けることを保証し、必要に応じて再接続します。

リアクティブな値とリアクティブなロジック

直感的には、イベントハンドラーは常に「手動」で、例えばボタンをクリックすることでトリガーされると言えます。一方、エフェクトは「自動的」です:同期を維持するために必要なだけ頻繁に実行および再実行されます。

これについて考える、より正確な方法があります。

コンポーネントの本体で宣言されたprops、state、変数はリアクティブな値と呼ばれます。この例では、serverUrlはリアクティブな値ではありませんが、roomIdmessageはリアクティブな値です。これらはレンダリングのデータフローに参加します:

このようなリアクティブな値は、再レンダリングによって変化する可能性があります。例えば、ユーザーがmessageを編集したり、ドロップダウンで別のroomIdを選択したりするかもしれません。イベントハンドラとエフェクトは、変化に対して異なる方法で応答します:

  • イベントハンドラ内のロジックはリアクティブではありません。ユーザーが同じ操作(例:クリック)を再度行わない限り、再度実行されることはありません。イベントハンドラは、リアクティブな値の変化に「反応」することなく、それらの値を読み取ることができます。
  • エフェクト内のロジックはリアクティブです。エフェクトがリアクティブな値を読み取る場合、それを依存関係として指定する必要があります。その後、再レンダリングによってその値が変化すると、Reactは新しい値を使用してエフェクトのロジックを再実行します。

この違いを説明するために、前の例を振り返ってみましょう。

イベントハンドラ内のロジックはリアクティブではありません

このコード行を見てください。このロジックはリアクティブであるべきでしょうか、それともそうではないでしょうか?

ユーザーの視点から見ると、messageが変化することは、メッセージを送信したいという意味ではありません。それは単にユーザーが入力していることを意味します。言い換えれば、メッセージを送信するロジックはリアクティブであるべきではありません。リアクティブな値が変化したからといって、再度実行されるべきではありません。そのため、それはイベントハンドラに属します:

イベントハンドラはリアクティブではないため、sendMessage(message)はユーザーが送信ボタンをクリックしたときにのみ実行されます。

エフェクト内のロジックはリアクティブです

では、これらの行に戻りましょう:

ユーザーの視点から見ると、roomIdが変化することは、別のルームに接続したいという意味です。言い換えれば、ルームへの接続ロジックはリアクティブであるべきです。あなたはこれらのコード行がリアクティブな値に「追従」し、その値が異なる場合に再度実行されることを望んでいます。そのため、それはエフェクトに属します:

エフェクトはリアクティブであるため、createConnection(serverUrl, roomId)connection.connect()roomIdの各異なる値に対して実行されます。あなたのエフェクトは、チャット接続を現在選択されているルームに同期させ続けます。

エフェクトから非リアクティブロジックを抽出する

リアクティブなロジックと非リアクティブなロジックを混在させたい場合、事態はより複雑になります。

例えば、ユーザーがチャットに接続したときに通知を表示したいと想像してください。正しい色で通知を表示できるように、プロップスから現在のテーマ(ダークまたはライト)を読み取ります:

しかし、themeはリアクティブな値(再レンダリングの結果として変化する可能性があります)であり、エフェクトによって読み取られるすべてのリアクティブな値は、その依存関係として宣言されなければなりません。今、あなたはエフェクトの依存関係としてthemeを指定する必要があります:

この例を試して、このユーザーエクスペリエンスの問題点を見つけられるかどうか確認してください:

予想通り、roomIdが変更されるとチャットは再接続されます。しかし、themeも依存関係に含まれているため、ダークテーマとライトテーマを切り替えるたびにチャット再接続されてしまいます。これは良くありません!

言い換えると、この行は(リアクティブである)Effectの内部にあるにもかかわらず、リアクティブであってほしくないのです:

この非リアクティブなロジックを、それを囲むリアクティブなEffectから分離する方法が必要です。

Effect Eventの宣言

特別なHookであるuseEffectEventを使用して、この非リアクティブなロジックをEffectから抽出します:

ここで、onConnectedEffect Eventと呼ばれます。これはEffectロジックの一部ですが、イベントハンドラーによく似た振る舞いをします。その内部のロジックはリアクティブではなく、常にpropsとstateの最新の値を「見る」ことができます。

これで、Effect内部からonConnectedEffect Eventを呼び出すことができます:

これで問題は解決します。注意点として、Effectの依存関係リストから削除themeする必要がありました。なぜなら、もはやEffect内で使用されていないからです。また、追加onConnectedする必要もありません。なぜなら、Effect Eventはリアクティブではなく、依存関係から除外しなければならないからです。

新しい動作が期待通りに機能することを確認してください:

Effect Eventは、イベントハンドラーと非常に似ていると考えることができます。主な違いは、イベントハンドラーがユーザーの操作に応じて実行されるのに対し、Effect EventはあなたがEffectからトリガーすることです。Effect Eventは、Effectのリアクティビティとリアクティブであるべきではないコードの間の「連鎖を断ち切る」ことを可能にします。

Effect Eventによる最新のpropsとstateの読み取り

Effect Eventは、依存関係リンターを抑制したくなるかもしれない多くのパターンを修正するのに役立ちます。

例えば、ページ訪問をログに記録するEffectがあるとします:

その後、サイトに複数のルートを追加します。これで、Pageコンポーネントは現在のパスを持つurlプロップを受け取ります。logVisit呼び出しの一部としてurlを渡したいのですが、依存関係リンターが警告を出します:

コードに何をさせたいかを考えてみてください。異なるURLは異なるページを表すので、異なるURLに対して別々の訪問を記録したいのです。言い換えれば、このlogVisit呼び出しはurlに対して反応的であるべきです。このため、このケースでは依存関係リンターに従い、urlを依存関係として追加することが理にかなっています:

次に、すべてのページ訪問にショッピングカート内のアイテム数も含めたいとします:

Effect内でnumberOfItemsを使用したので、リンターはそれを依存関係として追加するよう求めます。しかし、logVisit呼び出しがnumberOfItemsに対して反応的であることは望みません。ユーザーがショッピングカートに何かを入れ、numberOfItemsが変化しても、これはユーザーが再度ページを訪問したことを意味しません。言い換えれば、ページの訪問は、ある意味で「イベント」です。それは特定の瞬間に起こります。

コードを2つの部分に分割します:

ここで、onVisitはEffect Eventです。その内部のコードは反応的ではありません。このため、numberOfItems(または他の反応的な値!)を、周囲のコードが変更時に再実行されることを心配せずに使用できます。

一方、Effect自体は反応的のままです。Effect内のコードはurlプロップを使用するため、Effectは異なるurlでの再レンダリングごとに再実行されます。これにより、onVisitEffect Eventが呼び出されます。

結果として、urlが変化するたびにlogVisitを呼び出し、常に最新のnumberOfItemsを読み取ります。しかし、numberOfItemsが単独で変化しても、コードの再実行は引き起こされません。

注記

引数なしでonVisit()を呼び出し、その内部でurlを読み取ることもできるのではないかと思うかもしれません:

これは機能しますが、このurlをEffect Eventに明示的に渡す方が良いです。Effect Eventにurlを引数として渡すことで、異なるurlを持つページへの訪問は、ユーザーの視点から別々の「イベント」を構成すると言っているのです。 visitedUrlは、起こった「イベント」の一部です:

Effect Eventsの制限

Effect Eventsは、その使用方法に非常に制限があります:

  • Effect内部からのみ呼び出してください。
  • 他のコンポーネントやフックに渡さないでください。

例えば、以下のようにEffect Eventを宣言して渡すことは避けてください:

代わりに、Effect Eventsは常にそれを使用するEffectの直近で宣言してください:

Effect Eventsは、Effectコードの非リアクティブな「断片」です。それらを使用するEffectの近くに配置する必要があります。

まとめ

  • イベントハンドラは、特定のインタラクションに応答して実行されます。
  • Effectは、同期が必要なときに実行されます。
  • イベントハンドラ内のロジックはリアクティブではありません。
  • Effect内のロジックはリアクティブです。
  • Effectから非リアクティブなロジックをEffect Eventsに移動できます。
  • Effect EventsはEffect内部からのみ呼び出してください。
  • Effect Eventsを他のコンポーネントやフックに渡さないでください。

Try out some challenges

Challenge 1 of 4:Fix a variable that doesn’t update #

This Timer component keeps a count state variable which increases every second. The value by which it’s increasing is stored in the increment state variable. You can control the increment variable with the plus and minus buttons.

However, no matter how many times you click the plus button, the counter is still incremented by one every second. What’s wrong with this code? Why is increment always equal to 1 inside the Effect’s code? Find the mistake and fix it.