From aaf029bbc94f14b2c906571d25528be250813ba3 Mon Sep 17 00:00:00 2001 From: Soichiro Miki Date: Wed, 10 Jan 2024 17:45:00 +0900 Subject: [PATCH] Refine "Separating Events from Effects" --- .../learn/separating-events-from-effects.md | 182 +++++++++--------- src/sidebarLearn.json | 2 +- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/src/content/learn/separating-events-from-effects.md b/src/content/learn/separating-events-from-effects.md index 32343a191..79ae5679a 100644 --- a/src/content/learn/separating-events-from-effects.md +++ b/src/content/learn/separating-events-from-effects.md @@ -1,16 +1,16 @@ --- -title: 'イベントとエフェクトを切り離す' +title: 'エフェクトからイベントを分離する' --- -イベントハンドラは同じユーザ操作を再度実行した場合のみ再実行されます。エフェクトはイベントハンドラとは異なり、props や state 変数のようなそれが読み取る値が前回のレンダー時の値と異なる場合に再同期を行います。また、ある値には反応して再実行するが、他の値には反応しないエフェクトなど、両方の動作をミックスさせたい場合もあります。このページでは、その方法を説明します。 +イベントハンドラは、ユーザが同じ操作を繰り返した場合にのみ再実行されます。エフェクトはイベントハンドラとは異なり、props や state 変数のようなそれが読み取る値が前回のレンダー時と異なる場合に、再同期を行います。また両方の動作をミックスさせて、ある値には反応して再実行されるが他の値には反応しないというエフェクトが欲しくなる場合もあります。このページでは、その方法を説明します。 -- イベントハンドラとエフェクトの選択方法 +- イベントハンドラとエフェクトのどちらを選ぶか - エフェクトがリアクティブで、イベントハンドラがリアクティブでない理由 - エフェクトのコードの一部をリアクティブにしたくない場合の対処法 - エフェクトイベントとは何か、そしてエフェクトイベントからエフェクトを抽出する方法 @@ -20,18 +20,18 @@ title: 'イベントとエフェクトを切り離す' ## イベントハンドラとエフェクトのどちらを選ぶか {/*choosing-between-event-handlers-and-effects*/} -まず、イベントハンドラとエフェクトの違いについておさらいしましょう。 +まず、イベントハンドラとエフェクトの違いについておさらいしておきましょう。 -チャットルームのコンポーネントを実装している場合を想像してください。要件は次のようなものです: +チャットルームのコンポーネントを実装している場合を想像してください。要件は次のようなものです。 -1. コンポーネントは選択されたチャットルームに自動的に接続する -2. "Send" ボタンをクリックすると、チャットにメッセージが送信される +1. コンポーネントは選択中のチャットルームに自動的に接続する。 +2. "Send" ボタンをクリックすると、チャットにメッセージが送信される。 -そのためのコードはすでに実装されているが、それをどこに置くか迷っているとしましょう。イベントハンドラを使うべきか、エフェクトを使うべきか。この質問に答える必要がある場合は常に、[*なぜ*そのコードが実行される必要があるのかを考えてみてください。](/learn/synchronizing-with-effects#what-are-effects-and-how-are-they-different-from-events) +このためのコードはすでに実装されているが、それをどこに置くか迷っているとしましょう。イベントハンドラを使うべきか、エフェクトを使うべきか。このような質問に答える必要がある場合は常に、[*なぜ*そのコードを実行する必要があるのか](/learn/synchronizing-with-effects#what-are-effects-and-how-are-they-different-from-events)を考えるようにしてください。 ### イベントハンドラは具体的なユーザ操作に反応して実行される {/*event-handlers-run-in-response-to-specific-interactions*/} -ユーザの立場からすると、メッセージの送信は、特定の "Send" ボタンがクリックされたから起こるはずです。それ以外のタイミングや理由でメッセージを送信すると、むしろユーザは怒るでしょう。そのため、メッセージの送信はイベントハンドラで行う必要があります。イベントハンドラを使えば、特定のユーザ操作を処理することができます: +ユーザの立場からすると、メッセージの送信とは、"Send" という特定のボタンがクリックされたから起こるべきものです。それ以外のタイミングや理由でメッセージが送信されてしまうとユーザは怒ることでしょう。これが、メッセージの送信はイベントハンドラで行うべき理由です。イベントハンドラを使うことで、特定のユーザ操作を処理できます。 ```js {4-6} function ChatRoom({ roomId }) { @@ -50,13 +50,13 @@ function ChatRoom({ roomId }) { } ``` -イベントハンドラを使えば、ユーザがボタンを押したときだけ `sendMessage(message)` が実行されるようにすることができます。 +イベントハンドラを使うことにより、ユーザがボタンを押したときに*だけ* `sendMessage(message)` が実行される、ということが保証されるのです。 -### エフェクトは同期が必要なときに実行される {/*effects-run-whenever-synchronization-is-needed*/} +### エフェクトは同期が必要なときに常に実行される {/*effects-run-whenever-synchronization-is-needed*/} -また、コンポーネントをチャットルームに接続しておく必要があることを思い出してください。そのコードはどこに記述されるのでしょうか? +コンポーネントはチャットルームへの接続を保持する必要がある、という要件を思い出しましょう。そのコードはどこに記述すべきでしょうか? -このコードを実行する*理由*は、何か特定のユーザ操作ではありません。ユーザがなぜ、どのようにチャットルームの画面に移動したかは問題ではありません。ユーザがチャットルームの画面を見てそれを操作できるようになった以上、このコンポーネントは、選択されたチャットサーバに接続されたままである必要があります。チャットルームコンポーネントがアプリの初期画面であり、ユーザが何の操作も行っていない場合でも、*やはり*接続する必要があります。これがエフェクトである理由です: +そのコードを実行する*理由*は、何か特定のユーザ操作ではありません。ユーザがチャットルーム画面に移動した理由や方法が問題なのではありません。ユーザがチャットルームの画面を見てそれを操作できるようになった以上、このコンポーネントは選択されたチャットサーバへの接続を維持する必要があります。チャットルームコンポーネントがアプリの初期画面で、ユーザが何の操作も行っていない場合であっても、*やはり*接続が必要です。これが、エフェクトを使用する理由です。 ```js {3-9} function ChatRoom({ roomId }) { @@ -72,7 +72,7 @@ function ChatRoom({ roomId }) { } ``` -このコードを使用すると、ユーザが行った特定の操作に*関係なく*、現在選択されているチャットサーバへの接続が常にアクティブであると確信することができます。ユーザがアプリを開いただけの場合でも、別のルームを選んだ場合でも、別の画面に移動して戻ってきた場合でも、このエフェクトはコンポーネントが現在選択されているルームと同期していることを保証し、[必要なときはいつでも再接続するようにします。](/learn/lifecycle-of-reactive-effects#why-synchronization-may-need-to-happen-more-than-once) +このコードにより、ユーザが行った特定の操作とは*関係なく*、現在選択されているチャットサーバへの接続が常にアクティブであると確信することができます。ユーザがアプリを開いただけの場合でも、別のルームを選んだ場合でも、他の画面に移動して戻ってきた場合でも、このエフェクトによりコンポーネントが現在選択されているルームと同期し、[必要なとき常に再接続を行う](/learn/lifecycle-of-reactive-effects#why-synchronization-may-need-to-happen-more-than-once)ことが保証されます。 @@ -156,11 +156,11 @@ input, select { margin-right: 20px; } ## リアクティブな値とリアクティブなロジック {/*reactive-values-and-reactive-logic*/} -直感的に言うと、イベントハンドラは、例えばボタンをクリックするなど、常に「手動」でトリガされます。一方、エフェクトは「自動」であり、同期を保つために必要な回数だけ実行され、再実行されます。 +直感的にはイベントハンドラとは、例えばボタンをクリックするなど、常に「手動」でトリガされるものだと言えるでしょう。一方、エフェクトは「自動」であり、同期を保つために必要なだけ実行・再実行されます。 しかし、もっと正確な考え方があります。 -コンポーネントの本体部分で宣言された props、state、変数をリアクティブな値 (reactive value) と呼びます。この例では、`serverUrl` はリアクティブ値ではありませんが、`roomId` と `message` はリアクティブな値です。これらは、レンダーのデータフローに参加しています: +コンポーネントの本体部分で宣言された props、state、変数のことをリアクティブな値 (reactive value) と呼びます。この例では、`serverUrl` はリアクティブな値ではありませんが、`roomId` と `message` はリアクティブな値です。これらはレンダーのデータフローに関わる値です。 ```js [[2, 3, "roomId"], [2, 4, "message"]] const serverUrl = 'https://localhost:1234'; @@ -172,16 +172,16 @@ function ChatRoom({ roomId }) { } ``` -これらのようなリアクティブな値は、再レンダーによって変更される可能性があります。例えば、ユーザが `message` を編集したり、ドロップダウンで別の `roomId` を選択することがあります。イベントハンドラとエフェクトは、それぞれ異なる方法で変化に対応します: +このようなリアクティブな値は、再レンダー時に変化する可能性があります。例えばユーザは `message` を編集したり、ドロップダウンで別の `roomId` を選択したりするかもしれません。イベントハンドラとエフェクトは、それぞれ異なる方法で変化に対応します。 -- **イベントハンドラ内のロジックはリアクティブではない**。ユーザが同じ操作(クリックなど)を再度行わない限り、再度実行されることはありません。イベントハンドラは、その変更に「反応」することなく、リアクティブな値を読み取ることができます。 -- **エフェクト内のロジックはリアクティブである**。エフェクトがリアクティブな値を読み取る場合、[依存配列としてそれを指定する必要があります。](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)そして、再レンダーによってその値が変更された場合、React は新しい値でエフェクトのロジックを再実行します。 +- **イベントハンドラ内のロジックはリアクティブではない**。ユーザが同じ操作(クリックなど)を再度行わない限り、再度実行されることはありません。イベントハンドラは、値の変化に「反応」することなく、リアクティブな値を読み取ることができます。 +- **エフェクト内のロジックはリアクティブである**。エフェクトがリアクティブな値を読み取る場合、[依存配列としてそれを指定する必要があります](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)。その後再レンダーによって値が変化した場合、React は新しい値でエフェクトのロジックを再実行します。 -この違いを説明するために、先ほどの例をもう一度見てみましょう。 +この違いを理解するため、先ほどの例をもう一度見てみましょう。 ### イベントハンドラ内のロジックはリアクティブではない {/*logic-inside-event-handlers-is-not-reactive*/} -コードのこの行を見てみてください。このロジックはリアクティブであるべきでしょうか、そうではないでしょうか? +このコードをご覧ください。このロジックはリアクティブであるべきでしょうか、そうではないでしょうか? ```js [[2, 2, "message"]] // ... @@ -189,7 +189,7 @@ function ChatRoom({ roomId }) { // ... ``` -ユーザの観点からは、**`message` が変化することがメッセージを送りたいという意味には*なりません***。あくまでも、ユーザが入力していることを意味します。つまり、メッセージを送るロジックはリアクティブであってはならないのです。リアクティブな値が変わったからと言って、再び実行されるべきではないのです。だから、イベントハンドラの中にあるのです: +ユーザの観点からは、**`message` が変化することがメッセージを送りたいという意味になるわけでは*ありません***。あくまでユーザが入力の最中だということでしかありません。つまり、メッセージ送信のロジックは、リアクティブであってはならないということです。リアクティブな値が変わったからと言って、再び実行されるべきではありません。したがって、これはイベントハンドラの中にあるべきです。 ```js {2} function handleSendClick() { @@ -201,7 +201,7 @@ function ChatRoom({ roomId }) { ### エフェクト内のロジックはリアクティブである {/*logic-inside-effects-is-reactive*/} -では、この行に戻りましょう: +こちらの行に戻りましょう。 ```js [[2, 2, "roomId"]] // ... @@ -210,7 +210,7 @@ function ChatRoom({ roomId }) { // ... ``` -ユーザの観点からは、**`roomId` が変化することは、別の部屋に接続したいことを意味します**。つまり、ルームに接続するためのロジックはリアクティブであるべきなのです。これらのコードがリアクティブな値に「ついていける」ようにし、その値が異なる場合は再度実行されるように*したい*のです。だから、エフェクトの中にあるのです: +ユーザの観点からは、**`roomId` が変化することは、別の部屋に接続したいことを意味します**。つまり、ルームに接続するためのロジックもリアクティブであるべきだ、ということです。コードがリアクティブな値の変化に「キャッチアップ」するようにし、値が変われば再度実行されるように*したい*のです。したがってこれはエフェクトの中にあるべきです。 ```js {2-3} useEffect(() => { @@ -222,13 +222,13 @@ function ChatRoom({ roomId }) { }, [roomId]); ``` -エフェクトはリアクティブなので、`createConnection(serverUrl, roomId)` と `connection.connect()` は、`roomId` の値が変わるごとに実行されます。エフェクトは、チャット接続が現在選択されているルームに同期された状態を維持します。 +エフェクトはリアクティブなので、`createConnection(serverUrl, roomId)` と `connection.connect()` は、`roomId` の値が変わるごとに実行されます。エフェクトにより、チャットの接続が選択中のルームに同期された状態が維持されます。 -## エフェクトから非リアクティブなロジックを抽出する {/*extracting-non-reactive-logic-out-of-effects*/} +## エフェクトから非リアクティブなロジックを分離する {/*extracting-non-reactive-logic-out-of-effects*/} -リアクティブなロジックと非リアクティブなロジックを混在させる場合は、やや厄介なことになります。 +リアクティブなロジックと非リアクティブなロジックを混在させたい場合、少し厄介なことになります。 -例えば、ユーザがチャットに接続したときに通知を表示したいとします。正しい色で通知を表示することができるよう、props から現在のテーマ(ダークまたはライト)を読み取ります。 +例えば、ユーザがチャットに接続したときに通知を表示したいとします。正しい色で通知を表示することができるよう、props から現在のテーマ(ダークまたはライト)を読み取ることにしましょう。 ```js {1,4-6} function ChatRoom({ roomId, theme }) { @@ -241,7 +241,7 @@ function ChatRoom({ roomId, theme }) { // ... ``` -しかし、`theme` はリアクティブな値であり(再レンダーの結果として変化する可能性がある)、[エフェクトが読み取るすべてのリアクティブな値は、依存値として宣言する必要があります。](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency)そこで、エフェクトの依存配列として `theme` を指定する必要があります: +しかし、`theme` は(再レンダーの結果として変化する可能性があるので)リアクティブな値であり、そして[エフェクトが読み取るすべてのリアクティブな値は依存値として宣言する必要があります](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency)。というわけで `theme` はエフェクトの依存配列として指定しないといけません。 ```js {5,11} function ChatRoom({ roomId, theme }) { @@ -258,7 +258,7 @@ function ChatRoom({ roomId, theme }) { // ... ``` -以下の例をいじってみて、ユーザエクスペリエンスに問題点を見つけることができますか? +以下の例をいじってみてください。ユーザエクスペリエンスの問題点が分かりますか? @@ -386,9 +386,9 @@ label { display: block; margin-top: 10px; } -`roomId` が変わると、期待通りチャットが再接続されます。しかし、`theme` も依存値であるため、ダークテーマとライトテーマを切り替えることでも毎回チャットが再接続されます。これはあまり良くないですね! +`roomId` が変わるとチャットが再接続され、それは期待通りの動作です。しかし、`theme` も依存値であるため、ダークテーマとライトテーマを切り替えることでも毎回チャットが再接続されてしまっています。これはあまり良くありません! -つまり、この行は(リアクティブである)エフェクトの中にあるにもかかわらず、リアクティブであってほしくないということです: +つまり、以下の行は(リアクティブである)エフェクトの中にあるにもかかわらず、リアクティブであってほしくないということです。 ```js // ... @@ -396,7 +396,7 @@ label { display: block; margin-top: 10px; } // ... ``` -この非リアクティブなロジックと、その周りのリアクティブなエフェクトを切り離す方法が必要です。 +この非リアクティブなロジックを、周囲にあるリアクティブなエフェクトのコードから分離する方法が必要です。 ### エフェクトイベントの宣言 {/*declaring-an-effect-event*/} @@ -406,7 +406,7 @@ label { display: block; margin-top: 10px; } -[`useEffectEvent`](/reference/react/experimental_useEffectEvent) という特別なフックを使って、エフェクトからこの非リアクティブなロジックを抽出します: +[`useEffectEvent`](/reference/react/experimental_useEffectEvent) という特別なフックを使うことで、エフェクトからこの非リアクティブなロジックを分離することができます。 ```js {1,4-6} import { useEffect, useEffectEvent } from 'react'; @@ -418,9 +418,9 @@ function ChatRoom({ roomId, theme }) { // ... ``` -ここでは、`onConnected` は*エフェクトイベント (Effect Event)* と呼ばれています。これはエフェクトロジックの一部ですが、イベントハンドラにより近い動作をします。この中のロジックはリアクティブではなく、常に props と state の最新の値を「見る」ことができます。 +ここで、`onConnected` は*エフェクトイベント (Effect Event)* と呼ばれるものです。これはエフェクトロジックの一部でありながら、むしろイベントハンドラに近い動作をします。この中のロジックはリアクティブではなく、常に props と state の最新の値を「見る」ことができます。 -これでエフェクトの内部から `onConnected` エフェクトイベントを呼び出せるようになりました: +これで、エフェクトの内部から `onConnected` エフェクトイベントを呼び出せるようになります。 ```js {2-4,9,13} function ChatRoom({ roomId, theme }) { @@ -439,9 +439,9 @@ function ChatRoom({ roomId, theme }) { // ... ``` -これで問題は解決しました。なお、エフェクトの依存値のリストから `onConnected` を削除する必要がありました。**エフェクトイベントはリアクティブではないので、依存配列から除外する必要があります。** +これで問題は解決しました。なお、エフェクトの依存値のリストに `onConnected` を入れてはいけません。**エフェクトイベント自体はリアクティブではないので、依存配列から除外する必要があります**。 -新しい動作が期待通りに振舞うことを確認してください: +新しい動作が期待通りであることを確認してください。 @@ -574,9 +574,9 @@ label { display: block; margin-top: 10px; } -エフェクトイベントは、イベントハンドラと非常に似ていると考えることができます。主な違いは、イベントハンドラがユーザの操作に反応して実行されるのに対し、エフェクトイベントはエフェクトからトリガされることです。エフェクトイベントを使うことで、リアクティブであるエフェクトと、リアクティブであってはならないコードとの間の「連鎖を断ち切る」ことができます。 +エフェクトイベントは、イベントハンドラと非常に似たものだと考えることができます。主な違いは、イベントハンドラがユーザの操作に反応して実行されるのに対し、エフェクトイベントはエフェクトからトリガされることです。エフェクトイベントを使うことで、リアクティブであるエフェクトと、リアクティブであってはならないコードとの間の「連鎖を断ち切る」ことができます。 -### エフェクトイベントで最新の props や state を取得する {/*reading-latest-props-and-state-with-effect-events*/} +### エフェクトイベントで最新の props や state を読み取る {/*reading-latest-props-and-state-with-effect-events*/} @@ -584,9 +584,9 @@ label { display: block; margin-top: 10px; } -エフェクトイベントによって、依存性リンタを抑制したくなるような多くのパターンを修正することができます。 +依存値に関するリンタを抑制したくなるようなパターンの多くは、エフェクトイベントによって回避可能です。 -例えば、ページの訪問を記録するエフェクトがあるとします: +例えば、ページへの訪問をログに記録するエフェクトがあるとしましょう。 ```js function Page() { @@ -597,7 +597,7 @@ function Page() { } ``` -その後、サイトに複数のページを追加するとします。そこで、`Page` コンポーネントは現在のパスを持つ `url` プロパティを受け取ります。この `url` を `logVisit` 呼び出しの一部として渡したいのですが、依存値リンタが文句を言ってきます: +後になって、サイトに複数のページを追加することになりました。`Page` コンポーネントは現在のパスを表す `url` を props として受け取るようになります。この `url` を `logVisit` コールに渡そうと思うのですが、そこで依存値リンタが文句を言ってきます。 ```js {1,3} function Page({ url }) { @@ -608,7 +608,7 @@ function Page({ url }) { } ``` -コードに何をさせたいか考えてみてください。各 URL は異なるページを表しているので、異なる URL に対して別々の訪問を記録*したいのです*。言い換えれば、この `logVisit` 呼び出しは、`url` に関してリアクティブで*なければなりません*。このため、この場合は、依存値のリンタに従って、`url` を依存配列に追加することが理にかなっています: +コードに何をさせたいのか考えてみましょう。それぞれの URL は別々のページを表しているので、*やりたいこと*はそれぞれの URL に対して別々に訪問ログを記録することです。言い換えれば、この `logVisit` の呼び出しは、`url` に関して確かに*リアクティブであるべき*ですね。したがってこの場合、依存値リンタが言う通り、`url` を依存配列に追加することは理にかなっています。 ```js {4} function Page({ url }) { @@ -619,7 +619,7 @@ function Page({ url }) { } ``` -ここで、個々のページ訪問ログにショッピングカート内にある商品数も含めたくなったとしましょう。 +ところがここで、個々のページ訪問ログに、ショッピングカート内にある商品の数も含めたくなったとしましょう。 ```js {2-3,6} function Page({ url }) { @@ -633,9 +633,9 @@ function Page({ url }) { } ``` -あなたはエフェクトの中で `numberOfItems` を使用したので、リンタは依存値としてそれを追加するように求めます。しかし、`logVisit` の呼び出しが `numberOfItems` に対してリアクティブであることは*望ましくありません*。ユーザがショッピングカートに何かを入れて、`numberOfItems` が変化しても、それはユーザが再びページを訪れたことを*意味しません*。つまり、*ページを訪れた*ということは、ある意味で「イベント」なのです。ある瞬間に起こるのです。 +エフェクト内で `numberOfItems` を使用したので、リンタは依存値としてそれを追加するように言ってきます。しかし、`logVisit` の呼び出しが `numberOfItems` に対してリアクティブであることは*望ましくありません*。ユーザがショッピングカートに何かを入れて、`numberOfItems` が変化しても、それはユーザが再びページを訪れたことを*意味しません*。つまり、*ページを訪れた*ということは、ある意味で「イベント」なのです。ある特定の瞬間に起こるのです。 -コードを 2 つに分割してみましょう: +このコードを 2 つに分割しましょう。 ```js {5-7,10} function Page({ url }) { @@ -655,13 +655,13 @@ function Page({ url }) { ここで、`onVisit` はエフェクトイベントです。この中のコードはリアクティブではありません。このため、`numberOfItems`(または他のリアクティブな値!)を使用しても、変更時に周囲のコードが再実行される心配はありません。 -一方、エフェクトそのものはリアクティブなままです。エフェクトの中のコードは `url` プロパティを使用するので、異なる `url` で再レンダーするたびにエフェクトが再実行されます。次にそれが `onVisit` エフェクトイベントを呼び出します。 +一方、エフェクトそのものはリアクティブなままです。エフェクトの中のコードは props である `url` を使用しているため、異なる `url` で再レンダーが起きるたびにエフェクトが再実行されます。次にそれが `onVisit` エフェクトイベントを呼び出します。 -その結果、`url` の変更ごとに `logVisit` が呼び出され、常に最新の `numberOfItems` を読み取ることになります。ただし、`numberOfItems` が独自に変化しても、コードの再実行には至りません。 +その結果、`url` に変化があるごとに `logVisit` が呼び出され、常に最新の `numberOfItems` が読み取れることになります。一方で `numberOfItems` だけが変化してもコードの再実行は起きません。 -`onVisit()` は引数なしで呼び出して、関数内から直に `url` を読み取ればいいのでは、と疑問に思うかもしれません: +`onVisit()` は引数なしで呼び出して、関数内から直に `url` を読み取ればいいのでは、と疑問に思うかもしれません。 ```js {2,6} const onVisit = useEffectEvent(() => { @@ -673,7 +673,7 @@ function Page({ url }) { }, [url]); ``` -これでもいいのですが、この `url` を明示的にエフェクトイベントに渡す方がいいでしょう。**エフェクトイベントの引数として `url` を渡すことで、異なる `url` を持つページを訪問することが、ユーザの視点から見ると別の「イベント」を構成していると伝えることになります。**`visitedUrl` は、起こった「イベント」の*一部*なのです: +これでも動作はするのですが、この `url` は明示的にエフェクトイベントに渡す方がいいでしょう。**エフェクトイベントの引数として `url` を渡すことにより、異なる `url` のページを訪問することがユーザの視点から見ると別の「イベント」を構成しているのだ、という意図を表現できます**。`visitedUrl` は、起こった「イベント」の*一部*なのです。 ```js {1-2,6} const onVisit = useEffectEvent(visitedUrl => { @@ -685,9 +685,9 @@ function Page({ url }) { }, [url]); ``` -エフェクトイベントで `visitedUrl` を明示的に「要求」するので、エフェクトの依存配列から誤って `url` を削除することができなくなりました。もし、`url` の依存値を削除してしまうと(別々のページへの訪問が 1 つとしてカウントされてしまう)、リンタはそれについて警告を発します。`onVisit` は `url` に関してリアクティブであってほしいのですから、`url` を内部で読み込む(そうするとリアクティブでなくなってしまう)のではなく、エフェクト*から*それを渡しましょう。 +エフェクトイベントが `visitedUrl` を明示的に「要求」しているので、エフェクト側の依存配列から誤って `url` を削除することができなくなりました。もし依存配列から `url` を削除してしまうと(別々のページへの訪問が 1 つとしてカウントされてしまう)、リンタはそれについて警告を発します。`onVisit` は `url` に対してはリアクティブであってほしいのですから、`url` はイベント内で読み込む(そうするとリアクティブでなくなってしまう)のではなく、*エフェクトから*渡すようにしましょう。 -これは、エフェクトの中に非同期のロジックがある場合に特に重要になります: +これは、エフェクトの中に非同期のロジックがある場合に特に重要になります。 ```js {6,8} const onVisit = useEffectEvent(visitedUrl => { @@ -701,15 +701,15 @@ function Page({ url }) { }, [url]); ``` -この場合、`onVisit` 内で `url` を読み取ると*最新*の `url`(既に変更されている可能性がある)を読み取ることに対応し、`visitedUrl` はこのエフェクト(およびこの `onVisit` コール)が実行される大元のきっかけとなった `url` に対応することになります。 +この場合、`onVisit` 内で `url` を読み取ると(既に別物に変わっている可能性がある)*最新*の `url` を読み取ってしまいますが、`visitedUrl` はこのエフェクト(およびこの `onVisit` コール)が実行される大元のきっかけとなった `url` に対応することになります。 -#### 代わりに依存値リンタを止めてもいいか? {/*is-it-okay-to-suppress-the-dependency-linter-instead*/} +#### 代わりに依存値リンタを止めても大丈夫? {/*is-it-okay-to-suppress-the-dependency-linter-instead*/} -既存のコードベースでは、このようにリントルールが抑制されているのを見かけることがあります: +既存のコードベースで、以下のようにリントルールが抑制されているのを見かけることがあるかもしれません。 ```js {7-9} function Page({ url }) { @@ -725,11 +725,11 @@ function Page({ url }) { } ``` -`useEffectEvent` が React の安定した一部となった後、**決してリンタを抑制しない**ことをお勧めします。 +`useEffectEvent` が React の安定版に含まれるようになった後は、**決してリンタを抑制しない**ことをお勧めします。 -ルールを抑制することの最初の欠点は、コードに導入した新しいリアクティブな依存値にエフェクトが「反応する」必要があるときに、React が警告を発しなくなることです。先ほどの例では、依存配列に `url` を追加したのは、React がそれをするよう思い出させてくれたからです。リンタを無効にすると、今後そのエフェクトを編集する際に、そのようなリマインダを受け取ることができなくなります。これはバグにつながります。 +このルールを止めてしまうことの最大の欠点は、新たにコードにリアクティブな依存値を追加してそれにエフェクトが「反応する」必要がある場合でも、もはや React が警告を表示できなくなってしまうことです。先ほどの例でも、`url` を依存配列に追加し忘れずに済んだのは、そうするよう React が教えてくれていたからでしたね。リンタを無効化してしまうと、今後そのエフェクトを編集する際に、そのようなリマインダを受け取ることができなくなります。これはバグにつながります。 -以下は、リンタを抑制することで発生する紛らわしいバグの一例です。この例では、`handleMove` 関数は、ドットがカーソルに従うべきかどうかを決定するために、現在の `canMove` state 変数の値を読むことになっています。しかし、`handleMove` の内部では `canMove` は常に `true` です。 +以下は、リンタを無効化することで発生する紛らわしいバグの一例です。この例では、`handleMove` 関数は、ドットがカーソルに従うべきかどうかを決定するために、`canMove` という state 変数の現在値を読み取ろうとしています。しかし `handleMove` の内部では `canMove` は常に `true` となります。 なぜかわかりますか? @@ -790,13 +790,13 @@ body { -このコードの問題は、依存性リンタを無効化してしまっていることです。それを解除すると、このエフェクトは `handleMove` 関数に依存する必要があることがわかります。これは理にかなっています。なぜならば、`handleMove` はコンポーネント本体の内部で宣言されているのでリアクティブな値だからです。すべてのリアクティブな値は依存値として指定されなければなりませんし、さもなくば時間の経過とともに古くなってしまう可能性があります! +このコードの問題は、依存値リンタを無効化してしまっていることです。無効化のコメントを外すと、このエフェクトの依存値として `handleMove` 関数を含める必要があることがわかります。これは理にかなっています。なぜなら `handleMove` はコンポーネント本体の内部で宣言されているのでリアクティブな値だからです。すべてのリアクティブな値は依存値として指定されなければなりませんし、さもなくば時間の経過とともに古くなってしまう可能性があります! -元のコードを書いた人は、React に対して「このエフェクトはどのリアクティブ値にも依存しない (`[]`)」と「嘘」をついています。そのため、React は `canMove`(とそれを使う `handleMove`)が変更された後にエフェクトを再同期させなかったのです。React はエフェクトを再同期しなかったため、リスナとしてアタッチされる `handleMove` は、初回レンダー時に作成された `handleMove` 関数となります。初回レンダー時には `canMove` は `true` であったため、初回レンダー時の `handleMove` は永遠にその値を見ることになります。 +元のコードを書いた人は、React に対して「このエフェクトはどのリアクティブな値にも依存しない (`[]`)」と「嘘」をついています。だから React は `canMove`(とそれを使う `handleMove`)が変化したのにエフェクトを再同期しなかったのです。React はエフェクトを再同期しなかったため、リスナとしてアタッチされる `handleMove` は、初回レンダー時に作成された `handleMove` 関数のままとなります。初回レンダー時に `canMove` は `true` であったため、同時に作られた `handleMove` からも永遠にその値が見え続けることになります。 -**リンタを抑制することがなければ、値が古くなることに関する問題が発生することはありません。** +**リンタを決して抑制しないようにすれば、値が古くなることに関する問題が発生することはありません**。 -`useEffectEvent` を使えば、リンタに「嘘」をつく必要はなく、期待通りにコードが動きます: +`useEffectEvent` を使えば、リンタに「嘘」をつく必要はなく、期待通りにコードが動きます。 @@ -870,13 +870,13 @@ body { -これは、`useEffectEvent` が*常に*正しい解決策であることを意味するものではありません。コードのリアクティブにしたくない行にのみ適用する必要があります。上記のサンドボックスでは、エフェクトのコードが `canMove` に関してリアクティブであることを望んでいませんでした。そのため、エフェクトイベントを抽出することが理にかなっています。 +だからといって `useEffectEvent` が*常に*正しい解決策だというわけではありません。コード中の、リアクティブにしたくない行にだけ適用するようにしてください。上記のサンドボックスでは、エフェクトコードが `canMove` に関してはリアクティブであってほしくなかったため、エフェクトイベントとして抜き出すことが理にかなっていたのです。 -エフェクトを無効化しないで済む他の方法については、[エフェクトから依存値を取り除く](/learn/removing-effect-dependencies)を参照してください。 +リンタを無効化しないで済む他の方法については、[エフェクトから依存値を取り除く](/learn/removing-effect-dependencies)を参照してください。 -### エフェクトイベントの制限について {/*limitations-of-effect-events*/} +### エフェクトイベントに関する制限事項 {/*limitations-of-effect-events*/} @@ -884,10 +884,10 @@ body { -エフェクトイベントは、使い方が非常に限定されています: +エフェクトイベントは、使い方が非常に限定されています。 -* **エフェクトの内部からしか呼び出すことができません。** -* **他のコンポーネントやフックに渡してはいけません。** +* **エフェクトの内部からしか呼び出すことができません**。 +* **他のコンポーネントやフックに渡してはいけません**。 例えば、次のようにエフェクトイベントを宣言して渡さないでください: @@ -916,7 +916,7 @@ function useTimer(callback, delay) { } ``` -その代わりに、常にエフェクトイベントを使用するエフェクトのすぐ隣で宣言してください: +こうではなく、常にエフェクトイベントを使用するエフェクトのすぐ隣で宣言してください。 ```js {10-12,16,21} function Timer() { @@ -943,7 +943,7 @@ function useTimer(callback, delay) { } ``` -エフェクトイベントは、エフェクトのコード中にある反応しない「パーツ」です。それを使用するエフェクトの隣に置く必要があります。 +エフェクトイベントとは、エフェクトコードを構成する「パーツ」のうちの非リアクティブな部分です。それを使用するエフェクトの隣に置くようにしましょう。 @@ -961,9 +961,9 @@ function useTimer(callback, delay) { #### 更新されない変数を修正 {/*fix-a-variable-that-doesnt-update*/} -この `Timer` コンポーネントは、1 秒ごとに値が増加する `count` state 変数を保持します。値をいくつ増加させるのかは、`increment` state 変数に格納されます。プラスボタンとマイナスボタンで `increment` 変数を制御できます。 +この `Timer` コンポーネントは、1 秒ごとに値が増加する `count` という state 変数を保持しています。値をいくつ増加させるのかは、`increment` という state 変数に格納されます。プラスボタンとマイナスボタンで `increment` 変数を制御できます。 -しかし、プラスボタンを何度クリックしても、カウンタは 1 秒ごとに 1 つずつ増えていきます。このコードの何が問題なのでしょうか? なぜエフェクトのコード内部では `increment` が常に 1 に等しいのでしょうか? 間違いを見つけて修正しましょう。 +しかし、プラスボタンを何度クリックしても、カウンタは 1 秒ごとに 1 つずつ増えていきます。このコードの何が問題なのでしょうか? なぜエフェクトのコード内部では `increment` が常に 1 になっているのでしょうか? 間違いを見つけて修正しましょう。 @@ -1020,9 +1020,9 @@ button { margin: 10px; } -例によって、エフェクトのバグを探すときは、リンタを抑制している箇所を探すことから始めてください。 +例によって、エフェクトのバグを探すときは、リンタを無効化している箇所を探すことから始めてください。 -抑制コメントを削除すると、React はこのエフェクトのコードが `increment` に依存していることを教えてくれますが、このエフェクトはリアクティブ値に依存していない (`[]`) と主張することで React に「嘘をついた」のです。依存配列に `increment` を追加します: +無効化コメントを削除すると、React はこのエフェクトのコードが `increment` に依存していることを教えてくれます。このエフェクトはリアクティブな値に依存していない (`[]`) と主張することで React に「嘘をついた」のです。依存配列に `increment` を追加しましょう。 @@ -1070,15 +1070,15 @@ button { margin: 10px; } -これで `increment` が変わると、React はエフェクトを再同期し、インターバルを再開します。 +これで `increment` が変わると、React はエフェクトを再同期し、インターバルも再スタートされます。 #### カウンタのフリーズを修正 {/*fix-a-freezing-counter*/} -この `Timer` コンポーネントは、1 秒ごとに増加する `count` state 変数を保持します。いくつずつ増加するのかという値は `increment` state 変数に格納され、プラスとマイナスのボタンでコントロールすることができます。例えば、プラスボタンを 9 回押してみると、1 秒ごとにカウントが 1 ずつではなく、10 ずつ増えていくことがわかります。 +この `Timer` コンポーネントは、1 秒ごとに値が増加する `count` という state 変数を保持しています。値をいくつ増加させるのかは `increment` という state 変数に格納され、プラスとマイナスのボタンでコントロールすることができます。例えば、プラスボタンを 9 回押してみると、1 秒ごとにカウントが 1 ずつではなく 10 ずつ増えていくことがわかります。 -このユーザインターフェースには少し問題があります。プラス・マイナスボタンを毎秒 1 回以上の速さで押し続けると、タイマーが一時停止してしまうのです。最後にどちらかのボタンを押してから 1 秒が経過すると、タイマーが再開します。この原因を突き止め、タイマーが止まらず*毎秒*動作するように修正しましょう。 +このユーザインターフェースには小さな問題があります。プラス・マイナスボタンを毎秒 1 回以上の速さで押し続けると、タイマーが一時停止してしまうのです。最後にどちらかのボタンを押してから 1 秒が経過すると、タイマーが再開します。この原因を突き止め、タイマーが止まらず*毎秒*動作するように修正しましょう。 @@ -1151,9 +1151,9 @@ button { margin: 10px; } -問題はエフェクト内のコードが `increment` state 変数を使用していることです。この変数はエフェクトの依存値なので、`increment` を変更するたびにエフェクトが再同期し、インターバルがクリアされることになります。発火する前に毎回インターバルをクリアし続けると、タイマーが停止したように見えてしまいます。 +問題はエフェクト内のコードが `increment` state 変数を使用していることです。この変数はエフェクトの依存値になっているので、`increment` を変更するたびにエフェクトが再同期し、インターバルがクリアされることになります。発火する前に毎回インターバルをクリアし続けると、タイマーが停止したように見えてしまいます。 -この問題を解決するには、エフェクトから `onTick` エフェクトイベントを抽出します: +この問題を解決するには、エフェクトから `onTick` エフェクトイベントを分離します。 @@ -1223,13 +1223,13 @@ button { margin: 10px; } -`onTick` はエフェクトイベントなので、その中のコードはリアクティブではありません。`increment` を変更しても、エフェクトはトリガしません。 +`onTick` はエフェクトイベントなので、その中のコードはリアクティブではありません。`increment` を変更しても、エフェクトはトリガされません。 #### 遅延を調整できない問題を修正 {/*fix-a-non-adjustable-delay*/} -この例では、インターバルの遅延をカスタマイズすることができます。これは、2 つのボタンによって更新される `delay` state 変数に格納されています。しかし、`delay` が 1000 ミリ秒(つまり 1 秒)になるまで "+ 100 ms" ボタンを押しても、タイマーは非常に速く(100 ミリ秒ごとに)増えていることに気づくでしょう。`delay` の変更が無視されているようです。このバグを発見し、修正してください。 +この例では、インターバルの遅延をカスタマイズすることができます。この値は `delay` という state 変数に格納され、2 つのボタンによって更新できます。しかし、`delay` が 1000 ミリ秒(つまり 1 秒)になるまで "+ 100 ms" ボタンを押しても、タイマーは非常に速く(100 ミリ秒ごとに)増えていることに気づくでしょう。`delay` の変更が無視されているようです。バグを特定し、修正してください。 @@ -1322,7 +1322,7 @@ button { margin: 10px; } -上記の例の問題点は、コードが実際に何をすべきかを考えずに `onMount` というエフェクトイベントを抽出してしまったことです。エフェクトイベントを抽出するのは、コードの一部を非リアクティブにしたいという特別な理由があるときだけです。しかし、`setInterval` の呼び出しは `delay` state 変数に対してリアクティブであるべきです。`delay` が変更された場合、インターバルを最初から設定する必要があります! このコードを修正するには、すべてのリアクティブなコードをエフェクトの内部に引き戻します: +上記の例の問題点は、コードが実際に何をすべきかを考えずに `onMount` というエフェクトイベントを抽出してしまったことです。エフェクトイベントを抽出するのは、コードの一部を非リアクティブにしたいという特別な理由があるときだけです。しかし、`setInterval` の呼び出しは `delay` state 変数に対してリアクティブであるべきです。`delay` が変更された場合、インターバルを最初から設定する必要があります! このコードを修正するには、すべてのリアクティブなコードをエフェクトの内部に戻します。 @@ -1402,7 +1402,7 @@ button { margin: 10px; } -一般的に、`onMount` のような、コードの*目的*ではなく*タイミング*に焦点を当てた名前の関数は疑ってかかるべきです。最初は「分かりやすい」と感じるかもしれませんが、実際にはあなたの意図を分かりづらくします。経験則から言うと、エフェクトイベントは*ユーザ*の視点から起こることに対応する必要があります。例えば、`onMessage`、`onTick`、`onVisit`、`onConnected` は、良いエフェクトイベント名です。これらのイベントの中のコードは、おそらくリアクティブである必要はないでしょう。一方、`onMount`、`onUpdate`、`onUnmount`、`onAfterRender` は汎用的すぎる名前のため、リアクティブにすべきコードを誤って入れてしまうことがあります。このため、エフェクトイベントの名前は、コードがいつ実行されるかではなく、*ユーザの視点から何が起こったのか*を基準にして付けるべきなのです。 +一般的に、`onMount` のような、コードの*目的*ではなく*タイミング*に焦点を当てた名前の関数は疑ってかかるべきです。最初は「分かりやすい」と感じるかもしれませんが、実際にはあなたの意図を分かりづらくします。経験則から言うと、エフェクトイベントは*ユーザ*の視点から起こることに対応する必要があります。例えば、`onMessage`、`onTick`、`onVisit`、`onConnected` は、良いエフェクトイベント名です。これらのイベントの中のコードは、おそらくリアクティブである必要はないでしょう。一方、`onMount`、`onUpdate`、`onUnmount`、`onAfterRender` は名前は過度に汎用的なものであり、リアクティブにすべきコードを誤って入れてしまうことがあります。このため、エフェクトイベントの名前は、コードがいつ実行されるかではなく、*ユーザの視点から何が起こったのか*を基準にして付けるべきなのです。 @@ -1410,13 +1410,13 @@ button { margin: 10px; } チャットルームに参加すると、このコンポーネントは通知を表示します。しかし、このコンポーネントはすぐに通知を表示するわけではありません。その代わり、ユーザが UI を見て回る機会があるように、通知を意図的に 2 秒遅らせて表示します。 -これはほとんど機能しますが、バグがあります。ドロップダウンを "general" から "travel"、そして "music" へと素早く変えてみてください。十分な速さで行うと、2 つの通知が表示されますが(予想通り!)、どちらも "Welcome to music" と表示されます。 +ほぼ機能していますが、バグがあります。ドロップダウンを "general" から "travel"、そして "music" へと素早く切り替えてみてください。十分な速さで行うと通知が 2 つ表示されますが(これ自体は期待通り)、どちらも "Welcome to music" と表示されてしまっています。 -"general" から "travel"、そして "music" に素早く切り替えると、2 つの通知が表示され、1 つ目は "Welcome to travel"、2 つ目は "Welcome to music" と表示されるように修正してください。(追加の課題として、*すでに*通知が正しい部屋を表示するようになっていると仮定して、後者の通知のみが表示されるようにコードを変更してみてください。) +"general" から "travel"、そして "music" に素早く切り替えると、2 つの通知が表示され、1 つ目は "Welcome to travel"、2 つ目は "Welcome to music" となるように修正してください。(追加の課題として、ちゃんと 2 個の通知が正しい部屋を表示するようにした後で、後者の通知のみが表示されるようにコードを修正してみてください。) -エフェクトはどのルームに接続したかを知っています。エフェクトイベントに渡したい情報はありますか? +エフェクトはどのルームに接続したかを知っています。エフェクトイベントに渡したい情報がありませんか? @@ -1557,9 +1557,9 @@ label { display: block; margin-top: 10px; } エフェクトイベントの内部では、`roomId` は*エフェクトイベントが呼び出された時点*の値です。 -今回のエフェクトイベントは、2 秒間の遅延を伴って呼び出されます。travel ルームから music ルームに素早く切り替える場合、travel ルームの通知が表示される頃には、`roomId` は既に `"music"` になっています。そのため、両方の通知で "Welcome to music" と表示されます。 +今回のエフェクトイベントは、2 秒間の遅延を伴って呼び出されます。travel ルームから music ルームに素早く切り替える場合、travel ルームの通知が表示される頃には、`roomId` は既に `"music"` になっています。そのため、両方の通知で "Welcome to music" と表示されてしまったのです。 -この問題を解決するには、エフェクトイベントの中で*最新の* `roomId` を読み込むのではなく、以下の `connectedRoomId` のように、エフェクトイベントのパラメータとして指定するようにします。そして、`onConnected(roomId)` のように呼び出すことで、エフェクトから `roomId` を渡します: +この問題を解決するには、エフェクトイベントの中で*最新の* `roomId` を読み込むのではなく、以下の `connectedRoomId` のように、エフェクトイベントのパラメータとして指定するようにします。そして、`onConnected(roomId)` のように呼び出すことで、エフェクトから `roomId` を渡すようにします。 @@ -1696,7 +1696,7 @@ label { display: block; margin-top: 10px; } `roomId` が `"travel"` に設定された(つまり `"travel"` ルームに接続したときの)エフェクトは、`"travel"` の通知を表示します。`roomId` が `"music"` に設定された(つまり `"music"` ルームに接続したときの)エフェクトは、`"music"` に対する通知を表示します。つまり、`connectedRoomId` はエフェクト(リアクティブなもの)に由来し、一方で `theme` は常に最新の値を使用するのです。 -追加の課題を解決するには、通知のタイムアウト ID を保存し、エフェクトのクリーンアップ関数でクリアしてください: +追加の課題を解決するには、通知のタイムアウト ID を保存し、エフェクトのクリーンアップ関数でクリアするようにしてください。 @@ -1837,7 +1837,7 @@ label { display: block; margin-top: 10px; } -これにより、部屋を変更したときに、すでに予定されている(まだ表示されていない)通知がキャンセルされるようになります。 +これによりルームを変更した際に、すでにスケジュール済み(だが未表示)の通知がキャンセルされるようになります。 diff --git a/src/sidebarLearn.json b/src/sidebarLearn.json index 306ba7f04..ed7b0d3db 100644 --- a/src/sidebarLearn.json +++ b/src/sidebarLearn.json @@ -189,7 +189,7 @@ "path": "/learn/lifecycle-of-reactive-effects" }, { - "title": "イベントとエフェクトを切り離す", + "title": "エフェクトからイベントを分離する", "path": "/learn/separating-events-from-effects" }, {